diff --git a/.flake8 b/.flake8
index 5735456ae7d..4ff88403244 100644
--- a/.flake8
+++ b/.flake8
@@ -29,6 +29,8 @@ ignore =
B950,
W191,
E124, # closing bracket, irritating while writing QB code
+ E131, # continuation line unaligned for hanging indent
+ E123, # closing bracket does not match indentation of opening bracket's line
max-line-length = 200
exclude=.github/helper/semgrep_rules
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index b5e46fb8107..9b5ea2f58ab 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -17,3 +17,6 @@ f0bcb753fb7ebbb64bb0d6906d431d002f0f7d8f
# imports cleanup
4b2be2999f2203493b49bf74c5b440d49e38b5e3
+
+# formatting with black
+c07713b860505211db2af685e2e950bf5dd7dd3a
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index e7f46410e6c..f9a7a024aea 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -2,6 +2,13 @@
set -e
+# Check for merge conflicts before proceeding
+python -m compileall -f "${GITHUB_WORKSPACE}"
+if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
+ then echo "Found merge conflicts"
+ exit 1
+fi
+
cd ~ || exit
sudo apt-get install redis-server libcups2-dev
diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml
index 4b1147e79f9..5b607a99406 100644
--- a/.github/workflows/docker-release.yml
+++ b/.github/workflows/docker-release.yml
@@ -11,4 +11,4 @@ jobs:
- name: curl
run: |
apk add curl bash
- curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token ${{ secrets.TRAVIS_CI_TOKEN }}" -d '{"request":{"branch":"master"}}' https://api.travis-ci.com/repo/frappe%2Ffrappe_docker/requests
+ curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer ${{ secrets.CI_PAT }}" https://api.github.com/repos/frappe/frappe_docker/actions/workflows/build_stable.yml/dispatches -d '{"ref":"main"}'
diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml
index 54b381d7f89..30ca22aedc5 100644
--- a/.github/workflows/patch.yml
+++ b/.github/workflows/patch.yml
@@ -5,7 +5,6 @@ on:
paths-ignore:
- '**.js'
- '**.md'
- types: [opened, unlabeled, synchronize, reopened]
workflow_dispatch:
@@ -30,11 +29,6 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- - name: Check for merge conficts label
- if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
- run: |
- echo "Remove merge conflicts and remove conflict label to run CI"
- exit 1
- name: Clone
uses: actions/checkout@v2
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000000..5a46002820c
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,31 @@
+name: Generate Semantic Release
+on:
+ push:
+ branches:
+ - version-13
+jobs:
+ release:
+ name: Release
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Entire Repository
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Setup Node.js v14
+ uses: actions/setup-node@v2
+ with:
+ node-version: 14
+ - name: Setup dependencies
+ run: |
+ npm install @semantic-release/git @semantic-release/exec --no-save
+ - name: Create Release
+ env:
+ GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
+ GIT_AUTHOR_NAME: "Frappe PR Bot"
+ GIT_AUTHOR_EMAIL: "developers@frappe.io"
+ GIT_COMMITTER_NAME: "Frappe PR Bot"
+ GIT_COMMITTER_EMAIL: "developers@frappe.io"
+ run: npx semantic-release
\ No newline at end of file
diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml
index 1c9743c5700..c62622eecec 100644
--- a/.github/workflows/server-tests.yml
+++ b/.github/workflows/server-tests.yml
@@ -5,7 +5,6 @@ on:
paths-ignore:
- '**.js'
- '**.md'
- types: [opened, unlabeled, synchronize, reopened]
workflow_dispatch:
push:
branches: [ develop ]
@@ -40,12 +39,6 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- - name: Check for merge conficts label
- if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
- run: |
- echo "Remove merge conflicts and remove conflict label to run CI"
- exit 1
-
- name: Clone
uses: actions/checkout@v2
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b74d9a640da..dc3011f050f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -26,12 +26,19 @@ repos:
args: ['--config', '.github/helper/.flake8_strict']
exclude: ".*setup.py$"
+ - repo: https://github.com/adityahase/black
+ rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119
+ hooks:
+ - id: black
+ additional_dependencies: ['click==8.0.4']
+
- repo: https://github.com/timothycrosley/isort
rev: 5.9.1
hooks:
- id: isort
exclude: ".*setup.py$"
+
ci:
autoupdate_schedule: weekly
skip: []
diff --git a/.releaserc b/.releaserc
new file mode 100644
index 00000000000..8a758ed30a6
--- /dev/null
+++ b/.releaserc
@@ -0,0 +1,24 @@
+{
+ "branches": ["version-13"],
+ "plugins": [
+ "@semantic-release/commit-analyzer", {
+ "preset": "angular",
+ "releaseRules": [
+ {"breaking": true, "release": false}
+ ]
+ },
+ "@semantic-release/release-notes-generator",
+ [
+ "@semantic-release/exec", {
+ "prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" erpnext/__init__.py'
+ }
+ ],
+ [
+ "@semantic-release/git", {
+ "assets": ["erpnext/__init__.py"],
+ "message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}"
+ }
+ ],
+ "@semantic-release/github"
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 5f87d7b5538..c77d8bca032 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -1,53 +1,60 @@
-
import inspect
import frappe
from erpnext.hooks import regional_overrides
-__version__ = '13.2.0'
+__version__ = "13.27.0"
+
def get_default_company(user=None):
- '''Get default company for user'''
+ """Get default company for user"""
from frappe.defaults import get_user_default_as_list
if not user:
user = frappe.session.user
- companies = get_user_default_as_list(user, 'company')
+ companies = get_user_default_as_list(user, "company")
if companies:
default_company = companies[0]
else:
- default_company = frappe.db.get_single_value('Global Defaults', 'default_company')
+ default_company = frappe.db.get_single_value("Global Defaults", "default_company")
return default_company
def get_default_currency():
- '''Returns the currency of the default company'''
+ """Returns the currency of the default company"""
company = get_default_company()
if company:
- return frappe.get_cached_value('Company', company, 'default_currency')
+ return frappe.get_cached_value("Company", company, "default_currency")
+
def get_default_cost_center(company):
- '''Returns the default cost center of the company'''
+ """Returns the default cost center of the company"""
if not company:
return None
if not frappe.flags.company_cost_center:
frappe.flags.company_cost_center = {}
if not company in frappe.flags.company_cost_center:
- frappe.flags.company_cost_center[company] = frappe.get_cached_value('Company', company, 'cost_center')
+ frappe.flags.company_cost_center[company] = frappe.get_cached_value(
+ "Company", company, "cost_center"
+ )
return frappe.flags.company_cost_center[company]
+
def get_company_currency(company):
- '''Returns the default company currency'''
+ """Returns the default company currency"""
if not frappe.flags.company_currency:
frappe.flags.company_currency = {}
if not company in frappe.flags.company_currency:
- frappe.flags.company_currency[company] = frappe.db.get_value('Company', company, 'default_currency', cache=True)
+ frappe.flags.company_currency[company] = frappe.db.get_value(
+ "Company", company, "default_currency", cache=True
+ )
return frappe.flags.company_currency[company]
+
def set_perpetual_inventory(enable=1, company=None):
if not company:
company = "_Test Company" if frappe.flags.in_test else get_default_company()
@@ -56,9 +63,10 @@ def set_perpetual_inventory(enable=1, company=None):
company.enable_perpetual_inventory = enable
company.save()
+
def encode_company_abbr(name, company=None, abbr=None):
- '''Returns name encoded with company abbreviation'''
- company_abbr = abbr or frappe.get_cached_value('Company', company, "abbr")
+ """Returns name encoded with company abbreviation"""
+ company_abbr = abbr or frappe.get_cached_value("Company", company, "abbr")
parts = name.rsplit(" - ", 1)
if parts[-1].lower() != company_abbr.lower():
@@ -66,65 +74,73 @@ def encode_company_abbr(name, company=None, abbr=None):
return " - ".join(parts)
+
def is_perpetual_inventory_enabled(company):
if not company:
company = "_Test Company" if frappe.flags.in_test else get_default_company()
- if not hasattr(frappe.local, 'enable_perpetual_inventory'):
+ if not hasattr(frappe.local, "enable_perpetual_inventory"):
frappe.local.enable_perpetual_inventory = {}
if not company in frappe.local.enable_perpetual_inventory:
- frappe.local.enable_perpetual_inventory[company] = frappe.get_cached_value('Company',
- company, "enable_perpetual_inventory") or 0
+ frappe.local.enable_perpetual_inventory[company] = (
+ frappe.get_cached_value("Company", company, "enable_perpetual_inventory") or 0
+ )
return frappe.local.enable_perpetual_inventory[company]
+
def get_default_finance_book(company=None):
if not company:
company = get_default_company()
- if not hasattr(frappe.local, 'default_finance_book'):
+ if not hasattr(frappe.local, "default_finance_book"):
frappe.local.default_finance_book = {}
if not company in frappe.local.default_finance_book:
- frappe.local.default_finance_book[company] = frappe.get_cached_value('Company',
- company, "default_finance_book")
+ frappe.local.default_finance_book[company] = frappe.get_cached_value(
+ "Company", company, "default_finance_book"
+ )
return frappe.local.default_finance_book[company]
+
def get_party_account_type(party_type):
- if not hasattr(frappe.local, 'party_account_types'):
+ if not hasattr(frappe.local, "party_account_types"):
frappe.local.party_account_types = {}
if not party_type in frappe.local.party_account_types:
- frappe.local.party_account_types[party_type] = frappe.db.get_value("Party Type",
- party_type, "account_type") or ''
+ frappe.local.party_account_types[party_type] = (
+ frappe.db.get_value("Party Type", party_type, "account_type") or ""
+ )
return frappe.local.party_account_types[party_type]
+
def get_region(company=None):
- '''Return the default country based on flag, company or global settings
+ """Return the default country based on flag, company or global settings
You can also set global company flag in `frappe.flags.company`
- '''
+ """
if company or frappe.flags.company:
- return frappe.get_cached_value('Company',
- company or frappe.flags.company, 'country')
+ return frappe.get_cached_value("Company", company or frappe.flags.company, "country")
elif frappe.flags.country:
return frappe.flags.country
else:
- return frappe.get_system_settings('country')
+ return frappe.get_system_settings("country")
+
def allow_regional(fn):
- '''Decorator to make a function regionally overridable
+ """Decorator to make a function regionally overridable
Example:
@erpnext.allow_regional
def myfunction():
- pass'''
+ pass"""
+
def caller(*args, **kwargs):
region = get_region()
- fn_name = inspect.getmodule(fn).__name__ + '.' + fn.__name__
+ fn_name = inspect.getmodule(fn).__name__ + "." + fn.__name__
if region in regional_overrides and fn_name in regional_overrides[region]:
return frappe.get_attr(regional_overrides[region][fn_name])(*args, **kwargs)
else:
@@ -132,10 +148,16 @@ def allow_regional(fn):
return caller
+
def get_last_membership(member):
- '''Returns last membership if exists'''
- last_membership = frappe.get_all('Membership', 'name,to_date,membership_type',
- dict(member=member, paid=1), order_by='to_date desc', limit=1)
+ """Returns last membership if exists"""
+ last_membership = frappe.get_all(
+ "Membership",
+ "name,to_date,membership_type",
+ dict(member=member, paid=1),
+ order_by="to_date desc",
+ limit=1,
+ )
if last_membership:
return last_membership[0]
diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py
index a6d08d8ff61..d844766e899 100644
--- a/erpnext/accounts/custom/address.py
+++ b/erpnext/accounts/custom/address.py
@@ -23,29 +23,31 @@ class ERPNextAddress(Address):
if self.is_your_company_address and not [
row for row in self.links if row.link_doctype == "Company"
]:
- frappe.throw(_("Address needs to be linked to a Company. Please add a row for Company in the Links table."),
- title=_("Company Not Linked"))
+ frappe.throw(
+ _("Address needs to be linked to a Company. Please add a row for Company in the Links table."),
+ title=_("Company Not Linked"),
+ )
def on_update(self):
"""
After Address is updated, update the related 'Primary Address' on Customer.
"""
address_display = get_address_display(self.as_dict())
- filters = { "customer_primary_address": self.name }
+ filters = {"customer_primary_address": self.name}
customers = frappe.db.get_all("Customer", filters=filters, as_list=True)
for customer_name in customers:
frappe.db.set_value("Customer", customer_name[0], "primary_address", address_display)
+
@frappe.whitelist()
-def get_shipping_address(company, address = None):
+def get_shipping_address(company, address=None):
filters = [
["Dynamic Link", "link_doctype", "=", "Company"],
["Dynamic Link", "link_name", "=", company],
- ["Address", "is_your_company_address", "=", 1]
+ ["Address", "is_your_company_address", "=", 1],
]
fields = ["*"]
- if address and frappe.db.get_value('Dynamic Link',
- {'parent': address, 'link_name': company}):
+ if address and frappe.db.get_value("Dynamic Link", {"parent": address, "link_name": company}):
filters.append(["Address", "name", "=", address])
if not address:
filters.append(["Address", "is_shipping_address", "=", 1])
diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
index 1c1364ed111..fefec0ee7b4 100644
--- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
+++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
@@ -12,15 +12,24 @@ from frappe.utils.nestedset import get_descendants_of
@frappe.whitelist()
@cache_source
-def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None,
- to_date = None, timespan = None, time_interval = None, heatmap_year = None):
+def get(
+ chart_name=None,
+ chart=None,
+ no_cache=None,
+ filters=None,
+ from_date=None,
+ to_date=None,
+ timespan=None,
+ time_interval=None,
+ heatmap_year=None,
+):
if chart_name:
- chart = frappe.get_doc('Dashboard Chart', chart_name)
+ chart = frappe.get_doc("Dashboard Chart", chart_name)
else:
chart = frappe._dict(frappe.parse_json(chart))
timespan = chart.timespan
- if chart.timespan == 'Select Date Range':
+ if chart.timespan == "Select Date Range":
from_date = chart.from_date
to_date = chart.to_date
@@ -31,17 +40,23 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
company = filters.get("company")
if not account and chart_name:
- frappe.throw(_("Account is not set for the dashboard chart {0}")
- .format(get_link_to_form("Dashboard Chart", chart_name)))
+ frappe.throw(
+ _("Account is not set for the dashboard chart {0}").format(
+ get_link_to_form("Dashboard Chart", chart_name)
+ )
+ )
if not frappe.db.exists("Account", account) and chart_name:
- frappe.throw(_("Account {0} does not exists in the dashboard chart {1}")
- .format(account, get_link_to_form("Dashboard Chart", chart_name)))
+ frappe.throw(
+ _("Account {0} does not exists in the dashboard chart {1}").format(
+ account, get_link_to_form("Dashboard Chart", chart_name)
+ )
+ )
if not to_date:
to_date = nowdate()
if not from_date:
- if timegrain in ('Monthly', 'Quarterly'):
+ if timegrain in ("Monthly", "Quarterly"):
from_date = get_from_date_from_timespan(to_date, timespan)
# fetch dates to plot
@@ -54,16 +69,14 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
result = build_result(account, dates, gl_entries)
return {
- "labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result],
- "datasets": [{
- "name": account,
- "values": [r[1] for r in result]
- }]
+ "labels": [formatdate(r[0].strftime("%Y-%m-%d")) for r in result],
+ "datasets": [{"name": account, "values": [r[1] for r in result]}],
}
+
def build_result(account, dates, gl_entries):
result = [[getdate(date), 0.0] for date in dates]
- root_type = frappe.db.get_value('Account', account, 'root_type')
+ root_type = frappe.db.get_value("Account", account, "root_type")
# start with the first date
date_index = 0
@@ -78,30 +91,34 @@ def build_result(account, dates, gl_entries):
result[date_index][1] += entry.debit - entry.credit
# if account type is credit, switch balances
- if root_type not in ('Asset', 'Expense'):
+ if root_type not in ("Asset", "Expense"):
for r in result:
r[1] = -1 * r[1]
# for balance sheet accounts, the totals are cumulative
- if root_type in ('Asset', 'Liability', 'Equity'):
+ if root_type in ("Asset", "Liability", "Equity"):
for i, r in enumerate(result):
if i > 0:
- r[1] = r[1] + result[i-1][1]
+ r[1] = r[1] + result[i - 1][1]
return result
+
def get_gl_entries(account, to_date):
- child_accounts = get_descendants_of('Account', account, ignore_permissions=True)
+ child_accounts = get_descendants_of("Account", account, ignore_permissions=True)
child_accounts.append(account)
- return frappe.db.get_all('GL Entry',
- fields = ['posting_date', 'debit', 'credit'],
- filters = [
- dict(posting_date = ('<', to_date)),
- dict(account = ('in', child_accounts)),
- dict(voucher_type = ('!=', 'Period Closing Voucher'))
+ return frappe.db.get_all(
+ "GL Entry",
+ fields=["posting_date", "debit", "credit"],
+ filters=[
+ dict(posting_date=("<", to_date)),
+ dict(account=("in", child_accounts)),
+ dict(voucher_type=("!=", "Period Closing Voucher")),
],
- order_by = 'posting_date asc')
+ order_by="posting_date asc",
+ )
+
def get_dates_from_timegrain(from_date, to_date, timegrain):
days = months = years = 0
@@ -116,6 +133,8 @@ def get_dates_from_timegrain(from_date, to_date, timegrain):
dates = [get_period_ending(from_date, timegrain)]
while getdate(dates[-1]) < getdate(to_date):
- date = get_period_ending(add_to_date(dates[-1], years=years, months=months, days=days), timegrain)
+ date = get_period_ending(
+ add_to_date(dates[-1], years=years, months=months, days=days), timegrain
+ )
dates.append(date)
return dates
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index 46b7dc6a2a6..a8776fa3448 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -1,4 +1,3 @@
-
import frappe
from frappe import _
from frappe.email import sendmail_to_system_managers
@@ -23,20 +22,23 @@ from erpnext.accounts.utils import get_account_currency
def validate_service_stop_date(doc):
- ''' Validates service_stop_date for Purchase Invoice and Sales Invoice '''
+ """Validates service_stop_date for Purchase Invoice and Sales Invoice"""
- enable_check = "enable_deferred_revenue" \
- if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
+ enable_check = (
+ "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
+ )
old_stop_dates = {}
- old_doc = frappe.db.get_all("{0} Item".format(doc.doctype),
- {"parent": doc.name}, ["name", "service_stop_date"])
+ old_doc = frappe.db.get_all(
+ "{0} Item".format(doc.doctype), {"parent": doc.name}, ["name", "service_stop_date"]
+ )
for d in old_doc:
old_stop_dates[d.name] = d.service_stop_date or ""
for item in doc.items:
- if not item.get(enable_check): continue
+ if not item.get(enable_check):
+ continue
if item.service_stop_date:
if date_diff(item.service_stop_date, item.service_start_date) < 0:
@@ -45,21 +47,31 @@ def validate_service_stop_date(doc):
if date_diff(item.service_stop_date, item.service_end_date) > 0:
frappe.throw(_("Service Stop Date cannot be after Service End Date"))
- if old_stop_dates and old_stop_dates.get(item.name) and item.service_stop_date!=old_stop_dates.get(item.name):
+ if (
+ old_stop_dates
+ and old_stop_dates.get(item.name)
+ and item.service_stop_date != old_stop_dates.get(item.name)
+ ):
frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))
+
def build_conditions(process_type, account, company):
- conditions=''
- deferred_account = "item.deferred_revenue_account" if process_type=="Income" else "item.deferred_expense_account"
+ conditions = ""
+ deferred_account = (
+ "item.deferred_revenue_account" if process_type == "Income" else "item.deferred_expense_account"
+ )
if account:
- conditions += "AND %s='%s'"%(deferred_account, account)
+ conditions += "AND %s='%s'" % (deferred_account, account)
elif company:
conditions += f"AND p.company = {frappe.db.escape(company)}"
return conditions
-def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_date=None, conditions=''):
+
+def convert_deferred_expense_to_expense(
+ deferred_process, start_date=None, end_date=None, conditions=""
+):
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
if not start_date:
@@ -68,14 +80,19 @@ def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_d
end_date = add_days(today(), -1)
# check for the purchase invoice for which GL entries has to be done
- invoices = frappe.db.sql_list('''
+ invoices = frappe.db.sql_list(
+ """
select distinct item.parent
from `tabPurchase Invoice Item` item, `tabPurchase Invoice` p
where item.service_start_date<=%s and item.service_end_date>=%s
and item.enable_deferred_expense = 1 and item.parent=p.name
and item.docstatus = 1 and ifnull(item.amount, 0) > 0
{0}
- '''.format(conditions), (end_date, start_date)) #nosec
+ """.format(
+ conditions
+ ),
+ (end_date, start_date),
+ ) # nosec
# For each invoice, book deferred expense
for invoice in invoices:
@@ -85,7 +102,10 @@ def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_d
if frappe.flags.deferred_accounting_error:
send_mail(deferred_process)
-def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_date=None, conditions=''):
+
+def convert_deferred_revenue_to_income(
+ deferred_process, start_date=None, end_date=None, conditions=""
+):
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
if not start_date:
@@ -94,14 +114,19 @@ def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_da
end_date = add_days(today(), -1)
# check for the sales invoice for which GL entries has to be done
- invoices = frappe.db.sql_list('''
+ invoices = frappe.db.sql_list(
+ """
select distinct item.parent
from `tabSales Invoice Item` item, `tabSales Invoice` p
where item.service_start_date<=%s and item.service_end_date>=%s
and item.enable_deferred_revenue = 1 and item.parent=p.name
and item.docstatus = 1 and ifnull(item.amount, 0) > 0
{0}
- '''.format(conditions), (end_date, start_date)) #nosec
+ """.format(
+ conditions
+ ),
+ (end_date, start_date),
+ ) # nosec
for invoice in invoices:
doc = frappe.get_doc("Sales Invoice", invoice)
@@ -110,31 +135,43 @@ def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_da
if frappe.flags.deferred_accounting_error:
send_mail(deferred_process)
+
def get_booking_dates(doc, item, posting_date=None):
if not posting_date:
posting_date = add_days(today(), -1)
last_gl_entry = False
- deferred_account = "deferred_revenue_account" if doc.doctype=="Sales Invoice" else "deferred_expense_account"
+ deferred_account = (
+ "deferred_revenue_account" if doc.doctype == "Sales Invoice" else "deferred_expense_account"
+ )
- prev_gl_entry = frappe.db.sql('''
+ prev_gl_entry = frappe.db.sql(
+ """
select name, posting_date from `tabGL Entry` where company=%s and account=%s and
voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
and is_cancelled = 0
order by posting_date desc limit 1
- ''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
+ """,
+ (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
+ as_dict=True,
+ )
- prev_gl_via_je = frappe.db.sql('''
+ prev_gl_via_je = frappe.db.sql(
+ """
SELECT p.name, p.posting_date FROM `tabJournal Entry` p, `tabJournal Entry Account` c
WHERE p.name = c.parent and p.company=%s and c.account=%s
and c.reference_type=%s and c.reference_name=%s
and c.reference_detail_no=%s and c.docstatus < 2 order by posting_date desc limit 1
- ''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
+ """,
+ (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
+ as_dict=True,
+ )
if prev_gl_via_je:
- if (not prev_gl_entry) or (prev_gl_entry and
- prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date):
+ if (not prev_gl_entry) or (
+ prev_gl_entry and prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date
+ ):
prev_gl_entry = prev_gl_via_je
if prev_gl_entry:
@@ -158,66 +195,94 @@ def get_booking_dates(doc, item, posting_date=None):
else:
return None, None, None
-def calculate_monthly_amount(doc, item, last_gl_entry, start_date, end_date, total_days, total_booking_days, account_currency):
+
+def calculate_monthly_amount(
+ doc, item, last_gl_entry, start_date, end_date, total_days, total_booking_days, account_currency
+):
amount, base_amount = 0, 0
if not last_gl_entry:
- total_months = (item.service_end_date.year - item.service_start_date.year) * 12 + \
- (item.service_end_date.month - item.service_start_date.month) + 1
+ total_months = (
+ (item.service_end_date.year - item.service_start_date.year) * 12
+ + (item.service_end_date.month - item.service_start_date.month)
+ + 1
+ )
- prorate_factor = flt(date_diff(item.service_end_date, item.service_start_date)) \
- / flt(date_diff(get_last_day(item.service_end_date), get_first_day(item.service_start_date)))
+ prorate_factor = flt(date_diff(item.service_end_date, item.service_start_date)) / flt(
+ date_diff(get_last_day(item.service_end_date), get_first_day(item.service_start_date))
+ )
actual_months = rounded(total_months * prorate_factor, 1)
- already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item)
+ already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
+ doc, item
+ )
base_amount = flt(item.base_net_amount / actual_months, item.precision("base_net_amount"))
if base_amount + already_booked_amount > item.base_net_amount:
base_amount = item.base_net_amount - already_booked_amount
- if account_currency==doc.company_currency:
+ if account_currency == doc.company_currency:
amount = base_amount
else:
- amount = flt(item.net_amount/actual_months, item.precision("net_amount"))
+ amount = flt(item.net_amount / actual_months, item.precision("net_amount"))
if amount + already_booked_amount_in_account_currency > item.net_amount:
amount = item.net_amount - already_booked_amount_in_account_currency
if not (get_first_day(start_date) == start_date and get_last_day(end_date) == end_date):
- partial_month = flt(date_diff(end_date, start_date)) \
- / flt(date_diff(get_last_day(end_date), get_first_day(start_date)))
+ partial_month = flt(date_diff(end_date, start_date)) / flt(
+ date_diff(get_last_day(end_date), get_first_day(start_date))
+ )
base_amount = rounded(partial_month, 1) * base_amount
amount = rounded(partial_month, 1) * amount
else:
- already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item)
- base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount"))
- if account_currency==doc.company_currency:
+ already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
+ doc, item
+ )
+ base_amount = flt(
+ item.base_net_amount - already_booked_amount, item.precision("base_net_amount")
+ )
+ if account_currency == doc.company_currency:
amount = base_amount
else:
- amount = flt(item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount"))
+ amount = flt(
+ item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount")
+ )
return amount, base_amount
+
def calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency):
amount, base_amount = 0, 0
if not last_gl_entry:
- base_amount = flt(item.base_net_amount*total_booking_days/flt(total_days), item.precision("base_net_amount"))
- if account_currency==doc.company_currency:
+ base_amount = flt(
+ item.base_net_amount * total_booking_days / flt(total_days), item.precision("base_net_amount")
+ )
+ if account_currency == doc.company_currency:
amount = base_amount
else:
- amount = flt(item.net_amount*total_booking_days/flt(total_days), item.precision("net_amount"))
+ amount = flt(
+ item.net_amount * total_booking_days / flt(total_days), item.precision("net_amount")
+ )
else:
- already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item)
+ already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
+ doc, item
+ )
- base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount"))
- if account_currency==doc.company_currency:
+ base_amount = flt(
+ item.base_net_amount - already_booked_amount, item.precision("base_net_amount")
+ )
+ if account_currency == doc.company_currency:
amount = base_amount
else:
- amount = flt(item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount"))
+ amount = flt(
+ item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount")
+ )
return amount, base_amount
+
def get_already_booked_amount(doc, item):
if doc.doctype == "Sales Invoice":
total_credit_debit, total_credit_debit_currency = "debit", "debit_in_account_currency"
@@ -226,21 +291,31 @@ def get_already_booked_amount(doc, item):
total_credit_debit, total_credit_debit_currency = "credit", "credit_in_account_currency"
deferred_account = "deferred_expense_account"
- gl_entries_details = frappe.db.sql('''
+ gl_entries_details = frappe.db.sql(
+ """
select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no
from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
and is_cancelled = 0
group by voucher_detail_no
- '''.format(total_credit_debit, total_credit_debit_currency),
- (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
+ """.format(
+ total_credit_debit, total_credit_debit_currency
+ ),
+ (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
+ as_dict=True,
+ )
- journal_entry_details = frappe.db.sql('''
+ journal_entry_details = frappe.db.sql(
+ """
SELECT sum(c.{0}) as total_credit, sum(c.{1}) as total_credit_in_account_currency, reference_detail_no
FROM `tabJournal Entry` p , `tabJournal Entry Account` c WHERE p.name = c.parent and
p.company = %s and c.account=%s and c.reference_type=%s and c.reference_name=%s and c.reference_detail_no=%s
and p.docstatus < 2 group by reference_detail_no
- '''.format(total_credit_debit, total_credit_debit_currency),
- (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
+ """.format(
+ total_credit_debit, total_credit_debit_currency
+ ),
+ (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
+ as_dict=True,
+ )
already_booked_amount = gl_entries_details[0].total_credit if gl_entries_details else 0
already_booked_amount += journal_entry_details[0].total_credit if journal_entry_details else 0
@@ -248,20 +323,29 @@ def get_already_booked_amount(doc, item):
if doc.currency == doc.company_currency:
already_booked_amount_in_account_currency = already_booked_amount
else:
- already_booked_amount_in_account_currency = gl_entries_details[0].total_credit_in_account_currency if gl_entries_details else 0
- already_booked_amount_in_account_currency += journal_entry_details[0].total_credit_in_account_currency if journal_entry_details else 0
+ already_booked_amount_in_account_currency = (
+ gl_entries_details[0].total_credit_in_account_currency if gl_entries_details else 0
+ )
+ already_booked_amount_in_account_currency += (
+ journal_entry_details[0].total_credit_in_account_currency if journal_entry_details else 0
+ )
return already_booked_amount, already_booked_amount_in_account_currency
+
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
- enable_check = "enable_deferred_revenue" \
- if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
+ enable_check = (
+ "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
+ )
- accounts_frozen_upto = frappe.get_cached_value('Accounts Settings', 'None', 'acc_frozen_upto')
+ accounts_frozen_upto = frappe.get_cached_value("Accounts Settings", "None", "acc_frozen_upto")
- def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on):
+ def _book_deferred_revenue_or_expense(
+ item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
+ ):
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
- if not (start_date and end_date): return
+ if not (start_date and end_date):
+ return
account_currency = get_account_currency(item.expense_account or item.income_account)
if doc.doctype == "Sales Invoice":
@@ -274,12 +358,21 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
total_days = date_diff(item.service_end_date, item.service_start_date) + 1
total_booking_days = date_diff(end_date, start_date) + 1
- if book_deferred_entries_based_on == 'Months':
- amount, base_amount = calculate_monthly_amount(doc, item, last_gl_entry,
- start_date, end_date, total_days, total_booking_days, account_currency)
+ if book_deferred_entries_based_on == "Months":
+ amount, base_amount = calculate_monthly_amount(
+ doc,
+ item,
+ last_gl_entry,
+ start_date,
+ end_date,
+ total_days,
+ total_booking_days,
+ account_currency,
+ )
else:
- amount, base_amount = calculate_amount(doc, item, last_gl_entry,
- total_days, total_booking_days, account_currency)
+ amount, base_amount = calculate_amount(
+ doc, item, last_gl_entry, total_days, total_booking_days, account_currency
+ )
if not amount:
return
@@ -289,92 +382,155 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
if via_journal_entry:
- book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
- base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
+ book_revenue_via_journal_entry(
+ doc,
+ credit_account,
+ debit_account,
+ amount,
+ base_amount,
+ end_date,
+ project,
+ account_currency,
+ item.cost_center,
+ item,
+ deferred_process,
+ submit_journal_entry,
+ )
else:
- make_gl_entries(doc, credit_account, debit_account, against,
- amount, base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process)
+ make_gl_entries(
+ doc,
+ credit_account,
+ debit_account,
+ against,
+ amount,
+ base_amount,
+ end_date,
+ project,
+ account_currency,
+ item.cost_center,
+ item,
+ deferred_process,
+ )
# Returned in case of any errors because it tries to submit the same record again and again in case of errors
if frappe.flags.deferred_accounting_error:
return
if getdate(end_date) < getdate(posting_date) and not last_gl_entry:
- _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on)
+ _book_deferred_revenue_or_expense(
+ item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
+ )
- via_journal_entry = cint(frappe.db.get_singles_value('Accounts Settings', 'book_deferred_entries_via_journal_entry'))
- submit_journal_entry = cint(frappe.db.get_singles_value('Accounts Settings', 'submit_journal_entries'))
- book_deferred_entries_based_on = frappe.db.get_singles_value('Accounts Settings', 'book_deferred_entries_based_on')
+ via_journal_entry = cint(
+ frappe.db.get_singles_value("Accounts Settings", "book_deferred_entries_via_journal_entry")
+ )
+ submit_journal_entry = cint(
+ frappe.db.get_singles_value("Accounts Settings", "submit_journal_entries")
+ )
+ book_deferred_entries_based_on = frappe.db.get_singles_value(
+ "Accounts Settings", "book_deferred_entries_based_on"
+ )
- for item in doc.get('items'):
+ for item in doc.get("items"):
if item.get(enable_check):
- _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on)
+ _book_deferred_revenue_or_expense(
+ item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
+ )
+
def process_deferred_accounting(posting_date=None):
- ''' Converts deferred income/expense into income/expense
- Executed via background jobs on every month end '''
+ """Converts deferred income/expense into income/expense
+ Executed via background jobs on every month end"""
if not posting_date:
posting_date = today()
- if not cint(frappe.db.get_singles_value('Accounts Settings', 'automatically_process_deferred_accounting_entry')):
+ if not cint(
+ frappe.db.get_singles_value(
+ "Accounts Settings", "automatically_process_deferred_accounting_entry"
+ )
+ ):
return
start_date = add_months(today(), -1)
end_date = add_days(today(), -1)
- companies = frappe.get_all('Company')
+ companies = frappe.get_all("Company")
for company in companies:
- for record_type in ('Income', 'Expense'):
- doc = frappe.get_doc(dict(
- doctype='Process Deferred Accounting',
- company=company.name,
- posting_date=posting_date,
- start_date=start_date,
- end_date=end_date,
- type=record_type
- ))
+ for record_type in ("Income", "Expense"):
+ doc = frappe.get_doc(
+ dict(
+ doctype="Process Deferred Accounting",
+ company=company.name,
+ posting_date=posting_date,
+ start_date=start_date,
+ end_date=end_date,
+ type=record_type,
+ )
+ )
doc.insert()
doc.submit()
-def make_gl_entries(doc, credit_account, debit_account, against,
- amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None):
+
+def make_gl_entries(
+ doc,
+ credit_account,
+ debit_account,
+ against,
+ amount,
+ base_amount,
+ posting_date,
+ project,
+ account_currency,
+ cost_center,
+ item,
+ deferred_process=None,
+):
# GL Entry for crediting the amount in the deferred expense
from erpnext.accounts.general_ledger import make_gl_entries
- if amount == 0: return
+ if amount == 0:
+ return
gl_entries = []
gl_entries.append(
- doc.get_gl_dict({
- "account": credit_account,
- "against": against,
- "credit": base_amount,
- "credit_in_account_currency": amount,
- "cost_center": cost_center,
- "voucher_detail_no": item.name,
- 'posting_date': posting_date,
- 'project': project,
- 'against_voucher_type': 'Process Deferred Accounting',
- 'against_voucher': deferred_process
- }, account_currency, item=item)
+ doc.get_gl_dict(
+ {
+ "account": credit_account,
+ "against": against,
+ "credit": base_amount,
+ "credit_in_account_currency": amount,
+ "cost_center": cost_center,
+ "voucher_detail_no": item.name,
+ "posting_date": posting_date,
+ "project": project,
+ "against_voucher_type": "Process Deferred Accounting",
+ "against_voucher": deferred_process,
+ },
+ account_currency,
+ item=item,
+ )
)
# GL Entry to debit the amount from the expense
gl_entries.append(
- doc.get_gl_dict({
- "account": debit_account,
- "against": against,
- "debit": base_amount,
- "debit_in_account_currency": amount,
- "cost_center": cost_center,
- "voucher_detail_no": item.name,
- 'posting_date': posting_date,
- 'project': project,
- 'against_voucher_type': 'Process Deferred Accounting',
- 'against_voucher': deferred_process
- }, account_currency, item=item)
+ doc.get_gl_dict(
+ {
+ "account": debit_account,
+ "against": against,
+ "debit": base_amount,
+ "debit_in_account_currency": amount,
+ "cost_center": cost_center,
+ "voucher_detail_no": item.name,
+ "posting_date": posting_date,
+ "project": project,
+ "against_voucher_type": "Process Deferred Accounting",
+ "against_voucher": deferred_process,
+ },
+ account_currency,
+ item=item,
+ )
)
if gl_entries:
@@ -384,68 +540,88 @@ def make_gl_entries(doc, credit_account, debit_account, against,
except Exception as e:
if frappe.flags.in_test:
traceback = frappe.get_traceback()
- frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
+ frappe.log_error(
+ title=_("Error while processing deferred accounting for Invoice {0}").format(doc.name),
+ message=traceback,
+ )
raise e
else:
frappe.db.rollback()
traceback = frappe.get_traceback()
- frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
+ frappe.log_error(
+ title=_("Error while processing deferred accounting for Invoice {0}").format(doc.name),
+ message=traceback,
+ )
frappe.flags.deferred_accounting_error = True
+
def send_mail(deferred_process):
title = _("Error while processing deferred accounting for {0}").format(deferred_process)
- link = get_link_to_form('Process Deferred Accounting', deferred_process)
+ link = get_link_to_form("Process Deferred Accounting", deferred_process)
content = _("Deferred accounting failed for some invoices:") + "\n"
- content += _("Please check Process Deferred Accounting {0} and submit manually after resolving errors.").format(link)
+ content += _(
+ "Please check Process Deferred Accounting {0} and submit manually after resolving errors."
+ ).format(link)
sendmail_to_system_managers(title, content)
-def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
- amount, base_amount, posting_date, project, account_currency, cost_center, item,
- deferred_process=None, submit='No'):
- if amount == 0: return
+def book_revenue_via_journal_entry(
+ doc,
+ credit_account,
+ debit_account,
+ amount,
+ base_amount,
+ posting_date,
+ project,
+ account_currency,
+ cost_center,
+ item,
+ deferred_process=None,
+ submit="No",
+):
- journal_entry = frappe.new_doc('Journal Entry')
+ if amount == 0:
+ return
+
+ journal_entry = frappe.new_doc("Journal Entry")
journal_entry.posting_date = posting_date
journal_entry.company = doc.company
- journal_entry.voucher_type = 'Deferred Revenue' if doc.doctype == 'Sales Invoice' \
- else 'Deferred Expense'
+ journal_entry.voucher_type = (
+ "Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense"
+ )
+ journal_entry.process_deferred_accounting = deferred_process
debit_entry = {
- 'account': credit_account,
- 'credit': base_amount,
- 'credit_in_account_currency': amount,
- 'account_currency': account_currency,
- 'reference_name': doc.name,
- 'reference_type': doc.doctype,
- 'reference_detail_no': item.name,
- 'cost_center': cost_center,
- 'project': project,
+ "account": credit_account,
+ "credit": base_amount,
+ "credit_in_account_currency": amount,
+ "account_currency": account_currency,
+ "reference_name": doc.name,
+ "reference_type": doc.doctype,
+ "reference_detail_no": item.name,
+ "cost_center": cost_center,
+ "project": project,
}
credit_entry = {
- 'account': debit_account,
- 'debit': base_amount,
- 'debit_in_account_currency': amount,
- 'account_currency': account_currency,
- 'reference_name': doc.name,
- 'reference_type': doc.doctype,
- 'reference_detail_no': item.name,
- 'cost_center': cost_center,
- 'project': project,
+ "account": debit_account,
+ "debit": base_amount,
+ "debit_in_account_currency": amount,
+ "account_currency": account_currency,
+ "reference_name": doc.name,
+ "reference_type": doc.doctype,
+ "reference_detail_no": item.name,
+ "cost_center": cost_center,
+ "project": project,
}
for dimension in get_accounting_dimensions():
- debit_entry.update({
- dimension: item.get(dimension)
- })
+ debit_entry.update({dimension: item.get(dimension)})
- credit_entry.update({
- dimension: item.get(dimension)
- })
+ credit_entry.update({dimension: item.get(dimension)})
- journal_entry.append('accounts', debit_entry)
- journal_entry.append('accounts', credit_entry)
+ journal_entry.append("accounts", debit_entry)
+ journal_entry.append("accounts", credit_entry)
try:
journal_entry.save()
@@ -457,20 +633,30 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
except Exception:
frappe.db.rollback()
traceback = frappe.get_traceback()
- frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
+ frappe.log_error(
+ title=_("Error while processing deferred accounting for Invoice {0}").format(doc.name),
+ message=traceback,
+ )
frappe.flags.deferred_accounting_error = True
+
def get_deferred_booking_accounts(doctype, voucher_detail_no, dr_or_cr):
- if doctype == 'Sales Invoice':
- credit_account, debit_account = frappe.db.get_value('Sales Invoice Item', {'name': voucher_detail_no},
- ['income_account', 'deferred_revenue_account'])
+ if doctype == "Sales Invoice":
+ credit_account, debit_account = frappe.db.get_value(
+ "Sales Invoice Item",
+ {"name": voucher_detail_no},
+ ["income_account", "deferred_revenue_account"],
+ )
else:
- credit_account, debit_account = frappe.db.get_value('Purchase Invoice Item', {'name': voucher_detail_no},
- ['deferred_expense_account', 'expense_account'])
+ credit_account, debit_account = frappe.db.get_value(
+ "Purchase Invoice Item",
+ {"name": voucher_detail_no},
+ ["deferred_expense_account", "expense_account"],
+ )
- if dr_or_cr == 'Debit':
+ if dr_or_cr == "Debit":
return debit_account
else:
return credit_account
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index f8a06c7243f..c71ea3648b9 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -10,11 +10,17 @@ from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_
import erpnext
-class RootNotEditable(frappe.ValidationError): pass
-class BalanceMismatchError(frappe.ValidationError): pass
+class RootNotEditable(frappe.ValidationError):
+ pass
+
+
+class BalanceMismatchError(frappe.ValidationError):
+ pass
+
class Account(NestedSet):
- nsm_parent_field = 'parent_account'
+ nsm_parent_field = "parent_account"
+
def on_update(self):
if frappe.local.flags.ignore_update_nsm:
return
@@ -22,17 +28,20 @@ class Account(NestedSet):
super(Account, self).on_update()
def onload(self):
- frozen_accounts_modifier = frappe.db.get_value("Accounts Settings", "Accounts Settings",
- "frozen_accounts_modifier")
+ frozen_accounts_modifier = frappe.db.get_value(
+ "Accounts Settings", "Accounts Settings", "frozen_accounts_modifier"
+ )
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
self.set_onload("can_freeze_account", True)
def autoname(self):
from erpnext.accounts.utils import get_autoname_with_number
+
self.name = get_autoname_with_number(self.account_number, self.account_name, None, self.company)
def validate(self):
from erpnext.accounts.utils import validate_field_number
+
if frappe.local.flags.allow_unverified_charts:
return
self.validate_parent()
@@ -49,22 +58,33 @@ class Account(NestedSet):
def validate_parent(self):
"""Fetch Parent Details and validate parent account"""
if self.parent_account:
- par = frappe.db.get_value("Account", self.parent_account,
- ["name", "is_group", "company"], as_dict=1)
+ par = frappe.db.get_value(
+ "Account", self.parent_account, ["name", "is_group", "company"], as_dict=1
+ )
if not par:
- throw(_("Account {0}: Parent account {1} does not exist").format(self.name, self.parent_account))
+ throw(
+ _("Account {0}: Parent account {1} does not exist").format(self.name, self.parent_account)
+ )
elif par.name == self.name:
throw(_("Account {0}: You can not assign itself as parent account").format(self.name))
elif not par.is_group:
- throw(_("Account {0}: Parent account {1} can not be a ledger").format(self.name, self.parent_account))
+ throw(
+ _("Account {0}: Parent account {1} can not be a ledger").format(
+ self.name, self.parent_account
+ )
+ )
elif par.company != self.company:
- throw(_("Account {0}: Parent account {1} does not belong to company: {2}")
- .format(self.name, self.parent_account, self.company))
+ throw(
+ _("Account {0}: Parent account {1} does not belong to company: {2}").format(
+ self.name, self.parent_account, self.company
+ )
+ )
def set_root_and_report_type(self):
if self.parent_account:
- par = frappe.db.get_value("Account", self.parent_account,
- ["report_type", "root_type"], as_dict=1)
+ par = frappe.db.get_value(
+ "Account", self.parent_account, ["report_type", "root_type"], as_dict=1
+ )
if par.report_type:
self.report_type = par.report_type
@@ -75,15 +95,20 @@ class Account(NestedSet):
db_value = frappe.db.get_value("Account", self.name, ["report_type", "root_type"], as_dict=1)
if db_value:
if self.report_type != db_value.report_type:
- frappe.db.sql("update `tabAccount` set report_type=%s where lft > %s and rgt < %s",
- (self.report_type, self.lft, self.rgt))
+ frappe.db.sql(
+ "update `tabAccount` set report_type=%s where lft > %s and rgt < %s",
+ (self.report_type, self.lft, self.rgt),
+ )
if self.root_type != db_value.root_type:
- frappe.db.sql("update `tabAccount` set root_type=%s where lft > %s and rgt < %s",
- (self.root_type, self.lft, self.rgt))
+ frappe.db.sql(
+ "update `tabAccount` set root_type=%s where lft > %s and rgt < %s",
+ (self.root_type, self.lft, self.rgt),
+ )
if self.root_type and not self.report_type:
- self.report_type = "Balance Sheet" \
- if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss"
+ self.report_type = (
+ "Balance Sheet" if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss"
+ )
def validate_root_details(self):
# does not exists parent
@@ -96,21 +121,26 @@ class Account(NestedSet):
def validate_root_company_and_sync_account_to_children(self):
# ignore validation while creating new compnay or while syncing to child companies
- if frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation:
+ if (
+ frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation
+ ):
return
ancestors = get_root_company(self.company)
if ancestors:
if frappe.get_value("Company", self.company, "allow_account_creation_against_child_company"):
return
- if not frappe.db.get_value("Account",
- {'account_name': self.account_name, 'company': ancestors[0]}, 'name'):
+ if not frappe.db.get_value(
+ "Account", {"account_name": self.account_name, "company": ancestors[0]}, "name"
+ ):
frappe.throw(_("Please add the account to root level Company - {}").format(ancestors[0]))
elif self.parent_account:
- descendants = get_descendants_of('Company', self.company)
- if not descendants: return
+ descendants = get_descendants_of("Company", self.company)
+ if not descendants:
+ return
parent_acc_name_map = {}
- parent_acc_name, parent_acc_number = frappe.db.get_value('Account', self.parent_account, \
- ["account_name", "account_number"])
+ parent_acc_name, parent_acc_number = frappe.db.get_value(
+ "Account", self.parent_account, ["account_name", "account_number"]
+ )
filters = {
"company": ["in", descendants],
"account_name": parent_acc_name,
@@ -118,10 +148,13 @@ class Account(NestedSet):
if parent_acc_number:
filters["account_number"] = parent_acc_number
- for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True):
+ for d in frappe.db.get_values(
+ "Account", filters=filters, fieldname=["company", "name"], as_dict=True
+ ):
parent_acc_name_map[d["company"]] = d["name"]
- if not parent_acc_name_map: return
+ if not parent_acc_name_map:
+ return
self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)
@@ -142,26 +175,38 @@ class Account(NestedSet):
def validate_frozen_accounts_modifier(self):
old_value = frappe.db.get_value("Account", self.name, "freeze_account")
if old_value and old_value != self.freeze_account:
- frozen_accounts_modifier = frappe.db.get_value('Accounts Settings', None, 'frozen_accounts_modifier')
- if not frozen_accounts_modifier or \
- frozen_accounts_modifier not in frappe.get_roles():
- throw(_("You are not authorized to set Frozen value"))
+ frozen_accounts_modifier = frappe.db.get_value(
+ "Accounts Settings", None, "frozen_accounts_modifier"
+ )
+ if not frozen_accounts_modifier or frozen_accounts_modifier not in frappe.get_roles():
+ throw(_("You are not authorized to set Frozen value"))
def validate_balance_must_be_debit_or_credit(self):
from erpnext.accounts.utils import get_balance_on
+
if not self.get("__islocal") and self.balance_must_be:
account_balance = get_balance_on(self.name)
if account_balance > 0 and self.balance_must_be == "Credit":
- frappe.throw(_("Account balance already in Debit, you are not allowed to set 'Balance Must Be' as 'Credit'"))
+ frappe.throw(
+ _(
+ "Account balance already in Debit, you are not allowed to set 'Balance Must Be' as 'Credit'"
+ )
+ )
elif account_balance < 0 and self.balance_must_be == "Debit":
- frappe.throw(_("Account balance already in Credit, you are not allowed to set 'Balance Must Be' as 'Debit'"))
+ frappe.throw(
+ _(
+ "Account balance already in Credit, you are not allowed to set 'Balance Must Be' as 'Debit'"
+ )
+ )
def validate_account_currency(self):
if not self.account_currency:
- self.account_currency = frappe.get_cached_value('Company', self.company, "default_currency")
+ self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
- elif self.account_currency != frappe.db.get_value("Account", self.name, "account_currency"):
+ gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency")
+
+ if gl_currency and self.account_currency != gl_currency:
if frappe.db.get_value("GL Entry", {"account": self.name}):
frappe.throw(_("Currency can not be changed after making entries using some other currency"))
@@ -170,45 +215,52 @@ class Account(NestedSet):
company_bold = frappe.bold(company)
parent_acc_name_bold = frappe.bold(parent_acc_name)
if not parent_acc_name_map.get(company):
- frappe.throw(_("While creating account for Child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA")
- .format(company_bold, parent_acc_name_bold), title=_("Account Not Found"))
+ frappe.throw(
+ _(
+ "While creating account for Child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA"
+ ).format(company_bold, parent_acc_name_bold),
+ title=_("Account Not Found"),
+ )
# validate if parent of child company account to be added is a group
- if (frappe.db.get_value("Account", self.parent_account, "is_group")
- and not frappe.db.get_value("Account", parent_acc_name_map[company], "is_group")):
- msg = _("While creating account for Child Company {0}, parent account {1} found as a ledger account.").format(company_bold, parent_acc_name_bold)
+ if frappe.db.get_value("Account", self.parent_account, "is_group") and not frappe.db.get_value(
+ "Account", parent_acc_name_map[company], "is_group"
+ ):
+ msg = _(
+ "While creating account for Child Company {0}, parent account {1} found as a ledger account."
+ ).format(company_bold, parent_acc_name_bold)
msg += "
"
- msg += _("Please convert the parent account in corresponding child company to a group account.")
+ msg += _(
+ "Please convert the parent account in corresponding child company to a group account."
+ )
frappe.throw(msg, title=_("Invalid Parent Account"))
- filters = {
- "account_name": self.account_name,
- "company": company
- }
+ filters = {"account_name": self.account_name, "company": company}
if self.account_number:
filters["account_number"] = self.account_number
- child_account = frappe.db.get_value("Account", filters, 'name')
+ child_account = frappe.db.get_value("Account", filters, "name")
if not child_account:
doc = frappe.copy_doc(self)
doc.flags.ignore_root_company_validation = True
- doc.update({
- "company": company,
- # parent account's currency should be passed down to child account's curreny
- # if it is None, it picks it up from default company currency, which might be unintended
- "account_currency": erpnext.get_company_currency(company),
- "parent_account": parent_acc_name_map[company]
- })
+ doc.update(
+ {
+ "company": company,
+ # parent account's currency should be passed down to child account's curreny
+ # if it is None, it picks it up from default company currency, which might be unintended
+ "account_currency": erpnext.get_company_currency(company),
+ "parent_account": parent_acc_name_map[company],
+ }
+ )
doc.save()
- frappe.msgprint(_("Account {0} is added in the child company {1}")
- .format(doc.name, company))
+ frappe.msgprint(_("Account {0} is added in the child company {1}").format(doc.name, company))
elif child_account:
# update the parent company's value in child companies
doc = frappe.get_doc("Account", child_account)
parent_value_changed = False
- for field in ['account_type', 'freeze_account', 'balance_must_be']:
+ for field in ["account_type", "freeze_account", "balance_must_be"]:
if doc.get(field) != self.get(field):
parent_value_changed = True
doc.set(field, self.get(field))
@@ -243,8 +295,11 @@ class Account(NestedSet):
return frappe.db.get_value("GL Entry", {"account": self.name})
def check_if_child_exists(self):
- return frappe.db.sql("""select name from `tabAccount` where parent_account = %s
- and docstatus != 2""", self.name)
+ return frappe.db.sql(
+ """select name from `tabAccount` where parent_account = %s
+ and docstatus != 2""",
+ self.name,
+ )
def validate_mandatory(self):
if not self.root_type:
@@ -260,73 +315,99 @@ class Account(NestedSet):
super(Account, self).on_trash(True)
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_parent_account(doctype, txt, searchfield, start, page_len, filters):
- return frappe.db.sql("""select name from tabAccount
+ return frappe.db.sql(
+ """select name from tabAccount
where is_group = 1 and docstatus != 2 and company = %s
- and %s like %s order by name limit %s, %s""" %
- ("%s", searchfield, "%s", "%s", "%s"),
- (filters["company"], "%%%s%%" % txt, start, page_len), as_list=1)
+ and %s like %s order by name limit %s, %s"""
+ % ("%s", searchfield, "%s", "%s", "%s"),
+ (filters["company"], "%%%s%%" % txt, start, page_len),
+ as_list=1,
+ )
+
def get_account_currency(account):
"""Helper function to get account currency"""
if not account:
return
+
def generator():
- account_currency, company = frappe.get_cached_value("Account", account, ["account_currency", "company"])
+ account_currency, company = frappe.get_cached_value(
+ "Account", account, ["account_currency", "company"]
+ )
if not account_currency:
- account_currency = frappe.get_cached_value('Company', company, "default_currency")
+ account_currency = frappe.get_cached_value("Company", company, "default_currency")
return account_currency
return frappe.local_cache("account_currency", account, generator)
+
def on_doctype_update():
frappe.db.add_index("Account", ["lft", "rgt"])
+
def get_account_autoname(account_number, account_name, company):
# first validate if company exists
- company = frappe.get_cached_value('Company', company, ["abbr", "name"], as_dict=True)
+ company = frappe.get_cached_value("Company", company, ["abbr", "name"], as_dict=True)
if not company:
- frappe.throw(_('Company {0} does not exist').format(company))
+ frappe.throw(_("Company {0} does not exist").format(company))
parts = [account_name.strip(), company.abbr]
if cstr(account_number).strip():
parts.insert(0, cstr(account_number).strip())
- return ' - '.join(parts)
+ return " - ".join(parts)
+
def validate_account_number(name, account_number, company):
if account_number:
- account_with_same_number = frappe.db.get_value("Account",
- {"account_number": account_number, "company": company, "name": ["!=", name]})
+ account_with_same_number = frappe.db.get_value(
+ "Account", {"account_number": account_number, "company": company, "name": ["!=", name]}
+ )
if account_with_same_number:
- frappe.throw(_("Account Number {0} already used in account {1}")
- .format(account_number, account_with_same_number))
+ frappe.throw(
+ _("Account Number {0} already used in account {1}").format(
+ account_number, account_with_same_number
+ )
+ )
+
@frappe.whitelist()
def update_account_number(name, account_name, account_number=None, from_descendant=False):
account = frappe.db.get_value("Account", name, "company", as_dict=True)
- if not account: return
+ if not account:
+ return
- old_acc_name, old_acc_number = frappe.db.get_value('Account', name, \
- ["account_name", "account_number"])
+ old_acc_name, old_acc_number = frappe.db.get_value(
+ "Account", name, ["account_name", "account_number"]
+ )
# check if account exists in parent company
ancestors = get_ancestors_of("Company", account.company)
- allow_independent_account_creation = frappe.get_value("Company", account.company, "allow_account_creation_against_child_company")
+ allow_independent_account_creation = frappe.get_value(
+ "Company", account.company, "allow_account_creation_against_child_company"
+ )
if ancestors and not allow_independent_account_creation:
for ancestor in ancestors:
- if frappe.db.get_value("Account", {'account_name': old_acc_name, 'company': ancestor}, 'name'):
+ if frappe.db.get_value("Account", {"account_name": old_acc_name, "company": ancestor}, "name"):
# same account in parent company exists
allow_child_account_creation = _("Allow Account Creation Against Child Company")
- message = _("Account {0} exists in parent company {1}.").format(frappe.bold(old_acc_name), frappe.bold(ancestor))
+ message = _("Account {0} exists in parent company {1}.").format(
+ frappe.bold(old_acc_name), frappe.bold(ancestor)
+ )
message += " "
- message += _("Renaming it is only allowed via parent company {0}, to avoid mismatch.").format(frappe.bold(ancestor))
+ message += _("Renaming it is only allowed via parent company {0}, to avoid mismatch.").format(
+ frappe.bold(ancestor)
+ )
message += "
"
- message += _("To overrule this, enable '{0}' in company {1}").format(allow_child_account_creation, frappe.bold(account.company))
+ message += _("To overrule this, enable '{0}' in company {1}").format(
+ allow_child_account_creation, frappe.bold(account.company)
+ )
frappe.throw(message, title=_("Rename Not Allowed"))
@@ -339,42 +420,53 @@ def update_account_number(name, account_name, account_number=None, from_descenda
if not from_descendant:
# Update and rename in child company accounts as well
- descendants = get_descendants_of('Company', account.company)
+ descendants = get_descendants_of("Company", account.company)
if descendants:
- sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number, old_acc_number)
+ sync_update_account_number_in_child(
+ descendants, old_acc_name, account_name, account_number, old_acc_number
+ )
new_name = get_account_autoname(account_number, account_name, account.company)
if name != new_name:
frappe.rename_doc("Account", name, new_name, force=1)
return new_name
+
@frappe.whitelist()
def merge_account(old, new, is_group, root_type, company):
# Validate properties before merging
if not frappe.db.exists("Account", new):
throw(_("Account {0} does not exist").format(new))
- val = list(frappe.db.get_value("Account", new,
- ["is_group", "root_type", "company"]))
+ val = list(frappe.db.get_value("Account", new, ["is_group", "root_type", "company"]))
if val != [cint(is_group), root_type, company]:
- throw(_("""Merging is only possible if following properties are same in both records. Is Group, Root Type, Company"""))
+ throw(
+ _(
+ """Merging is only possible if following properties are same in both records. Is Group, Root Type, Company"""
+ )
+ )
if is_group and frappe.db.get_value("Account", new, "parent_account") == old:
- frappe.db.set_value("Account", new, "parent_account",
- frappe.db.get_value("Account", old, "parent_account"))
+ frappe.db.set_value(
+ "Account", new, "parent_account", frappe.db.get_value("Account", old, "parent_account")
+ )
frappe.rename_doc("Account", old, new, merge=1, force=1)
return new
+
@frappe.whitelist()
def get_root_company(company):
# return the topmost company in the hierarchy
- ancestors = get_ancestors_of('Company', company, "lft asc")
+ ancestors = get_ancestors_of("Company", company, "lft asc")
return [ancestors[0]] if ancestors else []
-def sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number=None, old_acc_number=None):
+
+def sync_update_account_number_in_child(
+ descendants, old_acc_name, account_name, account_number=None, old_acc_number=None
+):
filters = {
"company": ["in", descendants],
"account_name": old_acc_name,
@@ -382,5 +474,7 @@ def sync_update_account_number_in_child(descendants, old_acc_name, account_name,
if old_acc_number:
filters["account_number"] = old_acc_number
- for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True):
- update_account_number(d["name"], account_name, account_number, from_descendant=True)
+ for d in frappe.db.get_values(
+ "Account", filters=filters, fieldname=["company", "name"], as_dict=True
+ ):
+ update_account_number(d["name"], account_name, account_number, from_descendant=True)
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
index 3a5514388ca..fd3c19149ba 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
@@ -11,7 +11,9 @@ from six import iteritems
from unidecode import unidecode
-def create_charts(company, chart_template=None, existing_company=None, custom_chart=None, from_coa_importer=None):
+def create_charts(
+ company, chart_template=None, existing_company=None, custom_chart=None, from_coa_importer=None
+):
chart = custom_chart or get_chart(chart_template, existing_company)
if chart:
accounts = []
@@ -21,30 +23,41 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
if root_account:
root_type = child.get("root_type")
- if account_name not in ["account_name", "account_number", "account_type",
- "root_type", "is_group", "tax_rate"]:
+ if account_name not in [
+ "account_name",
+ "account_number",
+ "account_type",
+ "root_type",
+ "is_group",
+ "tax_rate",
+ ]:
account_number = cstr(child.get("account_number")).strip()
- account_name, account_name_in_db = add_suffix_if_duplicate(account_name,
- account_number, accounts)
+ account_name, account_name_in_db = add_suffix_if_duplicate(
+ account_name, account_number, accounts
+ )
is_group = identify_is_group(child)
- report_type = "Balance Sheet" if root_type in ["Asset", "Liability", "Equity"] \
- else "Profit and Loss"
+ report_type = (
+ "Balance Sheet" if root_type in ["Asset", "Liability", "Equity"] else "Profit and Loss"
+ )
- account = frappe.get_doc({
- "doctype": "Account",
- "account_name": child.get('account_name') if from_coa_importer else account_name,
- "company": company,
- "parent_account": parent,
- "is_group": is_group,
- "root_type": root_type,
- "report_type": report_type,
- "account_number": account_number,
- "account_type": child.get("account_type"),
- "account_currency": child.get('account_currency') or frappe.db.get_value('Company', company, "default_currency"),
- "tax_rate": child.get("tax_rate")
- })
+ account = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": child.get("account_name") if from_coa_importer else account_name,
+ "company": company,
+ "parent_account": parent,
+ "is_group": is_group,
+ "root_type": root_type,
+ "report_type": report_type,
+ "account_number": account_number,
+ "account_type": child.get("account_type"),
+ "account_currency": child.get("account_currency")
+ or frappe.db.get_value("Company", company, "default_currency"),
+ "tax_rate": child.get("tax_rate"),
+ }
+ )
if root_account or frappe.local.flags.allow_unverified_charts:
account.flags.ignore_mandatory = True
@@ -64,10 +77,10 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
rebuild_tree("Account", "parent_account")
frappe.local.flags.ignore_update_nsm = False
+
def add_suffix_if_duplicate(account_name, account_number, accounts):
if account_number:
- account_name_in_db = unidecode(" - ".join([account_number,
- account_name.strip().lower()]))
+ account_name_in_db = unidecode(" - ".join([account_number, account_name.strip().lower()]))
else:
account_name_in_db = unidecode(account_name.strip().lower())
@@ -77,16 +90,21 @@ def add_suffix_if_duplicate(account_name, account_number, accounts):
return account_name, account_name_in_db
+
def identify_is_group(child):
if child.get("is_group"):
is_group = child.get("is_group")
- elif len(set(child.keys()) - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])):
+ elif len(
+ set(child.keys())
+ - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])
+ ):
is_group = 1
else:
is_group = 0
return is_group
+
def get_chart(chart_template, existing_company=None):
chart = {}
if existing_company:
@@ -96,11 +114,13 @@ def get_chart(chart_template, existing_company=None):
from erpnext.accounts.doctype.account.chart_of_accounts.verified import (
standard_chart_of_accounts,
)
+
return standard_chart_of_accounts.get()
elif chart_template == "Standard with Numbers":
from erpnext.accounts.doctype.account.chart_of_accounts.verified import (
standard_chart_of_accounts_with_account_number,
)
+
return standard_chart_of_accounts_with_account_number.get()
else:
folders = ("verified",)
@@ -116,6 +136,7 @@ def get_chart(chart_template, existing_company=None):
if chart and json.loads(chart).get("name") == chart_template:
return json.loads(chart).get("tree")
+
@frappe.whitelist()
def get_charts_for_country(country, with_standard=False):
charts = []
@@ -123,9 +144,10 @@ def get_charts_for_country(country, with_standard=False):
def _get_chart_name(content):
if content:
content = json.loads(content)
- if (content and content.get("disabled", "No") == "No") \
- or frappe.local.flags.allow_unverified_charts:
- charts.append(content["name"])
+ if (
+ content and content.get("disabled", "No") == "No"
+ ) or frappe.local.flags.allow_unverified_charts:
+ charts.append(content["name"])
country_code = frappe.db.get_value("Country", country, "code")
if country_code:
@@ -152,11 +174,21 @@ def get_charts_for_country(country, with_standard=False):
def get_account_tree_from_existing_company(existing_company):
- all_accounts = frappe.get_all('Account',
- filters={'company': existing_company},
- fields = ["name", "account_name", "parent_account", "account_type",
- "is_group", "root_type", "tax_rate", "account_number"],
- order_by="lft, rgt")
+ all_accounts = frappe.get_all(
+ "Account",
+ filters={"company": existing_company},
+ fields=[
+ "name",
+ "account_name",
+ "parent_account",
+ "account_type",
+ "is_group",
+ "root_type",
+ "tax_rate",
+ "account_number",
+ ],
+ order_by="lft, rgt",
+ )
account_tree = {}
@@ -165,6 +197,7 @@ def get_account_tree_from_existing_company(existing_company):
build_account_tree(account_tree, None, all_accounts)
return account_tree
+
def build_account_tree(tree, parent, all_accounts):
# find children
parent_account = parent.name if parent else ""
@@ -193,27 +226,29 @@ def build_account_tree(tree, parent, all_accounts):
# call recursively to build a subtree for current account
build_account_tree(tree[child.account_name], child, all_accounts)
+
@frappe.whitelist()
def validate_bank_account(coa, bank_account):
accounts = []
chart = get_chart(coa)
if chart:
+
def _get_account_names(account_master):
for account_name, child in iteritems(account_master):
- if account_name not in ["account_number", "account_type",
- "root_type", "is_group", "tax_rate"]:
+ if account_name not in ["account_number", "account_type", "root_type", "is_group", "tax_rate"]:
accounts.append(account_name)
_get_account_names(child)
_get_account_names(chart)
- return (bank_account in accounts)
+ return bank_account in accounts
+
@frappe.whitelist()
def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=False):
- ''' get chart template from its folder and parse the json to be rendered as tree '''
+ """get chart template from its folder and parse the json to be rendered as tree"""
chart = chart_data or get_chart(chart_template)
# if no template selected, return as it is
@@ -221,22 +256,33 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
return
accounts = []
+
def _import_accounts(children, parent):
- ''' recursively called to form a parent-child based list of dict from chart template '''
+ """recursively called to form a parent-child based list of dict from chart template"""
for account_name, child in iteritems(children):
account = {}
- if account_name in ["account_name", "account_number", "account_type",\
- "root_type", "is_group", "tax_rate"]: continue
+ if account_name in [
+ "account_name",
+ "account_number",
+ "account_type",
+ "root_type",
+ "is_group",
+ "tax_rate",
+ ]:
+ continue
if from_coa_importer:
- account_name = child['account_name']
+ account_name = child["account_name"]
- account['parent_account'] = parent
- account['expandable'] = True if identify_is_group(child) else False
- account['value'] = (cstr(child.get('account_number')).strip() + ' - ' + account_name) \
- if child.get('account_number') else account_name
+ account["parent_account"] = parent
+ account["expandable"] = True if identify_is_group(child) else False
+ account["value"] = (
+ (cstr(child.get("account_number")).strip() + " - " + account_name)
+ if child.get("account_number")
+ else account_name
+ )
accounts.append(account)
- _import_accounts(child, account['value'])
+ _import_accounts(child, account["value"])
_import_accounts(chart, None)
return accounts
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py b/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py
index 7d94c89ad7b..562b00fd000 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py
@@ -21,6 +21,7 @@ charts = {}
all_account_types = []
all_roots = {}
+
def go():
global accounts, charts
default_account_types = get_default_account_types()
@@ -35,14 +36,16 @@ def go():
accounts, charts = {}, {}
country_path = os.path.join(path, country_dir)
manifest = ast.literal_eval(open(os.path.join(country_path, "__openerp__.py")).read())
- data_files = manifest.get("data", []) + manifest.get("init_xml", []) + \
- manifest.get("update_xml", [])
+ data_files = (
+ manifest.get("data", []) + manifest.get("init_xml", []) + manifest.get("update_xml", [])
+ )
files_path = [os.path.join(country_path, d) for d in data_files]
xml_roots = get_xml_roots(files_path)
csv_content = get_csv_contents(files_path)
prefix = country_dir if csv_content else None
- account_types = get_account_types(xml_roots.get("account.account.type", []),
- csv_content.get("account.account.type", []), prefix)
+ account_types = get_account_types(
+ xml_roots.get("account.account.type", []), csv_content.get("account.account.type", []), prefix
+ )
account_types.update(default_account_types)
if xml_roots:
@@ -55,12 +58,15 @@ def go():
create_all_roots_file()
+
def get_default_account_types():
default_types_root = []
- default_types_root.append(ET.parse(os.path.join(path, "account", "data",
- "data_account_type.xml")).getroot())
+ default_types_root.append(
+ ET.parse(os.path.join(path, "account", "data", "data_account_type.xml")).getroot()
+ )
return get_account_types(default_types_root, None, prefix="account")
+
def get_xml_roots(files_path):
xml_roots = frappe._dict()
for filepath in files_path:
@@ -69,64 +75,69 @@ def get_xml_roots(files_path):
tree = ET.parse(filepath)
root = tree.getroot()
for node in root[0].findall("record"):
- if node.get("model") in ["account.account.template",
- "account.chart.template", "account.account.type"]:
+ if node.get("model") in [
+ "account.account.template",
+ "account.chart.template",
+ "account.account.type",
+ ]:
xml_roots.setdefault(node.get("model"), []).append(root)
break
return xml_roots
+
def get_csv_contents(files_path):
csv_content = {}
for filepath in files_path:
fname = os.path.basename(filepath)
- for file_type in ["account.account.template", "account.account.type",
- "account.chart.template"]:
+ for file_type in ["account.account.template", "account.account.type", "account.chart.template"]:
if fname.startswith(file_type) and fname.endswith(".csv"):
with open(filepath, "r") as csvfile:
try:
- csv_content.setdefault(file_type, [])\
- .append(read_csv_content(csvfile.read()))
+ csv_content.setdefault(file_type, []).append(read_csv_content(csvfile.read()))
except Exception as e:
continue
return csv_content
+
def get_account_types(root_list, csv_content, prefix=None):
types = {}
account_type_map = {
- 'cash': 'Cash',
- 'bank': 'Bank',
- 'tr_cash': 'Cash',
- 'tr_bank': 'Bank',
- 'receivable': 'Receivable',
- 'tr_receivable': 'Receivable',
- 'account rec': 'Receivable',
- 'payable': 'Payable',
- 'tr_payable': 'Payable',
- 'equity': 'Equity',
- 'stocks': 'Stock',
- 'stock': 'Stock',
- 'tax': 'Tax',
- 'tr_tax': 'Tax',
- 'tax-out': 'Tax',
- 'tax-in': 'Tax',
- 'charges_personnel': 'Chargeable',
- 'fixed asset': 'Fixed Asset',
- 'cogs': 'Cost of Goods Sold',
-
+ "cash": "Cash",
+ "bank": "Bank",
+ "tr_cash": "Cash",
+ "tr_bank": "Bank",
+ "receivable": "Receivable",
+ "tr_receivable": "Receivable",
+ "account rec": "Receivable",
+ "payable": "Payable",
+ "tr_payable": "Payable",
+ "equity": "Equity",
+ "stocks": "Stock",
+ "stock": "Stock",
+ "tax": "Tax",
+ "tr_tax": "Tax",
+ "tax-out": "Tax",
+ "tax-in": "Tax",
+ "charges_personnel": "Chargeable",
+ "fixed asset": "Fixed Asset",
+ "cogs": "Cost of Goods Sold",
}
for root in root_list:
for node in root[0].findall("record"):
- if node.get("model")=="account.account.type":
+ if node.get("model") == "account.account.type":
data = {}
for field in node.findall("field"):
- if field.get("name")=="code" and field.text.lower() != "none" \
- and account_type_map.get(field.text):
- data["account_type"] = account_type_map[field.text]
+ if (
+ field.get("name") == "code"
+ and field.text.lower() != "none"
+ and account_type_map.get(field.text)
+ ):
+ data["account_type"] = account_type_map[field.text]
node_id = prefix + "." + node.get("id") if prefix else node.get("id")
types[node_id] = data
- if csv_content and csv_content[0][0]=="id":
+ if csv_content and csv_content[0][0] == "id":
for row in csv_content[1:]:
row_dict = dict(zip(csv_content[0], row))
data = {}
@@ -137,21 +148,22 @@ def get_account_types(root_list, csv_content, prefix=None):
types[node_id] = data
return types
+
def make_maps_for_xml(xml_roots, account_types, country_dir):
"""make maps for `charts` and `accounts`"""
for model, root_list in iteritems(xml_roots):
for root in root_list:
for node in root[0].findall("record"):
- if node.get("model")=="account.account.template":
+ if node.get("model") == "account.account.template":
data = {}
for field in node.findall("field"):
- if field.get("name")=="name":
+ if field.get("name") == "name":
data["name"] = field.text
- if field.get("name")=="parent_id":
+ if field.get("name") == "parent_id":
parent_id = field.get("ref") or field.get("eval")
data["parent_id"] = parent_id
- if field.get("name")=="user_type":
+ if field.get("name") == "user_type":
value = field.get("ref")
if account_types.get(value, {}).get("account_type"):
data["account_type"] = account_types[value]["account_type"]
@@ -161,16 +173,17 @@ def make_maps_for_xml(xml_roots, account_types, country_dir):
data["children"] = []
accounts[node.get("id")] = data
- if node.get("model")=="account.chart.template":
+ if node.get("model") == "account.chart.template":
data = {}
for field in node.findall("field"):
- if field.get("name")=="name":
+ if field.get("name") == "name":
data["name"] = field.text
- if field.get("name")=="account_root_id":
+ if field.get("name") == "account_root_id":
data["account_root_id"] = field.get("ref")
data["id"] = country_dir
charts.setdefault(node.get("id"), {}).update(data)
+
def make_maps_for_csv(csv_content, account_types, country_dir):
for content in csv_content.get("account.account.template", []):
for row in content[1:]:
@@ -178,7 +191,7 @@ def make_maps_for_csv(csv_content, account_types, country_dir):
account = {
"name": data.get("name"),
"parent_id": data.get("parent_id:id") or data.get("parent_id/id"),
- "children": []
+ "children": [],
}
user_type = data.get("user_type/id") or data.get("user_type:id")
if account_types.get(user_type, {}).get("account_type"):
@@ -195,12 +208,14 @@ def make_maps_for_csv(csv_content, account_types, country_dir):
for row in content[1:]:
if row:
data = dict(zip(content[0], row))
- charts.setdefault(data.get("id"), {}).update({
- "account_root_id": data.get("account_root_id:id") or \
- data.get("account_root_id/id"),
- "name": data.get("name"),
- "id": country_dir
- })
+ charts.setdefault(data.get("id"), {}).update(
+ {
+ "account_root_id": data.get("account_root_id:id") or data.get("account_root_id/id"),
+ "name": data.get("name"),
+ "id": country_dir,
+ }
+ )
+
def make_account_trees():
"""build tree hierarchy"""
@@ -219,6 +234,7 @@ def make_account_trees():
if "children" in accounts[id] and not accounts[id].get("children"):
del accounts[id]["children"]
+
def make_charts():
"""write chart files in app/setup/doctype/company/charts"""
for chart_id in charts:
@@ -237,34 +253,38 @@ def make_charts():
chart["country_code"] = src["id"][5:]
chart["tree"] = accounts[src["account_root_id"]]
-
for key, val in chart["tree"].items():
if key in ["name", "parent_id"]:
chart["tree"].pop(key)
if type(val) == dict:
val["root_type"] = ""
if chart:
- fpath = os.path.join("erpnext", "erpnext", "accounts", "doctype", "account",
- "chart_of_accounts", filename + ".json")
+ fpath = os.path.join(
+ "erpnext", "erpnext", "accounts", "doctype", "account", "chart_of_accounts", filename + ".json"
+ )
with open(fpath, "r") as chartfile:
old_content = chartfile.read()
- if not old_content or (json.loads(old_content).get("is_active", "No") == "No" \
- and json.loads(old_content).get("disabled", "No") == "No"):
+ if not old_content or (
+ json.loads(old_content).get("is_active", "No") == "No"
+ and json.loads(old_content).get("disabled", "No") == "No"
+ ):
with open(fpath, "w") as chartfile:
chartfile.write(json.dumps(chart, indent=4, sort_keys=True))
all_roots.setdefault(filename, chart["tree"].keys())
+
def create_all_roots_file():
- with open('all_roots.txt', 'w') as f:
+ with open("all_roots.txt", "w") as f:
for filename, roots in sorted(all_roots.items()):
f.write(filename)
- f.write('\n----------------------\n')
+ f.write("\n----------------------\n")
for r in sorted(roots):
- f.write(r.encode('utf-8'))
- f.write('\n')
- f.write('\n\n\n')
+ f.write(r.encode("utf-8"))
+ f.write("\n")
+ f.write("\n\n\n")
-if __name__=="__main__":
+
+if __name__ == "__main__":
go()
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py
index 9248ffa6e57..e30ad24a374 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py
@@ -7,182 +7,103 @@ from frappe import _
def get():
return {
- _("Application of Funds (Assets)"): {
- _("Current Assets"): {
- _("Accounts Receivable"): {
- _("Debtors"): {
- "account_type": "Receivable"
- }
- },
- _("Bank Accounts"): {
- "account_type": "Bank",
- "is_group": 1
- },
- _("Cash In Hand"): {
- _("Cash"): {
- "account_type": "Cash"
- },
- "account_type": "Cash"
- },
- _("Loans and Advances (Assets)"): {
- _("Employee Advances"): {
- },
- },
- _("Securities and Deposits"): {
- _("Earnest Money"): {}
- },
- _("Stock Assets"): {
- _("Stock In Hand"): {
- "account_type": "Stock"
- },
- "account_type": "Stock",
- },
- _("Tax Assets"): {
- "is_group": 1
- }
- },
- _("Fixed Assets"): {
- _("Capital Equipments"): {
- "account_type": "Fixed Asset"
- },
- _("Electronic Equipments"): {
- "account_type": "Fixed Asset"
- },
- _("Furnitures and Fixtures"): {
- "account_type": "Fixed Asset"
- },
- _("Office Equipments"): {
- "account_type": "Fixed Asset"
- },
- _("Plants and Machineries"): {
- "account_type": "Fixed Asset"
- },
- _("Buildings"): {
- "account_type": "Fixed Asset"
+ _("Application of Funds (Assets)"): {
+ _("Current Assets"): {
+ _("Accounts Receivable"): {_("Debtors"): {"account_type": "Receivable"}},
+ _("Bank Accounts"): {"account_type": "Bank", "is_group": 1},
+ _("Cash In Hand"): {_("Cash"): {"account_type": "Cash"}, "account_type": "Cash"},
+ _("Loans and Advances (Assets)"): {
+ _("Employee Advances"): {},
},
- _("Softwares"): {
- "account_type": "Fixed Asset"
+ _("Securities and Deposits"): {_("Earnest Money"): {}},
+ _("Stock Assets"): {
+ _("Stock In Hand"): {"account_type": "Stock"},
+ "account_type": "Stock",
},
- _("Accumulated Depreciation"): {
- "account_type": "Accumulated Depreciation"
- },
- _("CWIP Account"): {
- "account_type": "Capital Work in Progress",
- }
- },
- _("Investments"): {
- "is_group": 1
- },
- _("Temporary Accounts"): {
- _("Temporary Opening"): {
- "account_type": "Temporary"
- }
- },
- "root_type": "Asset"
- },
- _("Expenses"): {
- _("Direct Expenses"): {
- _("Stock Expenses"): {
- _("Cost of Goods Sold"): {
- "account_type": "Cost of Goods Sold"
- },
- _("Expenses Included In Asset Valuation"): {
- "account_type": "Expenses Included In Asset Valuation"
- },
- _("Expenses Included In Valuation"): {
- "account_type": "Expenses Included In Valuation"
- },
- _("Stock Adjustment"): {
- "account_type": "Stock Adjustment"
- }
- },
- },
- _("Indirect Expenses"): {
- _("Administrative Expenses"): {},
- _("Commission on Sales"): {},
- _("Depreciation"): {
- "account_type": "Depreciation"
- },
- _("Entertainment Expenses"): {},
- _("Freight and Forwarding Charges"): {
- "account_type": "Chargeable"
- },
- _("Legal Expenses"): {},
- _("Marketing Expenses"): {
- "account_type": "Chargeable"
- },
- _("Miscellaneous Expenses"): {
- "account_type": "Chargeable"
- },
- _("Office Maintenance Expenses"): {},
- _("Office Rent"): {},
- _("Postal Expenses"): {},
- _("Print and Stationery"): {},
- _("Round Off"): {
- "account_type": "Round Off"
- },
- _("Salary"): {},
- _("Sales Expenses"): {},
- _("Telephone Expenses"): {},
- _("Travel Expenses"): {},
- _("Utility Expenses"): {},
+ _("Tax Assets"): {"is_group": 1},
+ },
+ _("Fixed Assets"): {
+ _("Capital Equipments"): {"account_type": "Fixed Asset"},
+ _("Electronic Equipments"): {"account_type": "Fixed Asset"},
+ _("Furnitures and Fixtures"): {"account_type": "Fixed Asset"},
+ _("Office Equipments"): {"account_type": "Fixed Asset"},
+ _("Plants and Machineries"): {"account_type": "Fixed Asset"},
+ _("Buildings"): {"account_type": "Fixed Asset"},
+ _("Softwares"): {"account_type": "Fixed Asset"},
+ _("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"},
+ _("CWIP Account"): {
+ "account_type": "Capital Work in Progress",
+ },
+ },
+ _("Investments"): {"is_group": 1},
+ _("Temporary Accounts"): {_("Temporary Opening"): {"account_type": "Temporary"}},
+ "root_type": "Asset",
+ },
+ _("Expenses"): {
+ _("Direct Expenses"): {
+ _("Stock Expenses"): {
+ _("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold"},
+ _("Expenses Included In Asset Valuation"): {
+ "account_type": "Expenses Included In Asset Valuation"
+ },
+ _("Expenses Included In Valuation"): {"account_type": "Expenses Included In Valuation"},
+ _("Stock Adjustment"): {"account_type": "Stock Adjustment"},
+ },
+ },
+ _("Indirect Expenses"): {
+ _("Administrative Expenses"): {},
+ _("Commission on Sales"): {},
+ _("Depreciation"): {"account_type": "Depreciation"},
+ _("Entertainment Expenses"): {},
+ _("Freight and Forwarding Charges"): {"account_type": "Chargeable"},
+ _("Legal Expenses"): {},
+ _("Marketing Expenses"): {"account_type": "Chargeable"},
+ _("Miscellaneous Expenses"): {"account_type": "Chargeable"},
+ _("Office Maintenance Expenses"): {},
+ _("Office Rent"): {},
+ _("Postal Expenses"): {},
+ _("Print and Stationery"): {},
+ _("Round Off"): {"account_type": "Round Off"},
+ _("Salary"): {},
+ _("Sales Expenses"): {},
+ _("Telephone Expenses"): {},
+ _("Travel Expenses"): {},
+ _("Utility Expenses"): {},
_("Write Off"): {},
_("Exchange Gain/Loss"): {},
- _("Gain/Loss on Asset Disposal"): {}
- },
- "root_type": "Expense"
- },
- _("Income"): {
- _("Direct Income"): {
- _("Sales"): {},
- _("Service"): {}
- },
- _("Indirect Income"): {
- "is_group": 1
- },
- "root_type": "Income"
- },
- _("Source of Funds (Liabilities)"): {
- _("Current Liabilities"): {
- _("Accounts Payable"): {
- _("Creditors"): {
- "account_type": "Payable"
- },
- _("Payroll Payable"): {},
- },
- _("Stock Liabilities"): {
- _("Stock Received But Not Billed"): {
- "account_type": "Stock Received But Not Billed"
- },
- _("Asset Received But Not Billed"): {
- "account_type": "Asset Received But Not Billed"
- }
- },
- _("Duties and Taxes"): {
- "account_type": "Tax",
- "is_group": 1
+ _("Gain/Loss on Asset Disposal"): {},
+ },
+ "root_type": "Expense",
+ },
+ _("Income"): {
+ _("Direct Income"): {_("Sales"): {}, _("Service"): {}},
+ _("Indirect Income"): {"is_group": 1},
+ "root_type": "Income",
+ },
+ _("Source of Funds (Liabilities)"): {
+ _("Current Liabilities"): {
+ _("Accounts Payable"): {
+ _("Creditors"): {"account_type": "Payable"},
+ _("Payroll Payable"): {},
},
+ _("Stock Liabilities"): {
+ _("Stock Received But Not Billed"): {"account_type": "Stock Received But Not Billed"},
+ _("Asset Received But Not Billed"): {"account_type": "Asset Received But Not Billed"},
+ },
+ _("Duties and Taxes"): {"account_type": "Tax", "is_group": 1},
_("Loans (Liabilities)"): {
_("Secured Loans"): {},
_("Unsecured Loans"): {},
_("Bank Overdraft Account"): {},
},
- },
- "root_type": "Liability"
- },
+ },
+ "root_type": "Liability",
+ },
_("Equity"): {
- _("Capital Stock"): {
- "account_type": "Equity"
- },
- _("Dividends Paid"): {
- "account_type": "Equity"
- },
- _("Opening Balance Equity"): {
- "account_type": "Equity"
- },
- _("Retained Earnings"): {
- "account_type": "Equity"
- },
- "root_type": "Equity"
- }
+ _("Capital Stock"): {"account_type": "Equity"},
+ _("Dividends Paid"): {"account_type": "Equity"},
+ _("Opening Balance Equity"): {"account_type": "Equity"},
+ _("Retained Earnings"): {"account_type": "Equity"},
+ "root_type": "Equity",
+ },
}
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py
index 31ae17189a7..0e46f1e08a5 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py
@@ -6,288 +6,153 @@ from frappe import _
def get():
- return {
- _("Application of Funds (Assets)"): {
- _("Current Assets"): {
- _("Accounts Receivable"): {
- _("Debtors"): {
- "account_type": "Receivable",
- "account_number": "1310"
- },
- "account_number": "1300"
- },
- _("Bank Accounts"): {
- "account_type": "Bank",
- "is_group": 1,
- "account_number": "1200"
- },
- _("Cash In Hand"): {
- _("Cash"): {
- "account_type": "Cash",
- "account_number": "1110"
- },
- "account_type": "Cash",
- "account_number": "1100"
- },
- _("Loans and Advances (Assets)"): {
- _("Employee Advances"): {
- "account_number": "1610"
- },
- "account_number": "1600"
- },
- _("Securities and Deposits"): {
- _("Earnest Money"): {
- "account_number": "1651"
- },
- "account_number": "1650"
- },
- _("Stock Assets"): {
- _("Stock In Hand"): {
- "account_type": "Stock",
- "account_number": "1410"
- },
- "account_type": "Stock",
- "account_number": "1400"
- },
- _("Tax Assets"): {
- "is_group": 1,
- "account_number": "1500"
- },
- "account_number": "1100-1600"
- },
- _("Fixed Assets"): {
- _("Capital Equipments"): {
- "account_type": "Fixed Asset",
- "account_number": "1710"
- },
- _("Electronic Equipments"): {
- "account_type": "Fixed Asset",
- "account_number": "1720"
- },
- _("Furnitures and Fixtures"): {
- "account_type": "Fixed Asset",
- "account_number": "1730"
- },
- _("Office Equipments"): {
- "account_type": "Fixed Asset",
- "account_number": "1740"
- },
- _("Plants and Machineries"): {
- "account_type": "Fixed Asset",
- "account_number": "1750"
- },
- _("Buildings"): {
- "account_type": "Fixed Asset",
- "account_number": "1760"
- },
- _("Softwares"): {
- "account_type": "Fixed Asset",
- "account_number": "1770"
- },
- _("Accumulated Depreciation"): {
- "account_type": "Accumulated Depreciation",
- "account_number": "1780"
- },
- _("CWIP Account"): {
- "account_type": "Capital Work in Progress",
- "account_number": "1790"
- },
- "account_number": "1700"
- },
- _("Investments"): {
- "is_group": 1,
- "account_number": "1800"
- },
- _("Temporary Accounts"): {
- _("Temporary Opening"): {
- "account_type": "Temporary",
- "account_number": "1910"
- },
- "account_number": "1900"
- },
- "root_type": "Asset",
- "account_number": "1000"
- },
- _("Expenses"): {
- _("Direct Expenses"): {
- _("Stock Expenses"): {
- _("Cost of Goods Sold"): {
- "account_type": "Cost of Goods Sold",
- "account_number": "5111"
- },
- _("Expenses Included In Asset Valuation"): {
- "account_type": "Expenses Included In Asset Valuation",
- "account_number": "5112"
- },
- _("Expenses Included In Valuation"): {
- "account_type": "Expenses Included In Valuation",
- "account_number": "5118"
- },
- _("Stock Adjustment"): {
- "account_type": "Stock Adjustment",
- "account_number": "5119"
- },
- "account_number": "5110"
- },
- "account_number": "5100"
- },
- _("Indirect Expenses"): {
- _("Administrative Expenses"): {
- "account_number": "5201"
- },
- _("Commission on Sales"): {
- "account_number": "5202"
- },
- _("Depreciation"): {
- "account_type": "Depreciation",
- "account_number": "5203"
- },
- _("Entertainment Expenses"): {
- "account_number": "5204"
- },
- _("Freight and Forwarding Charges"): {
- "account_type": "Chargeable",
- "account_number": "5205"
- },
- _("Legal Expenses"): {
- "account_number": "5206"
- },
- _("Marketing Expenses"): {
- "account_type": "Chargeable",
- "account_number": "5207"
- },
- _("Office Maintenance Expenses"): {
- "account_number": "5208"
- },
- _("Office Rent"): {
- "account_number": "5209"
- },
- _("Postal Expenses"): {
- "account_number": "5210"
- },
- _("Print and Stationery"): {
- "account_number": "5211"
- },
- _("Round Off"): {
- "account_type": "Round Off",
- "account_number": "5212"
- },
- _("Salary"): {
- "account_number": "5213"
- },
- _("Sales Expenses"): {
- "account_number": "5214"
- },
- _("Telephone Expenses"): {
- "account_number": "5215"
- },
- _("Travel Expenses"): {
- "account_number": "5216"
- },
- _("Utility Expenses"): {
- "account_number": "5217"
- },
- _("Write Off"): {
- "account_number": "5218"
- },
- _("Exchange Gain/Loss"): {
- "account_number": "5219"
- },
- _("Gain/Loss on Asset Disposal"): {
- "account_number": "5220"
- },
- _("Miscellaneous Expenses"): {
- "account_type": "Chargeable",
- "account_number": "5221"
- },
- "account_number": "5200"
- },
- "root_type": "Expense",
- "account_number": "5000"
- },
- _("Income"): {
- _("Direct Income"): {
- _("Sales"): {
- "account_number": "4110"
- },
- _("Service"): {
- "account_number": "4120"
- },
- "account_number": "4100"
- },
- _("Indirect Income"): {
- "is_group": 1,
- "account_number": "4200"
- },
- "root_type": "Income",
- "account_number": "4000"
- },
- _("Source of Funds (Liabilities)"): {
- _("Current Liabilities"): {
- _("Accounts Payable"): {
- _("Creditors"): {
- "account_type": "Payable",
- "account_number": "2110"
- },
- _("Payroll Payable"): {
- "account_number": "2120"
- },
- "account_number": "2100"
- },
- _("Stock Liabilities"): {
- _("Stock Received But Not Billed"): {
- "account_type": "Stock Received But Not Billed",
- "account_number": "2210"
- },
- _("Asset Received But Not Billed"): {
- "account_type": "Asset Received But Not Billed",
- "account_number": "2211"
- },
- "account_number": "2200"
- },
- _("Duties and Taxes"): {
- _("TDS Payable"): {
- "account_number": "2310"
- },
- "account_type": "Tax",
- "is_group": 1,
- "account_number": "2300"
- },
- _("Loans (Liabilities)"): {
- _("Secured Loans"): {
- "account_number": "2410"
- },
- _("Unsecured Loans"): {
- "account_number": "2420"
- },
- _("Bank Overdraft Account"): {
- "account_number": "2430"
- },
- "account_number": "2400"
- },
- "account_number": "2100-2400"
- },
- "root_type": "Liability",
- "account_number": "2000"
- },
- _("Equity"): {
- _("Capital Stock"): {
- "account_type": "Equity",
- "account_number": "3100"
- },
- _("Dividends Paid"): {
- "account_type": "Equity",
- "account_number": "3200"
- },
- _("Opening Balance Equity"): {
- "account_type": "Equity",
- "account_number": "3300"
- },
- _("Retained Earnings"): {
- "account_type": "Equity",
- "account_number": "3400"
- },
- "root_type": "Equity",
- "account_number": "3000"
- }
- }
+ return {
+ _("Application of Funds (Assets)"): {
+ _("Current Assets"): {
+ _("Accounts Receivable"): {
+ _("Debtors"): {"account_type": "Receivable", "account_number": "1310"},
+ "account_number": "1300",
+ },
+ _("Bank Accounts"): {"account_type": "Bank", "is_group": 1, "account_number": "1200"},
+ _("Cash In Hand"): {
+ _("Cash"): {"account_type": "Cash", "account_number": "1110"},
+ "account_type": "Cash",
+ "account_number": "1100",
+ },
+ _("Loans and Advances (Assets)"): {
+ _("Employee Advances"): {"account_number": "1610"},
+ "account_number": "1600",
+ },
+ _("Securities and Deposits"): {
+ _("Earnest Money"): {"account_number": "1651"},
+ "account_number": "1650",
+ },
+ _("Stock Assets"): {
+ _("Stock In Hand"): {"account_type": "Stock", "account_number": "1410"},
+ "account_type": "Stock",
+ "account_number": "1400",
+ },
+ _("Tax Assets"): {"is_group": 1, "account_number": "1500"},
+ "account_number": "1100-1600",
+ },
+ _("Fixed Assets"): {
+ _("Capital Equipments"): {"account_type": "Fixed Asset", "account_number": "1710"},
+ _("Electronic Equipments"): {"account_type": "Fixed Asset", "account_number": "1720"},
+ _("Furnitures and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"},
+ _("Office Equipments"): {"account_type": "Fixed Asset", "account_number": "1740"},
+ _("Plants and Machineries"): {"account_type": "Fixed Asset", "account_number": "1750"},
+ _("Buildings"): {"account_type": "Fixed Asset", "account_number": "1760"},
+ _("Softwares"): {"account_type": "Fixed Asset", "account_number": "1770"},
+ _("Accumulated Depreciation"): {
+ "account_type": "Accumulated Depreciation",
+ "account_number": "1780",
+ },
+ _("CWIP Account"): {"account_type": "Capital Work in Progress", "account_number": "1790"},
+ "account_number": "1700",
+ },
+ _("Investments"): {"is_group": 1, "account_number": "1800"},
+ _("Temporary Accounts"): {
+ _("Temporary Opening"): {"account_type": "Temporary", "account_number": "1910"},
+ "account_number": "1900",
+ },
+ "root_type": "Asset",
+ "account_number": "1000",
+ },
+ _("Expenses"): {
+ _("Direct Expenses"): {
+ _("Stock Expenses"): {
+ _("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold", "account_number": "5111"},
+ _("Expenses Included In Asset Valuation"): {
+ "account_type": "Expenses Included In Asset Valuation",
+ "account_number": "5112",
+ },
+ _("Expenses Included In Valuation"): {
+ "account_type": "Expenses Included In Valuation",
+ "account_number": "5118",
+ },
+ _("Stock Adjustment"): {"account_type": "Stock Adjustment", "account_number": "5119"},
+ "account_number": "5110",
+ },
+ "account_number": "5100",
+ },
+ _("Indirect Expenses"): {
+ _("Administrative Expenses"): {"account_number": "5201"},
+ _("Commission on Sales"): {"account_number": "5202"},
+ _("Depreciation"): {"account_type": "Depreciation", "account_number": "5203"},
+ _("Entertainment Expenses"): {"account_number": "5204"},
+ _("Freight and Forwarding Charges"): {"account_type": "Chargeable", "account_number": "5205"},
+ _("Legal Expenses"): {"account_number": "5206"},
+ _("Marketing Expenses"): {"account_type": "Chargeable", "account_number": "5207"},
+ _("Office Maintenance Expenses"): {"account_number": "5208"},
+ _("Office Rent"): {"account_number": "5209"},
+ _("Postal Expenses"): {"account_number": "5210"},
+ _("Print and Stationery"): {"account_number": "5211"},
+ _("Round Off"): {"account_type": "Round Off", "account_number": "5212"},
+ _("Salary"): {"account_number": "5213"},
+ _("Sales Expenses"): {"account_number": "5214"},
+ _("Telephone Expenses"): {"account_number": "5215"},
+ _("Travel Expenses"): {"account_number": "5216"},
+ _("Utility Expenses"): {"account_number": "5217"},
+ _("Write Off"): {"account_number": "5218"},
+ _("Exchange Gain/Loss"): {"account_number": "5219"},
+ _("Gain/Loss on Asset Disposal"): {"account_number": "5220"},
+ _("Miscellaneous Expenses"): {"account_type": "Chargeable", "account_number": "5221"},
+ "account_number": "5200",
+ },
+ "root_type": "Expense",
+ "account_number": "5000",
+ },
+ _("Income"): {
+ _("Direct Income"): {
+ _("Sales"): {"account_number": "4110"},
+ _("Service"): {"account_number": "4120"},
+ "account_number": "4100",
+ },
+ _("Indirect Income"): {"is_group": 1, "account_number": "4200"},
+ "root_type": "Income",
+ "account_number": "4000",
+ },
+ _("Source of Funds (Liabilities)"): {
+ _("Current Liabilities"): {
+ _("Accounts Payable"): {
+ _("Creditors"): {"account_type": "Payable", "account_number": "2110"},
+ _("Payroll Payable"): {"account_number": "2120"},
+ "account_number": "2100",
+ },
+ _("Stock Liabilities"): {
+ _("Stock Received But Not Billed"): {
+ "account_type": "Stock Received But Not Billed",
+ "account_number": "2210",
+ },
+ _("Asset Received But Not Billed"): {
+ "account_type": "Asset Received But Not Billed",
+ "account_number": "2211",
+ },
+ "account_number": "2200",
+ },
+ _("Duties and Taxes"): {
+ _("TDS Payable"): {"account_number": "2310"},
+ "account_type": "Tax",
+ "is_group": 1,
+ "account_number": "2300",
+ },
+ _("Loans (Liabilities)"): {
+ _("Secured Loans"): {"account_number": "2410"},
+ _("Unsecured Loans"): {"account_number": "2420"},
+ _("Bank Overdraft Account"): {"account_number": "2430"},
+ "account_number": "2400",
+ },
+ "account_number": "2100-2400",
+ },
+ "root_type": "Liability",
+ "account_number": "2000",
+ },
+ _("Equity"): {
+ _("Capital Stock"): {"account_type": "Equity", "account_number": "3100"},
+ _("Dividends Paid"): {"account_type": "Equity", "account_number": "3200"},
+ _("Opening Balance Equity"): {"account_type": "Equity", "account_number": "3300"},
+ _("Retained Earnings"): {"account_type": "Equity", "account_number": "3400"},
+ "root_type": "Equity",
+ "account_number": "3000",
+ },
+ }
diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py
index 0715823b300..f9c9173af08 100644
--- a/erpnext/accounts/doctype/account/test_account.py
+++ b/erpnext/accounts/doctype/account/test_account.py
@@ -20,8 +20,9 @@ class TestAccount(unittest.TestCase):
acc.company = "_Test Company"
acc.insert()
- account_number, account_name = frappe.db.get_value("Account", "1210 - Debtors - _TC",
- ["account_number", "account_name"])
+ account_number, account_name = frappe.db.get_value(
+ "Account", "1210 - Debtors - _TC", ["account_number", "account_name"]
+ )
self.assertEqual(account_number, "1210")
self.assertEqual(account_name, "Debtors")
@@ -30,8 +31,12 @@ class TestAccount(unittest.TestCase):
update_account_number("1210 - Debtors - _TC", new_account_name, new_account_number)
- new_acc = frappe.db.get_value("Account", "1211-11-4 - 6 - - Debtors 1 - Test - - _TC",
- ["account_name", "account_number"], as_dict=1)
+ new_acc = frappe.db.get_value(
+ "Account",
+ "1211-11-4 - 6 - - Debtors 1 - Test - - _TC",
+ ["account_name", "account_number"],
+ as_dict=1,
+ )
self.assertEqual(new_acc.account_name, "Debtors 1 - Test -")
self.assertEqual(new_acc.account_number, "1211-11-4 - 6 -")
@@ -79,7 +84,9 @@ class TestAccount(unittest.TestCase):
self.assertEqual(parent, "Securities and Deposits - _TC")
- merge_account("Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company)
+ merge_account(
+ "Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company
+ )
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
# Parent account of the child account changes after merging
@@ -91,14 +98,28 @@ class TestAccount(unittest.TestCase):
doc = frappe.get_doc("Account", "Current Assets - _TC")
# Raise error as is_group property doesn't match
- self.assertRaises(frappe.ValidationError, merge_account, "Current Assets - _TC",\
- "Accumulated Depreciation - _TC", doc.is_group, doc.root_type, doc.company)
+ self.assertRaises(
+ frappe.ValidationError,
+ merge_account,
+ "Current Assets - _TC",
+ "Accumulated Depreciation - _TC",
+ doc.is_group,
+ doc.root_type,
+ doc.company,
+ )
doc = frappe.get_doc("Account", "Capital Stock - _TC")
# Raise error as root_type property doesn't match
- self.assertRaises(frappe.ValidationError, merge_account, "Capital Stock - _TC",\
- "Softwares - _TC", doc.is_group, doc.root_type, doc.company)
+ self.assertRaises(
+ frappe.ValidationError,
+ merge_account,
+ "Capital Stock - _TC",
+ "Softwares - _TC",
+ doc.is_group,
+ doc.root_type,
+ doc.company,
+ )
def test_account_sync(self):
frappe.local.flags.pop("ignore_root_company_validation", None)
@@ -109,8 +130,12 @@ class TestAccount(unittest.TestCase):
acc.company = "_Test Company 3"
acc.insert()
- acc_tc_4 = frappe.db.get_value('Account', {'account_name': "Test Sync Account", "company": "_Test Company 4"})
- acc_tc_5 = frappe.db.get_value('Account', {'account_name': "Test Sync Account", "company": "_Test Company 5"})
+ acc_tc_4 = frappe.db.get_value(
+ "Account", {"account_name": "Test Sync Account", "company": "_Test Company 4"}
+ )
+ acc_tc_5 = frappe.db.get_value(
+ "Account", {"account_name": "Test Sync Account", "company": "_Test Company 5"}
+ )
self.assertEqual(acc_tc_4, "Test Sync Account - _TC4")
self.assertEqual(acc_tc_5, "Test Sync Account - _TC5")
@@ -138,8 +163,26 @@ class TestAccount(unittest.TestCase):
update_account_number(acc.name, "Test Rename Sync Account", "1234")
# Check if renamed in children
- self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 4", "account_number": "1234"}))
- self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 5", "account_number": "1234"}))
+ self.assertTrue(
+ frappe.db.exists(
+ "Account",
+ {
+ "account_name": "Test Rename Sync Account",
+ "company": "_Test Company 4",
+ "account_number": "1234",
+ },
+ )
+ )
+ self.assertTrue(
+ frappe.db.exists(
+ "Account",
+ {
+ "account_name": "Test Rename Sync Account",
+ "company": "_Test Company 5",
+ "account_number": "1234",
+ },
+ )
+ )
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC3")
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4")
@@ -155,25 +198,71 @@ class TestAccount(unittest.TestCase):
acc.company = "_Test Company 3"
acc.insert()
- self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 4"}))
- self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 5"}))
+ self.assertTrue(
+ frappe.db.exists(
+ "Account", {"account_name": "Test Group Account", "company": "_Test Company 4"}
+ )
+ )
+ self.assertTrue(
+ frappe.db.exists(
+ "Account", {"account_name": "Test Group Account", "company": "_Test Company 5"}
+ )
+ )
# Try renaming child company account
- acc_tc_5 = frappe.db.get_value('Account', {'account_name': "Test Group Account", "company": "_Test Company 5"})
- self.assertRaises(frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account")
+ acc_tc_5 = frappe.db.get_value(
+ "Account", {"account_name": "Test Group Account", "company": "_Test Company 5"}
+ )
+ self.assertRaises(
+ frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account"
+ )
# Rename child company account with allow_account_creation_against_child_company enabled
- frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 1)
+ frappe.db.set_value(
+ "Company", "_Test Company 5", "allow_account_creation_against_child_company", 1
+ )
update_account_number(acc_tc_5, "Test Modified Account")
- self.assertTrue(frappe.db.exists("Account", {'name': "Test Modified Account - _TC5", "company": "_Test Company 5"}))
+ self.assertTrue(
+ frappe.db.exists(
+ "Account", {"name": "Test Modified Account - _TC5", "company": "_Test Company 5"}
+ )
+ )
- frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 0)
+ frappe.db.set_value(
+ "Company", "_Test Company 5", "allow_account_creation_against_child_company", 0
+ )
- to_delete = ["Test Group Account - _TC3", "Test Group Account - _TC4", "Test Modified Account - _TC5"]
+ to_delete = [
+ "Test Group Account - _TC3",
+ "Test Group Account - _TC4",
+ "Test Modified Account - _TC5",
+ ]
for doc in to_delete:
frappe.delete_doc("Account", doc)
+ def test_validate_account_currency(self):
+ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
+
+ if not frappe.db.get_value("Account", "Test Currency Account - _TC"):
+ acc = frappe.new_doc("Account")
+ acc.account_name = "Test Currency Account"
+ acc.parent_account = "Tax Assets - _TC"
+ acc.company = "_Test Company"
+ acc.insert()
+ else:
+ acc = frappe.get_doc("Account", "Test Currency Account - _TC")
+
+ self.assertEqual(acc.account_currency, "INR")
+
+ # Make a JV against this account
+ make_journal_entry(
+ "Test Currency Account - _TC", "Miscellaneous Expenses - _TC", 100, submit=True
+ )
+
+ acc.account_currency = "USD"
+ self.assertRaises(frappe.ValidationError, acc.save)
+
def _make_test_records(verbose=None):
from frappe.test_runner import make_test_objects
@@ -184,20 +273,16 @@ def _make_test_records(verbose=None):
["_Test Bank USD", "Bank Accounts", 0, "Bank", "USD"],
["_Test Bank EUR", "Bank Accounts", 0, "Bank", "EUR"],
["_Test Cash", "Cash In Hand", 0, "Cash", None],
-
["_Test Account Stock Expenses", "Direct Expenses", 1, None, None],
["_Test Account Shipping Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
["_Test Account Customs Duty", "_Test Account Stock Expenses", 0, "Tax", None],
["_Test Account Insurance Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
["_Test Account Stock Adjustment", "_Test Account Stock Expenses", 0, "Stock Adjustment", None],
["_Test Employee Advance", "Current Liabilities", 0, None, None],
-
["_Test Account Tax Assets", "Current Assets", 1, None, None],
["_Test Account VAT", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Service Tax", "_Test Account Tax Assets", 0, "Tax", None],
-
["_Test Account Reserves and Surplus", "Current Liabilities", 0, None, None],
-
["_Test Account Cost for Goods Sold", "Expenses", 0, None, None],
["_Test Account Excise Duty", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Education Cess", "_Test Account Tax Assets", 0, "Tax", None],
@@ -206,38 +291,45 @@ def _make_test_records(verbose=None):
["_Test Account Discount", "Direct Expenses", 0, None, None],
["_Test Write Off", "Indirect Expenses", 0, None, None],
["_Test Exchange Gain/Loss", "Indirect Expenses", 0, None, None],
-
["_Test Account Sales", "Direct Income", 0, None, None],
-
# related to Account Inventory Integration
["_Test Account Stock In Hand", "Current Assets", 0, None, None],
-
# fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
["_Test Depreciations", "Expenses", 0, None, None],
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
-
# Receivable / Payable Account
["_Test Receivable", "Current Assets", 0, "Receivable", None],
["_Test Payable", "Current Liabilities", 0, "Payable", None],
["_Test Receivable USD", "Current Assets", 0, "Receivable", "USD"],
- ["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"]
+ ["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"],
]
- for company, abbr in [["_Test Company", "_TC"], ["_Test Company 1", "_TC1"], ["_Test Company with perpetual inventory", "TCP1"]]:
- test_objects = make_test_objects("Account", [{
- "doctype": "Account",
- "account_name": account_name,
- "parent_account": parent_account + " - " + abbr,
- "company": company,
- "is_group": is_group,
- "account_type": account_type,
- "account_currency": currency
- } for account_name, parent_account, is_group, account_type, currency in accounts])
+ for company, abbr in [
+ ["_Test Company", "_TC"],
+ ["_Test Company 1", "_TC1"],
+ ["_Test Company with perpetual inventory", "TCP1"],
+ ]:
+ test_objects = make_test_objects(
+ "Account",
+ [
+ {
+ "doctype": "Account",
+ "account_name": account_name,
+ "parent_account": parent_account + " - " + abbr,
+ "company": company,
+ "is_group": is_group,
+ "account_type": account_type,
+ "account_currency": currency,
+ }
+ for account_name, parent_account, is_group, account_type, currency in accounts
+ ],
+ )
return test_objects
+
def get_inventory_account(company, warehouse=None):
account = None
if warehouse:
@@ -247,19 +339,24 @@ def get_inventory_account(company, warehouse=None):
return account
+
def create_account(**kwargs):
- account = frappe.db.get_value("Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")})
+ account = frappe.db.get_value(
+ "Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")}
+ )
if account:
return account
else:
- account = frappe.get_doc(dict(
- doctype = "Account",
- account_name = kwargs.get('account_name'),
- account_type = kwargs.get('account_type'),
- parent_account = kwargs.get('parent_account'),
- company = kwargs.get('company'),
- account_currency = kwargs.get('account_currency')
- ))
+ account = frappe.get_doc(
+ dict(
+ doctype="Account",
+ account_name=kwargs.get("account_name"),
+ account_type=kwargs.get("account_type"),
+ parent_account=kwargs.get("parent_account"),
+ company=kwargs.get("company"),
+ account_currency=kwargs.get("account_currency"),
+ )
+ )
account.save()
return account.name
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index b6112e0cc57..3f1998a3b39 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -17,13 +17,21 @@ class AccountingDimension(Document):
self.set_fieldname_and_label()
def validate(self):
- if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project',
- 'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') :
+ if self.document_type in core_doctypes_list + (
+ "Accounting Dimension",
+ "Project",
+ "Cost Center",
+ "Accounting Dimension Detail",
+ "Company",
+ "Account",
+ ):
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg)
- exists = frappe.db.get_value("Accounting Dimension", {'document_type': self.document_type}, ['name'])
+ exists = frappe.db.get_value(
+ "Accounting Dimension", {"document_type": self.document_type}, ["name"]
+ )
if exists and self.is_new():
frappe.throw(_("Document Type already used as a dimension"))
@@ -42,13 +50,13 @@ class AccountingDimension(Document):
if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self)
else:
- frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue='long')
+ frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue="long")
def on_trash(self):
if frappe.flags.in_test:
delete_accounting_dimension(doc=self)
else:
- frappe.enqueue(delete_accounting_dimension, doc=self, queue='long')
+ frappe.enqueue(delete_accounting_dimension, doc=self, queue="long")
def set_fieldname_and_label(self):
if not self.label:
@@ -60,6 +68,7 @@ class AccountingDimension(Document):
def on_update(self):
frappe.flags.accounting_dimensions = None
+
def make_dimension_in_accounting_doctypes(doc, doclist=None):
if not doclist:
doclist = get_doctypes_with_dimensions()
@@ -70,9 +79,9 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
for doctype in doclist:
if (doc_count + 1) % 2 == 0:
- insert_after_field = 'dimension_col_break'
+ insert_after_field = "dimension_col_break"
else:
- insert_after_field = 'accounting_dimensions_section'
+ insert_after_field = "accounting_dimensions_section"
df = {
"fieldname": doc.fieldname,
@@ -80,30 +89,33 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
"fieldtype": "Link",
"options": doc.document_type,
"insert_after": insert_after_field,
- "owner": "Administrator"
+ "owner": "Administrator",
}
meta = frappe.get_meta(doctype, cached=False)
fieldnames = [d.fieldname for d in meta.get("fields")]
- if df['fieldname'] not in fieldnames:
+ if df["fieldname"] not in fieldnames:
if doctype == "Budget":
add_dimension_to_budget_doctype(df.copy(), doc)
else:
- create_custom_field(doctype, df)
+ create_custom_field(doctype, df, ignore_validate=True)
count += 1
- frappe.publish_progress(count*100/len(doclist), title = _("Creating Dimensions..."))
+ frappe.publish_progress(count * 100 / len(doclist), title=_("Creating Dimensions..."))
frappe.clear_cache(doctype=doctype)
-def add_dimension_to_budget_doctype(df, doc):
- df.update({
- "insert_after": "cost_center",
- "depends_on": "eval:doc.budget_against == '{0}'".format(doc.document_type)
- })
- create_custom_field("Budget", df)
+def add_dimension_to_budget_doctype(df, doc):
+ df.update(
+ {
+ "insert_after": "cost_center",
+ "depends_on": "eval:doc.budget_against == '{0}'".format(doc.document_type),
+ }
+ )
+
+ create_custom_field("Budget", df, ignore_validate=True)
property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options")
@@ -112,36 +124,44 @@ def add_dimension_to_budget_doctype(df, doc):
property_setter_doc.value = property_setter_doc.value + "\n" + doc.document_type
property_setter_doc.save()
- frappe.clear_cache(doctype='Budget')
+ frappe.clear_cache(doctype="Budget")
else:
- frappe.get_doc({
- "doctype": "Property Setter",
- "doctype_or_field": "DocField",
- "doc_type": "Budget",
- "field_name": "budget_against",
- "property": "options",
- "property_type": "Text",
- "value": "\nCost Center\nProject\n" + doc.document_type
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Property Setter",
+ "doctype_or_field": "DocField",
+ "doc_type": "Budget",
+ "field_name": "budget_against",
+ "property": "options",
+ "property_type": "Text",
+ "value": "\nCost Center\nProject\n" + doc.document_type,
+ }
+ ).insert(ignore_permissions=True)
def delete_accounting_dimension(doc):
doclist = get_doctypes_with_dimensions()
- frappe.db.sql("""
+ frappe.db.sql(
+ """
DELETE FROM `tabCustom Field`
WHERE fieldname = %s
- AND dt IN (%s)""" % #nosec
- ('%s', ', '.join(['%s']* len(doclist))), tuple([doc.fieldname] + doclist))
+ AND dt IN (%s)"""
+ % ("%s", ", ".join(["%s"] * len(doclist))), # nosec
+ tuple([doc.fieldname] + doclist),
+ )
- frappe.db.sql("""
+ frappe.db.sql(
+ """
DELETE FROM `tabProperty Setter`
WHERE field_name = %s
- AND doc_type IN (%s)""" % #nosec
- ('%s', ', '.join(['%s']* len(doclist))), tuple([doc.fieldname] + doclist))
+ AND doc_type IN (%s)"""
+ % ("%s", ", ".join(["%s"] * len(doclist))), # nosec
+ tuple([doc.fieldname] + doclist),
+ )
budget_against_property = frappe.get_doc("Property Setter", "Budget-budget_against-options")
- value_list = budget_against_property.value.split('\n')[3:]
+ value_list = budget_against_property.value.split("\n")[3:]
if doc.document_type in value_list:
value_list.remove(doc.document_type)
@@ -152,6 +172,7 @@ def delete_accounting_dimension(doc):
for doctype in doclist:
frappe.clear_cache(doctype=doctype)
+
@frappe.whitelist()
def disable_dimension(doc):
if frappe.flags.in_test:
@@ -159,10 +180,11 @@ def disable_dimension(doc):
else:
frappe.enqueue(toggle_disabling, doc=doc)
+
def toggle_disabling(doc):
doc = json.loads(doc)
- if doc.get('disabled'):
+ if doc.get("disabled"):
df = {"read_only": 1}
else:
df = {"read_only": 0}
@@ -170,7 +192,7 @@ def toggle_disabling(doc):
doclist = get_doctypes_with_dimensions()
for doctype in doclist:
- field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": doc.get('fieldname')})
+ field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": doc.get("fieldname")})
if field:
custom_field = frappe.get_doc("Custom Field", field)
custom_field.update(df)
@@ -178,26 +200,40 @@ def toggle_disabling(doc):
frappe.clear_cache(doctype=doctype)
+
def get_doctypes_with_dimensions():
return frappe.get_hooks("accounting_dimension_doctypes")
-def get_accounting_dimensions(as_list=True):
+
+def get_accounting_dimensions(as_list=True, filters=None):
+
+ if not filters:
+ filters = {"disabled": 0}
+
if frappe.flags.accounting_dimensions is None:
- frappe.flags.accounting_dimensions = frappe.get_all("Accounting Dimension",
- fields=["label", "fieldname", "disabled", "document_type"])
+ frappe.flags.accounting_dimensions = frappe.get_all(
+ "Accounting Dimension",
+ fields=["label", "fieldname", "disabled", "document_type"],
+ filters=filters,
+ )
if as_list:
return [d.fieldname for d in frappe.flags.accounting_dimensions]
else:
return frappe.flags.accounting_dimensions
+
def get_checks_for_pl_and_bs_accounts():
- dimensions = frappe.db.sql("""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
+ dimensions = frappe.db.sql(
+ """SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
- WHERE p.name = c.parent""", as_dict=1)
+ WHERE p.name = c.parent""",
+ as_dict=1,
+ )
return dimensions
+
def get_dimension_with_children(doctype, dimension):
if isinstance(dimension, list):
@@ -205,34 +241,39 @@ def get_dimension_with_children(doctype, dimension):
all_dimensions = []
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
- children = frappe.get_all(doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft")
+ children = frappe.get_all(
+ doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
+ )
all_dimensions += [c.name for c in children]
return all_dimensions
+
@frappe.whitelist()
def get_dimensions(with_cost_center_and_project=False):
- dimension_filters = frappe.db.sql("""
+ dimension_filters = frappe.db.sql(
+ """
SELECT label, fieldname, document_type
FROM `tabAccounting Dimension`
WHERE disabled = 0
- """, as_dict=1)
+ """,
+ as_dict=1,
+ )
- default_dimensions = frappe.db.sql("""SELECT p.fieldname, c.company, c.default_dimension
+ default_dimensions = frappe.db.sql(
+ """SELECT p.fieldname, c.company, c.default_dimension
FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p
- WHERE c.parent = p.name""", as_dict=1)
+ WHERE c.parent = p.name""",
+ as_dict=1,
+ )
if with_cost_center_and_project:
- dimension_filters.extend([
- {
- 'fieldname': 'cost_center',
- 'document_type': 'Cost Center'
- },
- {
- 'fieldname': 'project',
- 'document_type': 'Project'
- }
- ])
+ dimension_filters.extend(
+ [
+ {"fieldname": "cost_center", "document_type": "Cost Center"},
+ {"fieldname": "project", "document_type": "Project"},
+ ]
+ )
default_dimensions_map = {}
for dimension in default_dimensions:
diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
index f781a221ddf..25ef2ea5c2c 100644
--- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
@@ -8,7 +8,8 @@ import frappe
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
-test_dependencies = ['Cost Center', 'Location', 'Warehouse', 'Department']
+test_dependencies = ["Cost Center", "Location", "Warehouse", "Department"]
+
class TestAccountingDimension(unittest.TestCase):
def setUp(self):
@@ -18,24 +19,27 @@ class TestAccountingDimension(unittest.TestCase):
si = create_sales_invoice(do_not_save=1)
si.location = "Block 1"
- si.append("items", {
- "item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC",
- "qty": 1,
- "rate": 100,
- "income_account": "Sales - _TC",
- "expense_account": "Cost of Goods Sold - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "department": "_Test Department - _TC",
- "location": "Block 1"
- })
+ si.append(
+ "items",
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 1,
+ "rate": 100,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "department": "_Test Department - _TC",
+ "location": "Block 1",
+ },
+ )
si.save()
si.submit()
gle = frappe.get_doc("GL Entry", {"voucher_no": si.name, "account": "Sales - _TC"})
- self.assertEqual(gle.get('department'), "_Test Department - _TC")
+ self.assertEqual(gle.get("department"), "_Test Department - _TC")
def test_dimension_against_journal_entry(self):
je = make_journal_entry("Sales - _TC", "Sales Expenses - _TC", 500, save=False)
@@ -50,21 +54,24 @@ class TestAccountingDimension(unittest.TestCase):
gle = frappe.get_doc("GL Entry", {"voucher_no": je.name, "account": "Sales - _TC"})
gle1 = frappe.get_doc("GL Entry", {"voucher_no": je.name, "account": "Sales Expenses - _TC"})
- self.assertEqual(gle.get('department'), "_Test Department - _TC")
- self.assertEqual(gle1.get('department'), "_Test Department - _TC")
+ self.assertEqual(gle.get("department"), "_Test Department - _TC")
+ self.assertEqual(gle1.get("department"), "_Test Department - _TC")
def test_mandatory(self):
si = create_sales_invoice(do_not_save=1)
- si.append("items", {
- "item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC",
- "qty": 1,
- "rate": 100,
- "income_account": "Sales - _TC",
- "expense_account": "Cost of Goods Sold - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "location": ""
- })
+ si.append(
+ "items",
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 1,
+ "rate": 100,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "location": "",
+ },
+ )
si.save()
self.assertRaises(frappe.ValidationError, si.submit)
@@ -72,31 +79,39 @@ class TestAccountingDimension(unittest.TestCase):
def tearDown(self):
disable_dimension()
+
def create_dimension():
frappe.set_user("Administrator")
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
- frappe.get_doc({
- "doctype": "Accounting Dimension",
- "document_type": "Department",
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Accounting Dimension",
+ "document_type": "Department",
+ }
+ ).insert()
else:
dimension = frappe.get_doc("Accounting Dimension", "Department")
dimension.disabled = 0
dimension.save()
if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
- dimension1 = frappe.get_doc({
- "doctype": "Accounting Dimension",
- "document_type": "Location",
- })
+ dimension1 = frappe.get_doc(
+ {
+ "doctype": "Accounting Dimension",
+ "document_type": "Location",
+ }
+ )
- dimension1.append("dimension_defaults", {
- "company": "_Test Company",
- "reference_document": "Location",
- "default_dimension": "Block 1",
- "mandatory_for_bs": 1
- })
+ dimension1.append(
+ "dimension_defaults",
+ {
+ "company": "_Test Company",
+ "reference_document": "Location",
+ "default_dimension": "Block 1",
+ "mandatory_for_bs": 1,
+ },
+ )
dimension1.insert()
dimension1.save()
@@ -105,6 +120,7 @@ def create_dimension():
dimension1.disabled = 0
dimension1.save()
+
def disable_dimension():
dimension1 = frappe.get_doc("Accounting Dimension", "Department")
dimension1.disabled = 1
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py
index 7d32bad0e78..80f736fa5bb 100644
--- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py
@@ -19,17 +19,27 @@ class AccountingDimensionFilter(Document):
WHERE d.name = a.parent
and d.name != %s
and d.accounting_dimension = %s
- """, (self.name, self.accounting_dimension), as_dict=1)
+ """,
+ (self.name, self.accounting_dimension),
+ as_dict=1,
+ )
account_list = [d.account for d in accounts]
- for account in self.get('accounts'):
+ for account in self.get("accounts"):
if account.applicable_on_account in account_list:
- frappe.throw(_("Row {0}: {1} account already applied for Accounting Dimension {2}").format(
- account.idx, frappe.bold(account.applicable_on_account), frappe.bold(self.accounting_dimension)))
+ frappe.throw(
+ _("Row {0}: {1} account already applied for Accounting Dimension {2}").format(
+ account.idx,
+ frappe.bold(account.applicable_on_account),
+ frappe.bold(self.accounting_dimension),
+ )
+ )
+
def get_dimension_filter_map():
- filters = frappe.db.sql("""
+ filters = frappe.db.sql(
+ """
SELECT
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
p.allow_or_restrict, a.is_mandatory
@@ -40,22 +50,30 @@ def get_dimension_filter_map():
p.name = a.parent
AND p.disabled = 0
AND p.name = d.parent
- """, as_dict=1)
+ """,
+ as_dict=1,
+ )
dimension_filter_map = {}
for f in filters:
f.fieldname = scrub(f.accounting_dimension)
- build_map(dimension_filter_map, f.fieldname, f.applicable_on_account, f.dimension_value,
- f.allow_or_restrict, f.is_mandatory)
+ build_map(
+ dimension_filter_map,
+ f.fieldname,
+ f.applicable_on_account,
+ f.dimension_value,
+ f.allow_or_restrict,
+ f.is_mandatory,
+ )
return dimension_filter_map
+
def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory):
- map_object.setdefault((dimension, account), {
- 'allowed_dimensions': [],
- 'is_mandatory': is_mandatory,
- 'allow_or_restrict': allow_or_restrict
- })
- map_object[(dimension, account)]['allowed_dimensions'].append(filter_value)
+ map_object.setdefault(
+ (dimension, account),
+ {"allowed_dimensions": [], "is_mandatory": is_mandatory, "allow_or_restrict": allow_or_restrict},
+ )
+ map_object[(dimension, account)]["allowed_dimensions"].append(filter_value)
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py
index e2f85ba21a9..f13f2f9f279 100644
--- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py
@@ -12,7 +12,8 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
-test_dependencies = ['Location', 'Cost Center', 'Department']
+test_dependencies = ["Location", "Cost Center", "Department"]
+
class TestAccountingDimensionFilter(unittest.TestCase):
def setUp(self):
@@ -22,9 +23,9 @@ class TestAccountingDimensionFilter(unittest.TestCase):
def test_allowed_dimension_validation(self):
si = create_sales_invoice(do_not_save=1)
- si.items[0].cost_center = 'Main - _TC'
- si.department = 'Accounts - _TC'
- si.location = 'Block 1'
+ si.items[0].cost_center = "Main - _TC"
+ si.department = "Accounts - _TC"
+ si.location = "Block 1"
si.save()
self.assertRaises(InvalidAccountDimensionError, si.submit)
@@ -32,12 +33,12 @@ class TestAccountingDimensionFilter(unittest.TestCase):
def test_mandatory_dimension_validation(self):
si = create_sales_invoice(do_not_save=1)
- si.department = ''
- si.location = 'Block 1'
+ si.department = ""
+ si.location = "Block 1"
# Test with no department for Sales Account
- si.items[0].department = ''
- si.items[0].cost_center = '_Test Cost Center 2 - _TC'
+ si.items[0].department = ""
+ si.items[0].cost_center = "_Test Cost Center 2 - _TC"
si.save()
self.assertRaises(MandatoryAccountDimensionError, si.submit)
@@ -52,53 +53,54 @@ class TestAccountingDimensionFilter(unittest.TestCase):
if si.docstatus == 1:
si.cancel()
+
def create_accounting_dimension_filter():
- if not frappe.db.get_value('Accounting Dimension Filter',
- {'accounting_dimension': 'Cost Center'}):
- frappe.get_doc({
- 'doctype': 'Accounting Dimension Filter',
- 'accounting_dimension': 'Cost Center',
- 'allow_or_restrict': 'Allow',
- 'company': '_Test Company',
- 'accounts': [{
- 'applicable_on_account': 'Sales - _TC',
- }],
- 'dimensions': [{
- 'accounting_dimension': 'Cost Center',
- 'dimension_value': '_Test Cost Center 2 - _TC'
- }]
- }).insert()
+ if not frappe.db.get_value(
+ "Accounting Dimension Filter", {"accounting_dimension": "Cost Center"}
+ ):
+ frappe.get_doc(
+ {
+ "doctype": "Accounting Dimension Filter",
+ "accounting_dimension": "Cost Center",
+ "allow_or_restrict": "Allow",
+ "company": "_Test Company",
+ "accounts": [
+ {
+ "applicable_on_account": "Sales - _TC",
+ }
+ ],
+ "dimensions": [
+ {"accounting_dimension": "Cost Center", "dimension_value": "_Test Cost Center 2 - _TC"}
+ ],
+ }
+ ).insert()
else:
- doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'})
+ doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Cost Center"})
doc.disabled = 0
doc.save()
- if not frappe.db.get_value('Accounting Dimension Filter',
- {'accounting_dimension': 'Department'}):
- frappe.get_doc({
- 'doctype': 'Accounting Dimension Filter',
- 'accounting_dimension': 'Department',
- 'allow_or_restrict': 'Allow',
- 'company': '_Test Company',
- 'accounts': [{
- 'applicable_on_account': 'Sales - _TC',
- 'is_mandatory': 1
- }],
- 'dimensions': [{
- 'accounting_dimension': 'Department',
- 'dimension_value': 'Accounts - _TC'
- }]
- }).insert()
+ if not frappe.db.get_value("Accounting Dimension Filter", {"accounting_dimension": "Department"}):
+ frappe.get_doc(
+ {
+ "doctype": "Accounting Dimension Filter",
+ "accounting_dimension": "Department",
+ "allow_or_restrict": "Allow",
+ "company": "_Test Company",
+ "accounts": [{"applicable_on_account": "Sales - _TC", "is_mandatory": 1}],
+ "dimensions": [{"accounting_dimension": "Department", "dimension_value": "Accounts - _TC"}],
+ }
+ ).insert()
else:
- doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'})
+ doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Department"})
doc.disabled = 0
doc.save()
+
def disable_dimension_filter():
- doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'})
+ doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Cost Center"})
doc.disabled = 1
doc.save()
- doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'})
+ doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Department"})
doc.disabled = 1
doc.save()
diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.py b/erpnext/accounts/doctype/accounting_period/accounting_period.py
index e2949378e5f..0c15d6a207d 100644
--- a/erpnext/accounts/doctype/accounting_period/accounting_period.py
+++ b/erpnext/accounts/doctype/accounting_period/accounting_period.py
@@ -7,7 +7,9 @@ from frappe import _
from frappe.model.document import Document
-class OverlapError(frappe.ValidationError): pass
+class OverlapError(frappe.ValidationError):
+ pass
+
class AccountingPeriod(Document):
def validate(self):
@@ -17,11 +19,12 @@ class AccountingPeriod(Document):
self.bootstrap_doctypes_for_closing()
def autoname(self):
- company_abbr = frappe.get_cached_value('Company', self.company, "abbr")
+ company_abbr = frappe.get_cached_value("Company", self.company, "abbr")
self.name = " - ".join([self.period_name, company_abbr])
def validate_overlap(self):
- existing_accounting_period = frappe.db.sql("""select name from `tabAccounting Period`
+ existing_accounting_period = frappe.db.sql(
+ """select name from `tabAccounting Period`
where (
(%(start_date)s between start_date and end_date)
or (%(end_date)s between start_date and end_date)
@@ -32,18 +35,29 @@ class AccountingPeriod(Document):
"start_date": self.start_date,
"end_date": self.end_date,
"name": self.name,
- "company": self.company
- }, as_dict=True)
+ "company": self.company,
+ },
+ as_dict=True,
+ )
if len(existing_accounting_period) > 0:
- frappe.throw(_("Accounting Period overlaps with {0}")
- .format(existing_accounting_period[0].get("name")), OverlapError)
+ frappe.throw(
+ _("Accounting Period overlaps with {0}").format(existing_accounting_period[0].get("name")),
+ OverlapError,
+ )
@frappe.whitelist()
def get_doctypes_for_closing(self):
docs_for_closing = []
- doctypes = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Payroll Entry", \
- "Bank Clearance", "Asset", "Stock Entry"]
+ doctypes = [
+ "Sales Invoice",
+ "Purchase Invoice",
+ "Journal Entry",
+ "Payroll Entry",
+ "Bank Clearance",
+ "Asset",
+ "Stock Entry",
+ ]
closed_doctypes = [{"document_type": doctype, "closed": 1} for doctype in doctypes]
for closed_doctype in closed_doctypes:
docs_for_closing.append(closed_doctype)
@@ -53,7 +67,7 @@ class AccountingPeriod(Document):
def bootstrap_doctypes_for_closing(self):
if len(self.closed_documents) == 0:
for doctype_for_closing in self.get_doctypes_for_closing():
- self.append('closed_documents', {
- "document_type": doctype_for_closing.document_type,
- "closed": doctype_for_closing.closed
- })
+ self.append(
+ "closed_documents",
+ {"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed},
+ )
diff --git a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
index c06c2e0338b..85025d190f5 100644
--- a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
+++ b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
@@ -10,29 +10,38 @@ from erpnext.accounts.doctype.accounting_period.accounting_period import Overlap
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.general_ledger import ClosedAccountingPeriod
-test_dependencies = ['Item']
+test_dependencies = ["Item"]
+
class TestAccountingPeriod(unittest.TestCase):
def test_overlap(self):
- ap1 = create_accounting_period(start_date = "2018-04-01",
- end_date = "2018-06-30", company = "Wind Power LLC")
+ ap1 = create_accounting_period(
+ start_date="2018-04-01", end_date="2018-06-30", company="Wind Power LLC"
+ )
ap1.save()
- ap2 = create_accounting_period(start_date = "2018-06-30",
- end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1")
+ ap2 = create_accounting_period(
+ start_date="2018-06-30",
+ end_date="2018-07-10",
+ company="Wind Power LLC",
+ period_name="Test Accounting Period 1",
+ )
self.assertRaises(OverlapError, ap2.save)
def test_accounting_period(self):
- ap1 = create_accounting_period(period_name = "Test Accounting Period 2")
+ ap1 = create_accounting_period(period_name="Test Accounting Period 2")
ap1.save()
- doc = create_sales_invoice(do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC")
+ doc = create_sales_invoice(
+ do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
+ )
self.assertRaises(ClosedAccountingPeriod, doc.submit)
def tearDown(self):
for d in frappe.get_all("Accounting Period"):
frappe.delete_doc("Accounting Period", d.name)
+
def create_accounting_period(**args):
args = frappe._dict(args)
@@ -41,8 +50,6 @@ def create_accounting_period(**args):
accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
accounting_period.company = args.company or "_Test Company"
accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
- accounting_period.append("closed_documents", {
- "document_type": 'Sales Invoice', "closed": 1
- })
+ accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1})
return accounting_period
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
index 48392074102..835498176c7 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
@@ -18,11 +18,13 @@ class AccountsSettings(Document):
frappe.clear_cache()
def validate(self):
- frappe.db.set_default("add_taxes_from_item_tax_template",
- self.get("add_taxes_from_item_tax_template", 0))
+ frappe.db.set_default(
+ "add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0)
+ )
- frappe.db.set_default("enable_common_party_accounting",
- self.get("enable_common_party_accounting", 0))
+ frappe.db.set_default(
+ "enable_common_party_accounting", self.get("enable_common_party_accounting", 0)
+ )
self.validate_stale_days()
self.enable_payment_schedule_in_print()
@@ -32,34 +34,91 @@ class AccountsSettings(Document):
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:
frappe.msgprint(
- _("Stale Days should start from 1."), title='Error', indicator='red',
- raise_exception=1)
+ _("Stale Days should start from 1."), title="Error", indicator="red", raise_exception=1
+ )
def enable_payment_schedule_in_print(self):
show_in_print = cint(self.show_payment_schedule_in_print)
for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"):
- make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False)
- make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check", validate_fields_for_doctype=False)
+ make_property_setter(
+ doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False
+ )
+ make_property_setter(
+ doctype,
+ "payment_schedule",
+ "print_hide",
+ 0 if show_in_print else 1,
+ "Check",
+ validate_fields_for_doctype=False,
+ )
def toggle_discount_accounting_fields(self):
enable_discount_accounting = cint(self.enable_discount_accounting)
for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]:
- make_property_setter(doctype, "discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
+ make_property_setter(
+ doctype,
+ "discount_account",
+ "hidden",
+ not (enable_discount_accounting),
+ "Check",
+ validate_fields_for_doctype=False,
+ )
if enable_discount_accounting:
- make_property_setter(doctype, "discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False)
+ make_property_setter(
+ doctype,
+ "discount_account",
+ "mandatory_depends_on",
+ "eval: doc.discount_amount",
+ "Code",
+ validate_fields_for_doctype=False,
+ )
else:
- make_property_setter(doctype, "discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False)
+ make_property_setter(
+ doctype,
+ "discount_account",
+ "mandatory_depends_on",
+ "",
+ "Code",
+ validate_fields_for_doctype=False,
+ )
for doctype in ["Sales Invoice", "Purchase Invoice"]:
- make_property_setter(doctype, "additional_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
+ make_property_setter(
+ doctype,
+ "additional_discount_account",
+ "hidden",
+ not (enable_discount_accounting),
+ "Check",
+ validate_fields_for_doctype=False,
+ )
if enable_discount_accounting:
- make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False)
+ make_property_setter(
+ doctype,
+ "additional_discount_account",
+ "mandatory_depends_on",
+ "eval: doc.discount_amount",
+ "Code",
+ validate_fields_for_doctype=False,
+ )
else:
- make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False)
-
- make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
+ make_property_setter(
+ doctype,
+ "additional_discount_account",
+ "mandatory_depends_on",
+ "",
+ "Code",
+ validate_fields_for_doctype=False,
+ )
+ make_property_setter(
+ "Item",
+ "default_discount_account",
+ "hidden",
+ not (enable_discount_accounting),
+ "Check",
+ validate_fields_for_doctype=False,
+ )
def validate_pending_reposts(self):
if self.acc_frozen_upto:
diff --git a/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.py
index eb90fc7a861..a350cc385da 100644
--- a/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.py
+++ b/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.py
@@ -1,4 +1,3 @@
-
import unittest
import frappe
@@ -8,12 +7,12 @@ class TestAccountsSettings(unittest.TestCase):
def tearDown(self):
# Just in case `save` method succeeds, we need to take things back to default so that other tests
# don't break
- cur_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings')
+ cur_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
cur_settings.allow_stale = 1
cur_settings.save()
def test_stale_days(self):
- cur_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings')
+ cur_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
cur_settings.allow_stale = 0
cur_settings.stale_days = 0
diff --git a/erpnext/accounts/doctype/bank/bank.py b/erpnext/accounts/doctype/bank/bank.py
index f111433c321..d44be9af23e 100644
--- a/erpnext/accounts/doctype/bank/bank.py
+++ b/erpnext/accounts/doctype/bank/bank.py
@@ -15,4 +15,4 @@ class Bank(Document):
load_address_and_contact(self)
def on_trash(self):
- delete_contact_and_address('Bank', self.name)
+ delete_contact_and_address("Bank", self.name)
diff --git a/erpnext/accounts/doctype/bank/bank_dashboard.py b/erpnext/accounts/doctype/bank/bank_dashboard.py
index e7ef6aa25f5..7e40a1a6b8e 100644
--- a/erpnext/accounts/doctype/bank/bank_dashboard.py
+++ b/erpnext/accounts/doctype/bank/bank_dashboard.py
@@ -1,14 +1,8 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'bank',
- 'transactions': [
- {
- 'label': _('Bank Details'),
- 'items': ['Bank Account', 'Bank Guarantee']
- }
- ]
+ "fieldname": "bank",
+ "transactions": [{"label": _("Bank Details"), "items": ["Bank Account", "Bank Guarantee"]}],
}
diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py
index f9140c31d64..addcf62e5b6 100644
--- a/erpnext/accounts/doctype/bank_account/bank_account.py
+++ b/erpnext/accounts/doctype/bank_account/bank_account.py
@@ -20,7 +20,7 @@ class BankAccount(Document):
self.name = self.account_name + " - " + self.bank
def on_trash(self):
- delete_contact_and_address('BankAccount', self.name)
+ delete_contact_and_address("BankAccount", self.name)
def validate(self):
self.validate_company()
@@ -31,9 +31,9 @@ class BankAccount(Document):
frappe.throw(_("Company is manadatory for company account"))
def validate_iban(self):
- '''
+ """
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
- '''
+ """
# IBAN field is optional
if not self.iban:
return
@@ -43,7 +43,7 @@ class BankAccount(Document):
return str(9 + ord(c) - 64)
# remove whitespaces, upper case to get the right number from ord()
- iban = ''.join(self.iban.split(' ')).upper()
+ iban = "".join(self.iban.split(" ")).upper()
# Move country code and checksum from the start to the end
flipped = iban[4:] + iban[:4]
@@ -52,12 +52,12 @@ class BankAccount(Document):
encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped]
try:
- to_check = int(''.join(encoded))
+ to_check = int("".join(encoded))
except ValueError:
- frappe.throw(_('IBAN is not valid'))
+ frappe.throw(_("IBAN is not valid"))
if to_check % 97 != 1:
- frappe.throw(_('IBAN is not valid'))
+ frappe.throw(_("IBAN is not valid"))
@frappe.whitelist()
@@ -69,12 +69,14 @@ def make_bank_account(doctype, docname):
return doc
+
@frappe.whitelist()
def get_party_bank_account(party_type, party):
- return frappe.db.get_value(party_type,
- party, 'default_bank_account')
+ return frappe.db.get_value(party_type, party, "default_bank_account")
+
@frappe.whitelist()
def get_bank_account_details(bank_account):
- return frappe.db.get_value("Bank Account",
- bank_account, ['account', 'bank', 'bank_account_no'], as_dict=1)
+ return frappe.db.get_value(
+ "Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
+ )
diff --git a/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py b/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py
index bc08eab0351..8bf8d8a5cd0 100644
--- a/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py
+++ b/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py
@@ -1,28 +1,20 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'bank_account',
- 'non_standard_fieldnames': {
- 'Customer': 'default_bank_account',
- 'Supplier': 'default_bank_account',
+ "fieldname": "bank_account",
+ "non_standard_fieldnames": {
+ "Customer": "default_bank_account",
+ "Supplier": "default_bank_account",
},
- 'transactions': [
+ "transactions": [
{
- 'label': _('Payments'),
- 'items': ['Payment Entry', 'Payment Request', 'Payment Order', 'Payroll Entry']
+ "label": _("Payments"),
+ "items": ["Payment Entry", "Payment Request", "Payment Order", "Payroll Entry"],
},
- {
- 'label': _('Party'),
- 'items': ['Customer', 'Supplier']
- },
- {
- 'items': ['Bank Guarantee']
- },
- {
- 'items': ['Journal Entry']
- }
- ]
+ {"label": _("Party"), "items": ["Customer", "Supplier"]},
+ {"items": ["Bank Guarantee"]},
+ {"items": ["Journal Entry"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/bank_account/test_bank_account.py b/erpnext/accounts/doctype/bank_account/test_bank_account.py
index 5f23f88af6c..8949524a561 100644
--- a/erpnext/accounts/doctype/bank_account/test_bank_account.py
+++ b/erpnext/accounts/doctype/bank_account/test_bank_account.py
@@ -8,28 +8,28 @@ from frappe import ValidationError
# test_records = frappe.get_test_records('Bank Account')
-class TestBankAccount(unittest.TestCase):
+class TestBankAccount(unittest.TestCase):
def test_validate_iban(self):
valid_ibans = [
- 'GB82 WEST 1234 5698 7654 32',
- 'DE91 1000 0000 0123 4567 89',
- 'FR76 3000 6000 0112 3456 7890 189'
+ "GB82 WEST 1234 5698 7654 32",
+ "DE91 1000 0000 0123 4567 89",
+ "FR76 3000 6000 0112 3456 7890 189",
]
invalid_ibans = [
# wrong checksum (3rd place)
- 'GB72 WEST 1234 5698 7654 32',
- 'DE81 1000 0000 0123 4567 89',
- 'FR66 3000 6000 0112 3456 7890 189'
+ "GB72 WEST 1234 5698 7654 32",
+ "DE81 1000 0000 0123 4567 89",
+ "FR66 3000 6000 0112 3456 7890 189",
]
- bank_account = frappe.get_doc({'doctype':'Bank Account'})
+ bank_account = frappe.get_doc({"doctype": "Bank Account"})
try:
bank_account.validate_iban()
except AttributeError:
- msg = 'BankAccount.validate_iban() failed for empty IBAN'
+ msg = "BankAccount.validate_iban() failed for empty IBAN"
self.fail(msg=msg)
for iban in valid_ibans:
@@ -37,11 +37,11 @@ class TestBankAccount(unittest.TestCase):
try:
bank_account.validate_iban()
except ValidationError:
- msg = 'BankAccount.validate_iban() failed for valid IBAN {}'.format(iban)
+ msg = "BankAccount.validate_iban() failed for valid IBAN {}".format(iban)
self.fail(msg=msg)
for not_iban in invalid_ibans:
bank_account.iban = not_iban
- msg = 'BankAccount.validate_iban() accepted invalid IBAN {}'.format(not_iban)
+ msg = "BankAccount.validate_iban() accepted invalid IBAN {}".format(not_iban)
with self.assertRaises(ValidationError, msg=msg):
bank_account.validate_iban()
diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
index a3bbb2288d3..98ba399a35d 100644
--- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
+++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
@@ -5,11 +5,13 @@
import frappe
from frappe import _, msgprint
from frappe.model.document import Document
-from frappe.utils import flt, fmt_money, getdate, nowdate
+from frappe.query_builder.custom import ConstantColumn
+from frappe.utils import flt, fmt_money, getdate
+
+import erpnext
+
+form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"}
-form_grid_templates = {
- "journal_entries": "templates/form_grid/bank_reconciliation_grid.html"
-}
class BankClearance(Document):
@frappe.whitelist()
@@ -24,7 +26,8 @@ class BankClearance(Document):
if not self.include_reconciled_entries:
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
- journal_entries = frappe.db.sql("""
+ journal_entries = frappe.db.sql(
+ """
select
"Journal Entry" as payment_document, t1.name as payment_entry,
t1.cheque_no as cheque_number, t1.cheque_date,
@@ -38,12 +41,18 @@ class BankClearance(Document):
and ifnull(t1.is_opening, 'No') = 'No' {condition}
group by t2.account, t1.name
order by t1.posting_date ASC, t1.name DESC
- """.format(condition=condition), {"account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1)
+ """.format(
+ condition=condition
+ ),
+ {"account": self.account, "from": self.from_date, "to": self.to_date},
+ as_dict=1,
+ )
if self.bank_account:
- condition += 'and bank_account = %(bank_account)s'
+ condition += "and bank_account = %(bank_account)s"
- payment_entries = frappe.db.sql("""
+ payment_entries = frappe.db.sql(
+ """
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date,
@@ -58,12 +67,69 @@ class BankClearance(Document):
{condition}
order by
posting_date ASC, name DESC
- """.format(condition=condition), {"account": self.account, "from":self.from_date,
- "to": self.to_date, "bank_account": self.bank_account}, as_dict=1)
+ """.format(
+ condition=condition
+ ),
+ {
+ "account": self.account,
+ "from": self.from_date,
+ "to": self.to_date,
+ "bank_account": self.bank_account,
+ },
+ as_dict=1,
+ )
+
+ loan_disbursement = frappe.qb.DocType("Loan Disbursement")
+
+ loan_disbursements = (
+ frappe.qb.from_(loan_disbursement)
+ .select(
+ ConstantColumn("Loan Disbursement").as_("payment_document"),
+ loan_disbursement.name.as_("payment_entry"),
+ loan_disbursement.disbursed_amount.as_("credit"),
+ ConstantColumn(0).as_("debit"),
+ loan_disbursement.reference_number.as_("cheque_number"),
+ loan_disbursement.reference_date.as_("cheque_date"),
+ loan_disbursement.disbursement_date.as_("posting_date"),
+ loan_disbursement.applicant.as_("against_account"),
+ )
+ .where(loan_disbursement.docstatus == 1)
+ .where(loan_disbursement.disbursement_date >= self.from_date)
+ .where(loan_disbursement.disbursement_date <= self.to_date)
+ .where(loan_disbursement.clearance_date.isnull())
+ .where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
+ .orderby(loan_disbursement.disbursement_date)
+ .orderby(loan_disbursement.name, frappe.qb.desc)
+ ).run(as_dict=1)
+
+ loan_repayment = frappe.qb.DocType("Loan Repayment")
+
+ loan_repayments = (
+ frappe.qb.from_(loan_repayment)
+ .select(
+ ConstantColumn("Loan Repayment").as_("payment_document"),
+ loan_repayment.name.as_("payment_entry"),
+ loan_repayment.amount_paid.as_("debit"),
+ ConstantColumn(0).as_("credit"),
+ loan_repayment.reference_number.as_("cheque_number"),
+ loan_repayment.reference_date.as_("cheque_date"),
+ loan_repayment.applicant.as_("against_account"),
+ loan_repayment.posting_date,
+ )
+ .where(loan_repayment.docstatus == 1)
+ .where(loan_repayment.clearance_date.isnull())
+ .where(loan_repayment.repay_from_salary == 0)
+ .where(loan_repayment.posting_date >= self.from_date)
+ .where(loan_repayment.posting_date <= self.to_date)
+ .where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
+ .orderby(loan_repayment.posting_date)
+ .orderby(loan_repayment.name, frappe.qb.desc)
+ ).run(as_dict=1)
pos_sales_invoices, pos_purchase_invoices = [], []
if self.include_pos_transactions:
- pos_sales_invoices = frappe.db.sql("""
+ pos_sales_invoices = frappe.db.sql(
+ """
select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
si.posting_date, si.customer as against_account, sip.clearance_date,
@@ -74,9 +140,13 @@ class BankClearance(Document):
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
order by
si.posting_date ASC, si.name DESC
- """, {"account":self.account, "from":self.from_date, "to":self.to_date}, as_dict=1)
+ """,
+ {"account": self.account, "from": self.from_date, "to": self.to_date},
+ as_dict=1,
+ )
- pos_purchase_invoices = frappe.db.sql("""
+ pos_purchase_invoices = frappe.db.sql(
+ """
select
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
pi.posting_date, pi.supplier as against_account, pi.clearance_date,
@@ -87,21 +157,36 @@ class BankClearance(Document):
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
order by
pi.posting_date ASC, pi.name DESC
- """, {"account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1)
+ """,
+ {"account": self.account, "from": self.from_date, "to": self.to_date},
+ as_dict=1,
+ )
- entries = sorted(list(payment_entries) + list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)),
- key=lambda k: k['posting_date'] or getdate(nowdate()))
+ entries = sorted(
+ list(payment_entries)
+ + list(journal_entries)
+ + list(pos_sales_invoices)
+ + list(pos_purchase_invoices)
+ + list(loan_disbursements)
+ + list(loan_repayments),
+ key=lambda k: getdate(k["posting_date"]),
+ )
- self.set('payment_entries', [])
+ self.set("payment_entries", [])
self.total_amount = 0.0
+ default_currency = erpnext.get_default_currency()
for d in entries:
- row = self.append('payment_entries', {})
+ row = self.append("payment_entries", {})
- amount = flt(d.get('debit', 0)) - flt(d.get('credit', 0))
+ amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0))
+
+ if not d.get("account_currency"):
+ d.account_currency = default_currency
formatted_amount = fmt_money(abs(amount), 2, d.account_currency)
d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr"))
+ d.posting_date = getdate(d.posting_date)
d.pop("credit")
d.pop("debit")
@@ -112,21 +197,24 @@ class BankClearance(Document):
@frappe.whitelist()
def update_clearance_date(self):
clearance_date_updated = False
- for d in self.get('payment_entries'):
+ for d in self.get("payment_entries"):
if d.clearance_date:
if not d.payment_document:
frappe.throw(_("Row #{0}: Payment document is required to complete the transaction"))
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
- frappe.throw(_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}")
- .format(d.idx, d.clearance_date, d.cheque_date))
+ frappe.throw(
+ _("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}").format(
+ d.idx, d.clearance_date, d.cheque_date
+ )
+ )
if d.clearance_date or self.include_reconciled_entries:
if not d.clearance_date:
d.clearance_date = None
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
- payment_entry.db_set('clearance_date', d.clearance_date)
+ payment_entry.db_set("clearance_date", d.clearance_date)
clearance_date_updated = True
diff --git a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py
index 706fbbe245c..c1e55f6f723 100644
--- a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py
+++ b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py
@@ -1,9 +1,96 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-# import frappe
import unittest
+import frappe
+from frappe.utils import add_months, getdate
+
+from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+from erpnext.loan_management.doctype.loan.test_loan import (
+ create_loan,
+ create_loan_accounts,
+ create_loan_type,
+ create_repayment_entry,
+ make_loan_disbursement_entry,
+)
+
class TestBankClearance(unittest.TestCase):
- pass
+ @classmethod
+ def setUpClass(cls):
+ make_bank_account()
+ create_loan_accounts()
+ create_loan_masters()
+ add_transactions()
+
+ # Basic test case to test if bank clearance tool doesn't break
+ # Detailed test can be added later
+ def test_bank_clearance(self):
+ bank_clearance = frappe.get_doc("Bank Clearance")
+ bank_clearance.account = "_Test Bank Clearance - _TC"
+ bank_clearance.from_date = add_months(getdate(), -1)
+ bank_clearance.to_date = getdate()
+ bank_clearance.get_payment_entries()
+ self.assertEqual(len(bank_clearance.payment_entries), 3)
+
+
+def make_bank_account():
+ if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"):
+ frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_type": "Bank",
+ "account_name": "_Test Bank Clearance",
+ "company": "_Test Company",
+ "parent_account": "Bank Accounts - _TC",
+ }
+ ).insert()
+
+
+def create_loan_masters():
+ create_loan_type(
+ "Clearance Loan",
+ 2000000,
+ 13.5,
+ 25,
+ 0,
+ 5,
+ "Cash",
+ "_Test Bank Clearance - _TC",
+ "_Test Bank Clearance - _TC",
+ "Loan Account - _TC",
+ "Interest Income Account - _TC",
+ "Penalty Income Account - _TC",
+ )
+
+
+def add_transactions():
+ make_payment_entry()
+ make_loan()
+
+
+def make_loan():
+ loan = create_loan(
+ "_Test Customer",
+ "Clearance Loan",
+ 280000,
+ "Repay Over Number of Periods",
+ 20,
+ applicant_type="Customer",
+ )
+ loan.submit()
+ make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate())
+ repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount)
+ repayment_entry.save()
+ repayment_entry.submit()
+
+
+def make_payment_entry():
+ pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690)
+ pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC")
+ pe.reference_no = "Conrad Oct 18"
+ pe.reference_date = "2018-10-24"
+ pe.insert()
+ pe.submit()
diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py
index cfbcf16b91e..9144a29c6ef 100644
--- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py
+++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py
@@ -23,10 +23,16 @@ class BankGuarantee(Document):
if not self.bank:
frappe.throw(_("Enter the name of the bank or lending institution before submittting."))
+
@frappe.whitelist()
def get_vouchar_detials(column_list, doctype, docname):
column_list = json.loads(column_list)
for col in column_list:
sanitize_searchfield(col)
- return frappe.db.sql(''' select {columns} from `tab{doctype}` where name=%s'''
- .format(columns=", ".join(column_list), doctype=doctype), docname, as_dict=1)[0]
+ return frappe.db.sql(
+ """ select {columns} from `tab{doctype}` where name=%s""".format(
+ columns=", ".join(column_list), doctype=doctype
+ ),
+ docname,
+ as_dict=1,
+ )[0]
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 4211bd0169d..0efe086d94e 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -7,6 +7,7 @@ import json
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt
from erpnext import get_company_currency
@@ -21,48 +22,63 @@ from erpnext.accounts.utils import get_balance_on
class BankReconciliationTool(Document):
pass
+
@frappe.whitelist()
-def get_bank_transactions(bank_account, from_date = None, to_date = None):
+def get_bank_transactions(bank_account, from_date=None, to_date=None):
# returns bank transactions for a bank account
filters = []
- filters.append(['bank_account', '=', bank_account])
- filters.append(['docstatus', '=', 1])
- filters.append(['unallocated_amount', '>', 0])
+ filters.append(["bank_account", "=", bank_account])
+ filters.append(["docstatus", "=", 1])
+ filters.append(["unallocated_amount", ">", 0])
if to_date:
- filters.append(['date', '<=', to_date])
+ filters.append(["date", "<=", to_date])
if from_date:
- filters.append(['date', '>=', from_date])
+ filters.append(["date", ">=", from_date])
transactions = frappe.get_all(
- 'Bank Transaction',
- fields = ['date', 'deposit', 'withdrawal', 'currency',
- 'description', 'name', 'bank_account', 'company',
- 'unallocated_amount', 'reference_number', 'party_type', 'party'],
- filters = filters
+ "Bank Transaction",
+ fields=[
+ "date",
+ "deposit",
+ "withdrawal",
+ "currency",
+ "description",
+ "name",
+ "bank_account",
+ "company",
+ "unallocated_amount",
+ "reference_number",
+ "party_type",
+ "party",
+ ],
+ filters=filters,
)
return transactions
+
@frappe.whitelist()
def get_account_balance(bank_account, till_date):
# returns account balance till the specified date
- account = frappe.db.get_value('Bank Account', bank_account, 'account')
- filters = frappe._dict({
- "account": account,
- "report_date": till_date,
- "include_pos_transactions": 1
- })
+ account = frappe.db.get_value("Bank Account", bank_account, "account")
+ filters = frappe._dict(
+ {"account": account, "report_date": till_date, "include_pos_transactions": 1}
+ )
data = get_entries(filters)
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
- total_debit, total_credit = 0,0
+ total_debit, total_credit = 0, 0
for d in data:
total_debit += flt(d.debit)
total_credit += flt(d.credit)
amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters)
- bank_bal = flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) \
+ bank_bal = (
+ flt(balance_as_per_system)
+ - flt(total_debit)
+ + flt(total_credit)
+ amounts_not_reflected_in_system
+ )
return bank_bal
@@ -75,71 +91,94 @@ def update_bank_transaction(bank_transaction_name, reference_number, party_type=
bank_transaction.party_type = party_type
bank_transaction.party = party
bank_transaction.save()
- return frappe.db.get_all('Bank Transaction',
- filters={
- 'name': bank_transaction_name
- },
- fields=['date', 'deposit', 'withdrawal', 'currency',
- 'description', 'name', 'bank_account', 'company',
- 'unallocated_amount', 'reference_number',
- 'party_type', 'party'],
+ return frappe.db.get_all(
+ "Bank Transaction",
+ filters={"name": bank_transaction_name},
+ fields=[
+ "date",
+ "deposit",
+ "withdrawal",
+ "currency",
+ "description",
+ "name",
+ "bank_account",
+ "company",
+ "unallocated_amount",
+ "reference_number",
+ "party_type",
+ "party",
+ ],
)[0]
@frappe.whitelist()
-def create_journal_entry_bts( bank_transaction_name, reference_number=None, reference_date=None, posting_date=None, entry_type=None,
- second_account=None, mode_of_payment=None, party_type=None, party=None, allow_edit=None):
+def create_journal_entry_bts(
+ bank_transaction_name,
+ reference_number=None,
+ reference_date=None,
+ posting_date=None,
+ entry_type=None,
+ second_account=None,
+ mode_of_payment=None,
+ party_type=None,
+ party=None,
+ allow_edit=None,
+):
# Create a new journal entry based on the bank transaction
bank_transaction = frappe.db.get_values(
- "Bank Transaction", bank_transaction_name,
- fieldname=["name", "deposit", "withdrawal", "bank_account"] ,
- as_dict=True
+ "Bank Transaction",
+ bank_transaction_name,
+ fieldname=["name", "deposit", "withdrawal", "bank_account"],
+ as_dict=True,
)[0]
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
account_type = frappe.db.get_value("Account", second_account, "account_type")
if account_type in ["Receivable", "Payable"]:
if not (party_type and party):
- frappe.throw(_("Party Type and Party is required for Receivable / Payable account {0}").format( second_account))
+ frappe.throw(
+ _("Party Type and Party is required for Receivable / Payable account {0}").format(
+ second_account
+ )
+ )
accounts = []
# Multi Currency?
- accounts.append({
+ accounts.append(
+ {
"account": second_account,
- "credit_in_account_currency": bank_transaction.deposit
- if bank_transaction.deposit > 0
- else 0,
- "debit_in_account_currency":bank_transaction.withdrawal
- if bank_transaction.withdrawal > 0
- else 0,
- "party_type":party_type,
- "party":party,
- })
+ "credit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
+ "debit_in_account_currency": bank_transaction.withdrawal
+ if bank_transaction.withdrawal > 0
+ else 0,
+ "party_type": party_type,
+ "party": party,
+ }
+ )
- accounts.append({
+ accounts.append(
+ {
"account": company_account,
"bank_account": bank_transaction.bank_account,
"credit_in_account_currency": bank_transaction.withdrawal
- if bank_transaction.withdrawal > 0
- else 0,
- "debit_in_account_currency":bank_transaction.deposit
- if bank_transaction.deposit > 0
- else 0,
- })
+ if bank_transaction.withdrawal > 0
+ else 0,
+ "debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
+ }
+ )
company = frappe.get_value("Account", company_account, "company")
journal_entry_dict = {
- "voucher_type" : entry_type,
- "company" : company,
- "posting_date" : posting_date,
- "cheque_date" : reference_date,
- "cheque_no" : reference_number,
- "mode_of_payment" : mode_of_payment
+ "voucher_type": entry_type,
+ "company": company,
+ "posting_date": posting_date,
+ "cheque_date": reference_date,
+ "cheque_no": reference_number,
+ "mode_of_payment": mode_of_payment,
}
- journal_entry = frappe.new_doc('Journal Entry')
+ journal_entry = frappe.new_doc("Journal Entry")
journal_entry.update(journal_entry_dict)
journal_entry.set("accounts", accounts)
-
if allow_edit:
return journal_entry
@@ -151,21 +190,32 @@ def create_journal_entry_bts( bank_transaction_name, reference_number=None, refe
else:
paid_amount = bank_transaction.withdrawal
- vouchers = json.dumps([{
- "payment_doctype":"Journal Entry",
- "payment_name":journal_entry.name,
- "amount":paid_amount}])
+ vouchers = json.dumps(
+ [{"payment_doctype": "Journal Entry", "payment_name": journal_entry.name, "amount": paid_amount}]
+ )
return reconcile_vouchers(bank_transaction.name, vouchers)
+
@frappe.whitelist()
-def create_payment_entry_bts( bank_transaction_name, reference_number=None, reference_date=None, party_type=None, party=None, posting_date=None,
- mode_of_payment=None, project=None, cost_center=None, allow_edit=None):
+def create_payment_entry_bts(
+ bank_transaction_name,
+ reference_number=None,
+ reference_date=None,
+ party_type=None,
+ party=None,
+ posting_date=None,
+ mode_of_payment=None,
+ project=None,
+ cost_center=None,
+ allow_edit=None,
+):
# Create a new payment entry based on the bank transaction
bank_transaction = frappe.db.get_values(
- "Bank Transaction", bank_transaction_name,
- fieldname=["name", "unallocated_amount", "deposit", "bank_account"] ,
- as_dict=True
+ "Bank Transaction",
+ bank_transaction_name,
+ fieldname=["name", "unallocated_amount", "deposit", "bank_account"],
+ as_dict=True,
)[0]
paid_amount = bank_transaction.unallocated_amount
payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay"
@@ -173,27 +223,26 @@ def create_payment_entry_bts( bank_transaction_name, reference_number=None, refe
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
company = frappe.get_value("Account", company_account, "company")
payment_entry_dict = {
- "company" : company,
- "payment_type" : payment_type,
- "reference_no" : reference_number,
- "reference_date" : reference_date,
- "party_type" : party_type,
- "party" : party,
- "posting_date" : posting_date,
+ "company": company,
+ "payment_type": payment_type,
+ "reference_no": reference_number,
+ "reference_date": reference_date,
+ "party_type": party_type,
+ "party": party,
+ "posting_date": posting_date,
"paid_amount": paid_amount,
- "received_amount": paid_amount
+ "received_amount": paid_amount,
}
payment_entry = frappe.new_doc("Payment Entry")
-
payment_entry.update(payment_entry_dict)
if mode_of_payment:
- payment_entry.mode_of_payment = mode_of_payment
+ payment_entry.mode_of_payment = mode_of_payment
if project:
- payment_entry.project = project
+ payment_entry.project = project
if cost_center:
- payment_entry.cost_center = cost_center
+ payment_entry.cost_center = cost_center
if payment_type == "Receive":
payment_entry.paid_to = company_account
else:
@@ -207,80 +256,111 @@ def create_payment_entry_bts( bank_transaction_name, reference_number=None, refe
payment_entry.insert()
payment_entry.submit()
- vouchers = json.dumps([{
- "payment_doctype":"Payment Entry",
- "payment_name":payment_entry.name,
- "amount":paid_amount}])
+ vouchers = json.dumps(
+ [{"payment_doctype": "Payment Entry", "payment_name": payment_entry.name, "amount": paid_amount}]
+ )
return reconcile_vouchers(bank_transaction.name, vouchers)
+
@frappe.whitelist()
def reconcile_vouchers(bank_transaction_name, vouchers):
# updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
- company_account = frappe.db.get_value('Bank Account', transaction.bank_account, 'account')
+ company_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
if transaction.unallocated_amount == 0:
frappe.throw(_("This bank transaction is already fully reconciled"))
total_amount = 0
for voucher in vouchers:
- voucher['payment_entry'] = frappe.get_doc(voucher['payment_doctype'], voucher['payment_name'])
- total_amount += get_paid_amount(frappe._dict({
- 'payment_document': voucher['payment_doctype'],
- 'payment_entry': voucher['payment_name'],
- }), transaction.currency, company_account)
+ voucher["payment_entry"] = frappe.get_doc(voucher["payment_doctype"], voucher["payment_name"])
+ total_amount += get_paid_amount(
+ frappe._dict(
+ {
+ "payment_document": voucher["payment_doctype"],
+ "payment_entry": voucher["payment_name"],
+ }
+ ),
+ transaction.currency,
+ company_account,
+ )
if total_amount > transaction.unallocated_amount:
- frappe.throw(_("The Sum Total of Amounts of All Selected Vouchers Should be Less than the Unallocated Amount of the Bank Transaction"))
+ frappe.throw(
+ _(
+ "The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction"
+ )
+ )
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
for voucher in vouchers:
- gl_entry = frappe.db.get_value("GL Entry", dict(account=account, voucher_type=voucher['payment_doctype'], voucher_no=voucher['payment_name']), ['credit', 'debit'], as_dict=1)
- gl_amount, transaction_amount = (gl_entry.credit, transaction.deposit) if gl_entry.credit > 0 else (gl_entry.debit, transaction.withdrawal)
+ gl_entry = frappe.db.get_value(
+ "GL Entry",
+ dict(
+ account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
+ ),
+ ["credit", "debit"],
+ as_dict=1,
+ )
+ gl_amount, transaction_amount = (
+ (gl_entry.credit, transaction.deposit)
+ if gl_entry.credit > 0
+ else (gl_entry.debit, transaction.withdrawal)
+ )
allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
- transaction.append("payment_entries", {
- "payment_document": voucher['payment_entry'].doctype,
- "payment_entry": voucher['payment_entry'].name,
- "allocated_amount": allocated_amount
- })
+ transaction.append(
+ "payment_entries",
+ {
+ "payment_document": voucher["payment_entry"].doctype,
+ "payment_entry": voucher["payment_entry"].name,
+ "allocated_amount": allocated_amount,
+ },
+ )
transaction.save()
transaction.update_allocations()
return frappe.get_doc("Bank Transaction", bank_transaction_name)
+
@frappe.whitelist()
-def get_linked_payments(bank_transaction_name, document_types = None):
+def get_linked_payments(bank_transaction_name, document_types=None):
# get all matching payments for a bank transaction
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
bank_account = frappe.db.get_values(
- "Bank Account",
- transaction.bank_account,
- ["account", "company"],
- as_dict=True)[0]
+ "Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
+ )[0]
(account, company) = (bank_account.account, bank_account.company)
matching = check_matching(account, company, transaction, document_types)
return matching
+
def check_matching(bank_account, company, transaction, document_types):
# combine all types of vouchers
subquery = get_queries(bank_account, company, transaction, document_types)
filters = {
- "amount": transaction.unallocated_amount,
- "payment_type" : "Receive" if transaction.deposit > 0 else "Pay",
- "reference_no": transaction.reference_number,
- "party_type": transaction.party_type,
- "party": transaction.party,
- "bank_account": bank_account
- }
+ "amount": transaction.unallocated_amount,
+ "payment_type": "Receive" if transaction.deposit > 0 else "Pay",
+ "reference_no": transaction.reference_number,
+ "party_type": transaction.party_type,
+ "party": transaction.party,
+ "bank_account": bank_account,
+ }
matching_vouchers = []
+
+ matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters))
+
for query in subquery:
matching_vouchers.extend(
- frappe.db.sql(query, filters,)
+ frappe.db.sql(
+ query,
+ filters,
+ )
)
- return sorted(matching_vouchers, key = lambda x: x[0], reverse=True) if matching_vouchers else []
+ return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else []
+
def get_queries(bank_account, company, transaction, document_types):
# get queries to get matching vouchers
@@ -297,7 +377,7 @@ def get_queries(bank_account, company, transaction, document_types):
queries.extend([je_amount_matching])
if transaction.deposit > 0 and "sales_invoice" in document_types:
- si_amount_matching = get_si_matching_query(amount_condition)
+ si_amount_matching = get_si_matching_query(amount_condition)
queries.extend([si_amount_matching])
if transaction.withdrawal > 0:
@@ -311,13 +391,104 @@ def get_queries(bank_account, company, transaction, document_types):
return queries
+
+def get_loan_vouchers(bank_account, transaction, document_types, filters):
+ vouchers = []
+ amount_condition = True if "exact_match" in document_types else False
+
+ if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
+ vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters))
+
+ if transaction.deposit > 0 and "loan_repayment" in document_types:
+ vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
+
+ return vouchers
+
+
+def get_ld_matching_query(bank_account, amount_condition, filters):
+ loan_disbursement = frappe.qb.DocType("Loan Disbursement")
+ matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
+ matching_party = loan_disbursement.applicant_type == filters.get(
+ "party_type"
+ ) and loan_disbursement.applicant == filters.get("party")
+
+ rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0)
+
+ rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0)
+
+ query = (
+ frappe.qb.from_(loan_disbursement)
+ .select(
+ rank + rank1 + 1,
+ ConstantColumn("Loan Disbursement").as_("doctype"),
+ loan_disbursement.name,
+ loan_disbursement.disbursed_amount,
+ loan_disbursement.reference_number,
+ loan_disbursement.reference_date,
+ loan_disbursement.applicant_type,
+ loan_disbursement.disbursement_date,
+ )
+ .where(loan_disbursement.docstatus == 1)
+ .where(loan_disbursement.clearance_date.isnull())
+ .where(loan_disbursement.disbursement_account == bank_account)
+ )
+
+ if amount_condition:
+ query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
+ else:
+ query.where(loan_disbursement.disbursed_amount <= filters.get("amount"))
+
+ vouchers = query.run(as_list=True)
+
+ return vouchers
+
+
+def get_lr_matching_query(bank_account, amount_condition, filters):
+ loan_repayment = frappe.qb.DocType("Loan Repayment")
+ matching_reference = loan_repayment.reference_number == filters.get("reference_number")
+ matching_party = loan_repayment.applicant_type == filters.get(
+ "party_type"
+ ) and loan_repayment.applicant == filters.get("party")
+
+ rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0)
+
+ rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0)
+
+ query = (
+ frappe.qb.from_(loan_repayment)
+ .select(
+ rank + rank1 + 1,
+ ConstantColumn("Loan Repayment").as_("doctype"),
+ loan_repayment.name,
+ loan_repayment.amount_paid,
+ loan_repayment.reference_number,
+ loan_repayment.reference_date,
+ loan_repayment.applicant_type,
+ loan_repayment.posting_date,
+ )
+ .where(loan_repayment.docstatus == 1)
+ .where(loan_repayment.repay_from_salary == 0)
+ .where(loan_repayment.clearance_date.isnull())
+ .where(loan_repayment.payment_account == bank_account)
+ )
+
+ if amount_condition:
+ query.where(loan_repayment.amount_paid == filters.get("amount"))
+ else:
+ query.where(loan_repayment.amount_paid <= filters.get("amount"))
+
+ vouchers = query.run()
+
+ return vouchers
+
+
def get_pe_matching_query(amount_condition, account_from_to, transaction):
# get matching payment entries query
if transaction.deposit > 0:
currency_field = "paid_to_account_currency as currency"
else:
currency_field = "paid_from_account_currency as currency"
- return f"""
+ return f"""
SELECT
(CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
+ CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
@@ -348,7 +519,6 @@ def get_je_matching_query(amount_condition, transaction):
# We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
- company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
return f"""
@@ -407,6 +577,7 @@ def get_si_matching_query(amount_condition):
AND si.docstatus = 1
"""
+
def get_pi_matching_query(amount_condition):
# get matching purchase invoice query
return f"""
@@ -432,11 +603,16 @@ def get_pi_matching_query(amount_condition):
AND cash_bank_account = %(bank_account)s
"""
+
def get_ec_matching_query(bank_account, company, amount_condition):
# get matching Expense Claim query
- mode_of_payments = [x["parent"] for x in frappe.db.get_all("Mode of Payment Account",
- filters={"default_account": bank_account}, fields=["parent"])]
- mode_of_payments = '(\'' + '\', \''.join(mode_of_payments) + '\' )'
+ mode_of_payments = [
+ x["parent"]
+ for x in frappe.db.get_all(
+ "Mode of Payment Account", filters={"default_account": bank_account}, fields=["parent"]
+ )
+ ]
+ mode_of_payments = "('" + "', '".join(mode_of_payments) + "' )"
company_currency = get_company_currency(company)
return f"""
SELECT
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
index 57434bdd829..0370b3d91db 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
@@ -19,6 +19,7 @@ from six import string_types
INVALID_VALUES = ("", None)
+
class BankStatementImport(DataImport):
def __init__(self, *args, **kwargs):
super(BankStatementImport, self).__init__(*args, **kwargs)
@@ -50,16 +51,14 @@ class BankStatementImport(DataImport):
self.import_file, self.google_sheets_url
)
- if 'Bank Account' not in json.dumps(preview['columns']):
+ if "Bank Account" not in json.dumps(preview["columns"]):
frappe.throw(_("Please add the Bank Account column"))
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test:
- frappe.throw(
- _("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")
- )
+ frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
enqueued_jobs = [d.get("job_name") for d in get_info()]
@@ -82,21 +81,25 @@ class BankStatementImport(DataImport):
return False
+
@frappe.whitelist()
def get_preview_from_template(data_import, import_file=None, google_sheets_url=None):
return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
import_file, google_sheets_url
)
+
@frappe.whitelist()
def form_start_import(data_import):
return frappe.get_doc("Bank Statement Import", data_import).start_import()
+
@frappe.whitelist()
def download_errored_template(data_import_name):
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
data_import.export_errored_rows()
+
def parse_data_from_template(raw_data):
data = []
@@ -109,7 +112,10 @@ def parse_data_from_template(raw_data):
return data
-def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options):
+
+def start_import(
+ data_import, bank_account, import_file_path, google_sheets_url, bank, template_options
+):
"""This method runs in background job"""
update_mapping_db(bank, template_options)
@@ -117,7 +123,7 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
data_import = frappe.get_doc("Bank Statement Import", data_import)
file = import_file_path if import_file_path else google_sheets_url
- import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records")
+ import_file = ImportFile("Bank Transaction", file=file, import_type="Insert New Records")
data = parse_data_from_template(import_file.raw_data)
@@ -137,16 +143,18 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
frappe.publish_realtime("data_import_refresh", {"data_import": data_import.name})
+
def update_mapping_db(bank, template_options):
bank = frappe.get_doc("Bank", bank)
for d in bank.bank_transaction_mapping:
d.delete()
for d in json.loads(template_options)["column_to_field_map"].items():
- bank.append("bank_transaction_mapping", {"bank_transaction_field": d[1] ,"file_field": d[0]} )
+ bank.append("bank_transaction_mapping", {"bank_transaction_field": d[1], "file_field": d[0]})
bank.save()
+
def add_bank_account(data, bank_account):
bank_account_loc = None
if "Bank Account" not in data[0]:
@@ -162,6 +170,7 @@ def add_bank_account(data, bank_account):
else:
row.append(bank_account)
+
def write_files(import_file, data):
full_file_path = import_file.file_doc.get_full_path()
parts = import_file.file_doc.get_extension()
@@ -169,11 +178,12 @@ def write_files(import_file, data):
extension = extension.lstrip(".")
if extension == "csv":
- with open(full_file_path, 'w', newline='') as file:
+ with open(full_file_path, "w", newline="") as file:
writer = csv.writer(file)
writer.writerows(data)
elif extension == "xlsx" or "xls":
- write_xlsx(data, "trans", file_path = full_file_path)
+ write_xlsx(data, "trans", file_path=full_file_path)
+
def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
# from xlsx utils with changes
@@ -188,19 +198,21 @@ def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
ws.column_dimensions[get_column_letter(i + 1)].width = column_width
row1 = ws.row_dimensions[1]
- row1.font = Font(name='Calibri', bold=True)
+ row1.font = Font(name="Calibri", bold=True)
for row in data:
clean_row = []
for item in row:
- if isinstance(item, string_types) and (sheet_name not in ['Data Import Template', 'Data Export']):
+ if isinstance(item, string_types) and (
+ sheet_name not in ["Data Import Template", "Data Export"]
+ ):
value = handle_html(item)
else:
value = item
if isinstance(item, string_types) and next(ILLEGAL_CHARACTERS_RE.finditer(value), None):
# Remove illegal characters from the string
- value = re.sub(ILLEGAL_CHARACTERS_RE, '', value)
+ value = re.sub(ILLEGAL_CHARACTERS_RE, "", value)
clean_row.append(value)
@@ -209,19 +221,20 @@ def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
wb.save(file_path)
return True
+
@frappe.whitelist()
def upload_bank_statement(**args):
args = frappe._dict(args)
bsi = frappe.new_doc("Bank Statement Import")
if args.company:
- bsi.update({
- "company": args.company,
- })
+ bsi.update(
+ {
+ "company": args.company,
+ }
+ )
if args.bank_account:
- bsi.update({
- "bank_account": args.bank_account
- })
+ bsi.update({"bank_account": args.bank_account})
return bsi
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 44cea31ed38..e43f18b5c74 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -28,17 +28,26 @@ class BankTransaction(StatusUpdater):
def update_allocations(self):
if self.payment_entries:
- allocated_amount = reduce(lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries])
+ allocated_amount = reduce(
+ lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]
+ )
else:
allocated_amount = 0
if allocated_amount:
frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount))
- frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount))
+ frappe.db.set_value(
+ self.doctype,
+ self.name,
+ "unallocated_amount",
+ abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount),
+ )
else:
frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0)
- frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit)))
+ frappe.db.set_value(
+ self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))
+ )
amount = self.deposit or self.withdrawal
if amount == self.allocated_amount:
@@ -48,7 +57,14 @@ class BankTransaction(StatusUpdater):
def clear_linked_payment_entries(self, for_cancel=False):
for payment_entry in self.payment_entries:
- if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
+ if payment_entry.payment_document in [
+ "Payment Entry",
+ "Journal Entry",
+ "Purchase Invoice",
+ "Expense Claim",
+ "Loan Repayment",
+ "Loan Disbursement",
+ ]:
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document == "Sales Invoice":
@@ -56,38 +72,41 @@ class BankTransaction(StatusUpdater):
def clear_simple_entry(self, payment_entry, for_cancel=False):
if payment_entry.payment_document == "Payment Entry":
- if frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type") == "Internal Transfer":
+ if (
+ frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type")
+ == "Internal Transfer"
+ ):
if len(get_reconciled_bank_transactions(payment_entry)) < 2:
return
clearance_date = self.date if not for_cancel else None
frappe.db.set_value(
- payment_entry.payment_document, payment_entry.payment_entry,
- "clearance_date", clearance_date)
+ payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date
+ )
def clear_sales_invoice(self, payment_entry, for_cancel=False):
clearance_date = self.date if not for_cancel else None
frappe.db.set_value(
"Sales Invoice Payment",
- dict(
- parenttype=payment_entry.payment_document,
- parent=payment_entry.payment_entry
- ),
- "clearance_date", clearance_date)
+ dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry),
+ "clearance_date",
+ clearance_date,
+ )
+
def get_reconciled_bank_transactions(payment_entry):
reconciled_bank_transactions = frappe.get_all(
- 'Bank Transaction Payments',
- filters = {
- 'payment_entry': payment_entry.payment_entry
- },
- fields = ['parent']
+ "Bank Transaction Payments",
+ filters={"payment_entry": payment_entry.payment_entry},
+ fields=["parent"],
)
return reconciled_bank_transactions
+
def get_total_allocated_amount(payment_entry):
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
SELECT
SUM(btp.allocated_amount) as allocated_amount,
bt.name
@@ -100,36 +119,73 @@ def get_total_allocated_amount(payment_entry):
AND
btp.payment_entry = %s
AND
- bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True)
+ bt.docstatus = 1""",
+ (payment_entry.payment_document, payment_entry.payment_entry),
+ as_dict=True,
+ )
+
def get_paid_amount(payment_entry, currency, bank_account):
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
paid_amount_field = "paid_amount"
- if payment_entry.payment_document == 'Payment Entry':
+ if payment_entry.payment_document == "Payment Entry":
doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry)
- paid_amount_field = ("base_paid_amount"
- if doc.paid_to_account_currency == currency else "paid_amount")
- return frappe.db.get_value(payment_entry.payment_document,
- payment_entry.payment_entry, paid_amount_field)
+ if doc.payment_type == "Receive":
+ paid_amount_field = (
+ "received_amount" if doc.paid_to_account_currency == currency else "base_received_amount"
+ )
+ elif doc.payment_type == "Pay":
+ paid_amount_field = (
+ "paid_amount" if doc.paid_to_account_currency == currency else "base_paid_amount"
+ )
+
+ return frappe.db.get_value(
+ payment_entry.payment_document, payment_entry.payment_entry, paid_amount_field
+ )
elif payment_entry.payment_document == "Journal Entry":
- return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)")
+ return frappe.db.get_value(
+ "Journal Entry Account",
+ {"parent": payment_entry.payment_entry, "account": bank_account},
+ "sum(credit_in_account_currency)",
+ )
elif payment_entry.payment_document == "Expense Claim":
- return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
+ return frappe.db.get_value(
+ payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed"
+ )
+
+ elif payment_entry.payment_document == "Loan Disbursement":
+ return frappe.db.get_value(
+ payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount"
+ )
+
+ elif payment_entry.payment_document == "Loan Repayment":
+ return frappe.db.get_value(
+ payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
+ )
else:
- frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry))
+ frappe.throw(
+ "Please reconcile {0}: {1} manually".format(
+ payment_entry.payment_document, payment_entry.payment_entry
+ )
+ )
+
@frappe.whitelist()
def unclear_reference_payment(doctype, docname):
if frappe.db.exists(doctype, docname):
doc = frappe.get_doc(doctype, docname)
if doctype == "Sales Invoice":
- frappe.db.set_value("Sales Invoice Payment", dict(parenttype=doc.payment_document,
- parent=doc.payment_entry), "clearance_date", None)
+ frappe.db.set_value(
+ "Sales Invoice Payment",
+ dict(parenttype=doc.payment_document, parent=doc.payment_entry),
+ "clearance_date",
+ None,
+ )
else:
frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py
index 6125c27cdfd..494459ef0ea 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py
@@ -19,12 +19,14 @@ def upload_bank_statement():
fcontent = frappe.local.uploaded_file
fname = frappe.local.uploaded_filename
- if frappe.safe_encode(fname).lower().endswith("csv".encode('utf-8')):
+ if frappe.safe_encode(fname).lower().endswith("csv".encode("utf-8")):
from frappe.utils.csvutils import read_csv_content
+
rows = read_csv_content(fcontent, False)
- elif frappe.safe_encode(fname).lower().endswith("xlsx".encode('utf-8')):
+ elif frappe.safe_encode(fname).lower().endswith("xlsx".encode("utf-8")):
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
+
rows = read_xlsx_file_from_attached_file(fcontent=fcontent)
columns = rows[0]
@@ -44,12 +46,10 @@ def create_bank_entries(columns, data, bank_account):
continue
fields = {}
for key, value in iteritems(header_map):
- fields.update({key: d[int(value)-1]})
+ fields.update({key: d[int(value) - 1]})
try:
- bank_transaction = frappe.get_doc({
- "doctype": "Bank Transaction"
- })
+ bank_transaction = frappe.get_doc({"doctype": "Bank Transaction"})
bank_transaction.update(fields)
bank_transaction.date = getdate(parse_date(bank_transaction.date))
bank_transaction.bank_account = bank_account
@@ -62,6 +62,7 @@ def create_bank_entries(columns, data, bank_account):
return {"success": success, "errors": errors}
+
def get_header_mapping(columns, bank_account):
mapping = get_bank_mapping(bank_account)
@@ -72,10 +73,11 @@ def get_header_mapping(columns, bank_account):
return header_map
+
def get_bank_mapping(bank_account):
bank_name = frappe.db.get_value("Bank Account", bank_account, "bank")
bank = frappe.get_doc("Bank", bank_name)
- mapping = {row.file_field:row.bank_transaction_field for row in bank.bank_transaction_mapping}
+ mapping = {row.file_field: row.bank_transaction_field for row in bank.bank_transaction_mapping}
return mapping
diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index 72b6893faf5..ad8e73d27bf 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
@@ -17,6 +17,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
test_dependencies = ["Item", "Cost Center"]
+
class TestBankTransaction(unittest.TestCase):
@classmethod
def setUpClass(cls):
@@ -41,21 +42,34 @@ class TestBankTransaction(unittest.TestCase):
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
def test_linked_payments(self):
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"))
- linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match'])
+ bank_transaction = frappe.get_doc(
+ "Bank Transaction",
+ dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"),
+ )
+ linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"])
self.assertTrue(linked_payments[0][6] == "Conrad Electronic")
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
def test_reconcile(self):
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"))
+ bank_transaction = frappe.get_doc(
+ "Bank Transaction",
+ dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"),
+ )
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
- vouchers = json.dumps([{
- "payment_doctype":"Payment Entry",
- "payment_name":payment.name,
- "amount":bank_transaction.unallocated_amount}])
+ vouchers = json.dumps(
+ [
+ {
+ "payment_doctype": "Payment Entry",
+ "payment_name": payment.name,
+ "amount": bank_transaction.unallocated_amount,
+ }
+ ]
+ )
reconcile_vouchers(bank_transaction.name, vouchers)
- unallocated_amount = frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount")
+ unallocated_amount = frappe.db.get_value(
+ "Bank Transaction", bank_transaction.name, "unallocated_amount"
+ )
self.assertTrue(unallocated_amount == 0)
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
@@ -69,122 +83,177 @@ class TestBankTransaction(unittest.TestCase):
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
def test_debit_credit_output(self):
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"))
- linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match'])
+ bank_transaction = frappe.get_doc(
+ "Bank Transaction",
+ dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"),
+ )
+ linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"])
self.assertTrue(linked_payments[0][3])
# Check error if already reconciled
def test_already_reconciled(self):
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
+ bank_transaction = frappe.get_doc(
+ "Bank Transaction",
+ dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"),
+ )
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
- vouchers = json.dumps([{
- "payment_doctype":"Payment Entry",
- "payment_name":payment.name,
- "amount":bank_transaction.unallocated_amount}])
+ vouchers = json.dumps(
+ [
+ {
+ "payment_doctype": "Payment Entry",
+ "payment_name": payment.name,
+ "amount": bank_transaction.unallocated_amount,
+ }
+ ]
+ )
reconcile_vouchers(bank_transaction.name, vouchers)
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
+ bank_transaction = frappe.get_doc(
+ "Bank Transaction",
+ dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"),
+ )
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
- vouchers = json.dumps([{
- "payment_doctype":"Payment Entry",
- "payment_name":payment.name,
- "amount":bank_transaction.unallocated_amount}])
- self.assertRaises(frappe.ValidationError, reconcile_vouchers, bank_transaction_name=bank_transaction.name, vouchers=vouchers)
+ vouchers = json.dumps(
+ [
+ {
+ "payment_doctype": "Payment Entry",
+ "payment_name": payment.name,
+ "amount": bank_transaction.unallocated_amount,
+ }
+ ]
+ )
+ self.assertRaises(
+ frappe.ValidationError,
+ reconcile_vouchers,
+ bank_transaction_name=bank_transaction.name,
+ vouchers=vouchers,
+ )
# Raise an error if debitor transaction vs debitor payment
def test_clear_sales_invoice(self):
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio"))
+ bank_transaction = frappe.get_doc(
+ "Bank Transaction",
+ dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio"),
+ )
payment = frappe.get_doc("Sales Invoice", dict(customer="Fayva", status=["=", "Paid"]))
- vouchers = json.dumps([{
- "payment_doctype":"Sales Invoice",
- "payment_name":payment.name,
- "amount":bank_transaction.unallocated_amount}])
+ vouchers = json.dumps(
+ [
+ {
+ "payment_doctype": "Sales Invoice",
+ "payment_name": payment.name,
+ "amount": bank_transaction.unallocated_amount,
+ }
+ ]
+ )
reconcile_vouchers(bank_transaction.name, vouchers=vouchers)
- self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0)
- self.assertTrue(frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") is not None)
+ self.assertEqual(
+ frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0
+ )
+ self.assertTrue(
+ frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date")
+ is not None
+ )
+
def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
try:
- frappe.get_doc({
- "doctype": "Bank",
- "bank_name":bank_name,
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Bank",
+ "bank_name": bank_name,
+ }
+ ).insert()
except frappe.DuplicateEntryError:
pass
try:
- frappe.get_doc({
- "doctype": "Bank Account",
- "account_name":"Checking Account",
- "bank": bank_name,
- "account": account_name
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Bank Account",
+ "account_name": "Checking Account",
+ "bank": bank_name,
+ "account": account_name,
+ }
+ ).insert()
except frappe.DuplicateEntryError:
pass
+
def add_transactions():
create_bank_account()
- doc = frappe.get_doc({
- "doctype": "Bank Transaction",
- "description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
- "date": "2018-10-23",
- "deposit": 1200,
- "currency": "INR",
- "bank_account": "Checking Account - Citi Bank"
- }).insert()
+ doc = frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "description": "1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
+ "date": "2018-10-23",
+ "deposit": 1200,
+ "currency": "INR",
+ "bank_account": "Checking Account - Citi Bank",
+ }
+ ).insert()
doc.submit()
- doc = frappe.get_doc({
- "doctype": "Bank Transaction",
- "description":"1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G",
- "date": "2018-10-23",
- "deposit": 1700,
- "currency": "INR",
- "bank_account": "Checking Account - Citi Bank"
- }).insert()
+ doc = frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "description": "1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G",
+ "date": "2018-10-23",
+ "deposit": 1700,
+ "currency": "INR",
+ "bank_account": "Checking Account - Citi Bank",
+ }
+ ).insert()
doc.submit()
- doc = frappe.get_doc({
- "doctype": "Bank Transaction",
- "description":"Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic",
- "date": "2018-10-26",
- "withdrawal": 690,
- "currency": "INR",
- "bank_account": "Checking Account - Citi Bank"
- }).insert()
+ doc = frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "description": "Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic",
+ "date": "2018-10-26",
+ "withdrawal": 690,
+ "currency": "INR",
+ "bank_account": "Checking Account - Citi Bank",
+ }
+ ).insert()
doc.submit()
- doc = frappe.get_doc({
- "doctype": "Bank Transaction",
- "description":"Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07",
- "date": "2018-10-27",
- "deposit": 3900,
- "currency": "INR",
- "bank_account": "Checking Account - Citi Bank"
- }).insert()
+ doc = frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "description": "Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07",
+ "date": "2018-10-27",
+ "deposit": 3900,
+ "currency": "INR",
+ "bank_account": "Checking Account - Citi Bank",
+ }
+ ).insert()
doc.submit()
- doc = frappe.get_doc({
- "doctype": "Bank Transaction",
- "description":"I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio",
- "date": "2018-10-27",
- "withdrawal": 109080,
- "currency": "INR",
- "bank_account": "Checking Account - Citi Bank"
- }).insert()
+ doc = frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "description": "I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio",
+ "date": "2018-10-27",
+ "withdrawal": 109080,
+ "currency": "INR",
+ "bank_account": "Checking Account - Citi Bank",
+ }
+ ).insert()
doc.submit()
def add_vouchers():
try:
- frappe.get_doc({
- "doctype": "Supplier",
- "supplier_group":"All Supplier Groups",
- "supplier_type": "Company",
- "supplier_name": "Conrad Electronic"
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Supplier",
+ "supplier_group": "All Supplier Groups",
+ "supplier_type": "Company",
+ "supplier_name": "Conrad Electronic",
+ }
+ ).insert()
except frappe.DuplicateEntryError:
pass
@@ -198,12 +267,14 @@ def add_vouchers():
pe.submit()
try:
- frappe.get_doc({
- "doctype": "Supplier",
- "supplier_group":"All Supplier Groups",
- "supplier_type": "Company",
- "supplier_name": "Mr G"
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Supplier",
+ "supplier_group": "All Supplier Groups",
+ "supplier_type": "Company",
+ "supplier_name": "Mr G",
+ }
+ ).insert()
except frappe.DuplicateEntryError:
pass
@@ -222,26 +293,30 @@ def add_vouchers():
pe.submit()
try:
- frappe.get_doc({
- "doctype": "Supplier",
- "supplier_group":"All Supplier Groups",
- "supplier_type": "Company",
- "supplier_name": "Poore Simon's"
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Supplier",
+ "supplier_group": "All Supplier Groups",
+ "supplier_type": "Company",
+ "supplier_name": "Poore Simon's",
+ }
+ ).insert()
except frappe.DuplicateEntryError:
pass
try:
- frappe.get_doc({
- "doctype": "Customer",
- "customer_group":"All Customer Groups",
- "customer_type": "Company",
- "customer_name": "Poore Simon's"
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Customer",
+ "customer_group": "All Customer Groups",
+ "customer_type": "Company",
+ "customer_name": "Poore Simon's",
+ }
+ ).insert()
except frappe.DuplicateEntryError:
pass
- pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save =1)
+ pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1)
pi.cash_bank_account = "_Test Bank - _TC"
pi.insert()
pi.submit()
@@ -261,33 +336,31 @@ def add_vouchers():
pe.submit()
try:
- frappe.get_doc({
- "doctype": "Customer",
- "customer_group":"All Customer Groups",
- "customer_type": "Company",
- "customer_name": "Fayva"
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Customer",
+ "customer_group": "All Customer Groups",
+ "customer_type": "Company",
+ "customer_name": "Fayva",
+ }
+ ).insert()
except frappe.DuplicateEntryError:
pass
- mode_of_payment = frappe.get_doc({
- "doctype": "Mode of Payment",
- "name": "Cash"
- })
+ mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
- if not frappe.db.get_value('Mode of Payment Account', {'company': "_Test Company", 'parent': "Cash"}):
- mode_of_payment.append("accounts", {
- "company": "_Test Company",
- "default_account": "_Test Bank - _TC"
- })
+ if not frappe.db.get_value(
+ "Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}
+ ):
+ mode_of_payment.append(
+ "accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"}
+ )
mode_of_payment.save()
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
si.is_pos = 1
- si.append("payments", {
- "mode_of_payment": "Cash",
- "account": "_Test Bank - _TC",
- "amount": 109080
- })
+ si.append(
+ "payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080}
+ )
si.insert()
si.submit()
diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py
index 492bb365589..5527f9fb99f 100644
--- a/erpnext/accounts/doctype/budget/budget.py
+++ b/erpnext/accounts/doctype/budget/budget.py
@@ -14,13 +14,19 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
from erpnext.accounts.utils import get_fiscal_year
-class BudgetError(frappe.ValidationError): pass
-class DuplicateBudgetError(frappe.ValidationError): pass
+class BudgetError(frappe.ValidationError):
+ pass
+
+
+class DuplicateBudgetError(frappe.ValidationError):
+ pass
+
class Budget(Document):
def autoname(self):
- self.name = make_autoname(self.get(frappe.scrub(self.budget_against))
- + "/" + self.fiscal_year + "/.###")
+ self.name = make_autoname(
+ self.get(frappe.scrub(self.budget_against)) + "/" + self.fiscal_year + "/.###"
+ )
def validate(self):
if not self.get(frappe.scrub(self.budget_against)):
@@ -35,34 +41,44 @@ class Budget(Document):
budget_against = self.get(budget_against_field)
accounts = [d.account for d in self.accounts] or []
- existing_budget = frappe.db.sql("""
+ existing_budget = frappe.db.sql(
+ """
select
b.name, ba.account from `tabBudget` b, `tabBudget Account` ba
where
ba.parent = b.name and b.docstatus < 2 and b.company = %s and %s=%s and
b.fiscal_year=%s and b.name != %s and ba.account in (%s) """
- % ('%s', budget_against_field, '%s', '%s', '%s', ','.join(['%s'] * len(accounts))),
- (self.company, budget_against, self.fiscal_year, self.name) + tuple(accounts), as_dict=1)
+ % ("%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))),
+ (self.company, budget_against, self.fiscal_year, self.name) + tuple(accounts),
+ as_dict=1,
+ )
for d in existing_budget:
- frappe.throw(_("Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}")
- .format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year), DuplicateBudgetError)
+ frappe.throw(
+ _(
+ "Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}"
+ ).format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year),
+ DuplicateBudgetError,
+ )
def validate_accounts(self):
account_list = []
- for d in self.get('accounts'):
+ for d in self.get("accounts"):
if d.account:
- account_details = frappe.db.get_value("Account", d.account,
- ["is_group", "company", "report_type"], as_dict=1)
+ account_details = frappe.db.get_value(
+ "Account", d.account, ["is_group", "company", "report_type"], as_dict=1
+ )
if account_details.is_group:
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account))
elif account_details.company != self.company:
- frappe.throw(_("Account {0} does not belongs to company {1}")
- .format(d.account, self.company))
+ frappe.throw(_("Account {0} does not belongs to company {1}").format(d.account, self.company))
elif account_details.report_type != "Profit and Loss":
- frappe.throw(_("Budget cannot be assigned against {0}, as it's not an Income or Expense account")
- .format(d.account))
+ frappe.throw(
+ _("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format(
+ d.account
+ )
+ )
if d.account in account_list:
frappe.throw(_("Account {0} has been entered multiple times").format(d.account))
@@ -70,51 +86,66 @@ class Budget(Document):
account_list.append(d.account)
def set_null_value(self):
- if self.budget_against == 'Cost Center':
+ if self.budget_against == "Cost Center":
self.project = None
else:
self.cost_center = None
def validate_applicable_for(self):
- if (self.applicable_on_material_request
- and not (self.applicable_on_purchase_order and self.applicable_on_booking_actual_expenses)):
- frappe.throw(_("Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses"))
+ if self.applicable_on_material_request and not (
+ self.applicable_on_purchase_order and self.applicable_on_booking_actual_expenses
+ ):
+ frappe.throw(
+ _("Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses")
+ )
- elif (self.applicable_on_purchase_order
- and not (self.applicable_on_booking_actual_expenses)):
+ elif self.applicable_on_purchase_order and not (self.applicable_on_booking_actual_expenses):
frappe.throw(_("Please enable Applicable on Booking Actual Expenses"))
- elif not(self.applicable_on_material_request
- or self.applicable_on_purchase_order or self.applicable_on_booking_actual_expenses):
+ elif not (
+ self.applicable_on_material_request
+ or self.applicable_on_purchase_order
+ or self.applicable_on_booking_actual_expenses
+ ):
self.applicable_on_booking_actual_expenses = 1
+
def validate_expense_against_budget(args):
args = frappe._dict(args)
- if args.get('company') and not args.fiscal_year:
- args.fiscal_year = get_fiscal_year(args.get('posting_date'), company=args.get('company'))[0]
- frappe.flags.exception_approver_role = frappe.get_cached_value('Company',
- args.get('company'), 'exception_budget_approver_role')
+ if args.get("company") and not args.fiscal_year:
+ args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
+ frappe.flags.exception_approver_role = frappe.get_cached_value(
+ "Company", args.get("company"), "exception_budget_approver_role"
+ )
if not args.account:
args.account = args.get("expense_account")
- if not (args.get('account') and args.get('cost_center')) and args.item_code:
+ if not (args.get("account") and args.get("cost_center")) and args.item_code:
args.cost_center, args.account = get_item_details(args)
if not args.account:
return
- for budget_against in ['project', 'cost_center'] + get_accounting_dimensions():
- if (args.get(budget_against) and args.account
- and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})):
+ for budget_against in ["project", "cost_center"] + get_accounting_dimensions():
+ if (
+ args.get(budget_against)
+ and args.account
+ and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})
+ ):
doctype = frappe.unscrub(budget_against)
- if frappe.get_cached_value('DocType', doctype, 'is_tree'):
+ if frappe.get_cached_value("DocType", doctype, "is_tree"):
lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"])
condition = """and exists(select name from `tab%s`
- where lft<=%s and rgt>=%s and name=b.%s)""" % (doctype, lft, rgt, budget_against) #nosec
+ where lft<=%s and rgt>=%s and name=b.%s)""" % (
+ doctype,
+ lft,
+ rgt,
+ budget_against,
+ ) # nosec
args.is_tree = True
else:
condition = "and b.%s=%s" % (budget_against, frappe.db.escape(args.get(budget_against)))
@@ -123,7 +154,8 @@ def validate_expense_against_budget(args):
args.budget_against_field = budget_against
args.budget_against_doctype = doctype
- budget_records = frappe.db.sql("""
+ budget_records = frappe.db.sql(
+ """
select
b.{budget_against_field} as budget_against, ba.budget_amount, b.monthly_distribution,
ifnull(b.applicable_on_material_request, 0) as for_material_request,
@@ -138,11 +170,17 @@ def validate_expense_against_budget(args):
b.name=ba.parent and b.fiscal_year=%s
and ba.account=%s and b.docstatus=1
{condition}
- """.format(condition=condition, budget_against_field=budget_against), (args.fiscal_year, args.account), as_dict=True) #nosec
+ """.format(
+ condition=condition, budget_against_field=budget_against
+ ),
+ (args.fiscal_year, args.account),
+ as_dict=True,
+ ) # nosec
if budget_records:
validate_budget_records(args, budget_records)
+
def validate_budget_records(args, budget_records):
for budget in budget_records:
if flt(budget.budget_amount):
@@ -150,88 +188,118 @@ def validate_budget_records(args, budget_records):
yearly_action, monthly_action = get_actions(args, budget)
if monthly_action in ["Stop", "Warn"]:
- budget_amount = get_accumulated_monthly_budget(budget.monthly_distribution,
- args.posting_date, args.fiscal_year, budget.budget_amount)
+ budget_amount = get_accumulated_monthly_budget(
+ budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount
+ )
args["month_end_date"] = get_last_day(args.posting_date)
- compare_expense_with_budget(args, budget_amount,
- _("Accumulated Monthly"), monthly_action, budget.budget_against, amount)
+ compare_expense_with_budget(
+ args, budget_amount, _("Accumulated Monthly"), monthly_action, budget.budget_against, amount
+ )
+
+ if (
+ yearly_action in ("Stop", "Warn")
+ and monthly_action != "Stop"
+ and yearly_action != monthly_action
+ ):
+ compare_expense_with_budget(
+ args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount
+ )
- if yearly_action in ("Stop", "Warn") and monthly_action != "Stop" \
- and yearly_action != monthly_action:
- compare_expense_with_budget(args, flt(budget.budget_amount),
- _("Annual"), yearly_action, budget.budget_against, amount)
def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
actual_expense = amount or get_actual_expense(args)
if actual_expense > budget_amount:
diff = actual_expense - budget_amount
- currency = frappe.get_cached_value('Company', args.company, 'default_currency')
+ currency = frappe.get_cached_value("Company", args.company, "default_currency")
msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will exceed by {5}").format(
- _(action_for), frappe.bold(args.account), args.budget_against_field,
- frappe.bold(budget_against),
- frappe.bold(fmt_money(budget_amount, currency=currency)),
- frappe.bold(fmt_money(diff, currency=currency)))
+ _(action_for),
+ frappe.bold(args.account),
+ args.budget_against_field,
+ frappe.bold(budget_against),
+ frappe.bold(fmt_money(budget_amount, currency=currency)),
+ frappe.bold(fmt_money(diff, currency=currency)),
+ )
- if (frappe.flags.exception_approver_role
- and frappe.flags.exception_approver_role in frappe.get_roles(frappe.session.user)):
+ if (
+ frappe.flags.exception_approver_role
+ and frappe.flags.exception_approver_role in frappe.get_roles(frappe.session.user)
+ ):
action = "Warn"
- if action=="Stop":
+ if action == "Stop":
frappe.throw(msg, BudgetError)
else:
- frappe.msgprint(msg, indicator='orange')
+ frappe.msgprint(msg, indicator="orange")
+
def get_actions(args, budget):
yearly_action = budget.action_if_annual_budget_exceeded
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded
- if args.get('doctype') == 'Material Request' and budget.for_material_request:
+ if args.get("doctype") == "Material Request" and budget.for_material_request:
yearly_action = budget.action_if_annual_budget_exceeded_on_mr
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_mr
- elif args.get('doctype') == 'Purchase Order' and budget.for_purchase_order:
+ elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
yearly_action = budget.action_if_annual_budget_exceeded_on_po
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po
return yearly_action, monthly_action
+
def get_amount(args, budget):
amount = 0
- if args.get('doctype') == 'Material Request' and budget.for_material_request:
- amount = (get_requested_amount(args, budget)
- + get_ordered_amount(args, budget) + get_actual_expense(args))
+ if args.get("doctype") == "Material Request" and budget.for_material_request:
+ amount = (
+ get_requested_amount(args, budget) + get_ordered_amount(args, budget) + get_actual_expense(args)
+ )
- elif args.get('doctype') == 'Purchase Order' and budget.for_purchase_order:
+ elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
amount = get_ordered_amount(args, budget) + get_actual_expense(args)
return amount
-def get_requested_amount(args, budget):
- item_code = args.get('item_code')
- condition = get_other_condition(args, budget, 'Material Request')
- data = frappe.db.sql(""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
+def get_requested_amount(args, budget):
+ item_code = args.get("item_code")
+ condition = get_other_condition(args, budget, "Material Request")
+
+ data = frappe.db.sql(
+ """ select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
from `tabMaterial Request Item` child, `tabMaterial Request` parent where parent.name = child.parent and
child.item_code = %s and parent.docstatus = 1 and child.stock_qty > child.ordered_qty and {0} and
- parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(condition), item_code, as_list=1)
+ parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(
+ condition
+ ),
+ item_code,
+ as_list=1,
+ )
return data[0][0] if data else 0
+
def get_ordered_amount(args, budget):
- item_code = args.get('item_code')
- condition = get_other_condition(args, budget, 'Purchase Order')
+ item_code = args.get("item_code")
+ condition = get_other_condition(args, budget, "Purchase Order")
- data = frappe.db.sql(""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
+ data = frappe.db.sql(
+ """ select ifnull(sum(child.amount - child.billed_amt), 0) as amount
from `tabPurchase Order Item` child, `tabPurchase Order` parent where
parent.name = child.parent and child.item_code = %s and parent.docstatus = 1 and child.amount > child.billed_amt
- and parent.status != 'Closed' and {0}""".format(condition), item_code, as_list=1)
+ and parent.status != 'Closed' and {0}""".format(
+ condition
+ ),
+ item_code,
+ as_list=1,
+ )
return data[0][0] if data else 0
+
def get_other_condition(args, budget, for_doc):
condition = "expense_account = '%s'" % (args.expense_account)
budget_against_field = args.get("budget_against_field")
@@ -239,41 +307,51 @@ def get_other_condition(args, budget, for_doc):
if budget_against_field and args.get(budget_against_field):
condition += " and child.%s = '%s'" % (budget_against_field, args.get(budget_against_field))
- if args.get('fiscal_year'):
- date_field = 'schedule_date' if for_doc == 'Material Request' else 'transaction_date'
- start_date, end_date = frappe.db.get_value('Fiscal Year', args.get('fiscal_year'),
- ['year_start_date', 'year_end_date'])
+ if args.get("fiscal_year"):
+ date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
+ start_date, end_date = frappe.db.get_value(
+ "Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
+ )
condition += """ and parent.%s
- between '%s' and '%s' """ %(date_field, start_date, end_date)
+ between '%s' and '%s' """ % (
+ date_field,
+ start_date,
+ end_date,
+ )
return condition
+
def get_actual_expense(args):
if not args.budget_against_doctype:
args.budget_against_doctype = frappe.unscrub(args.budget_against_field)
- budget_against_field = args.get('budget_against_field')
- condition1 = " and gle.posting_date <= %(month_end_date)s" \
- if args.get("month_end_date") else ""
+ budget_against_field = args.get("budget_against_field")
+ condition1 = " and gle.posting_date <= %(month_end_date)s" if args.get("month_end_date") else ""
if args.is_tree:
- lft_rgt = frappe.db.get_value(args.budget_against_doctype,
- args.get(budget_against_field), ["lft", "rgt"], as_dict=1)
+ lft_rgt = frappe.db.get_value(
+ args.budget_against_doctype, args.get(budget_against_field), ["lft", "rgt"], as_dict=1
+ )
args.update(lft_rgt)
condition2 = """and exists(select name from `tab{doctype}`
where lft>=%(lft)s and rgt<=%(rgt)s
- and name=gle.{budget_against_field})""".format(doctype=args.budget_against_doctype, #nosec
- budget_against_field=budget_against_field)
+ and name=gle.{budget_against_field})""".format(
+ doctype=args.budget_against_doctype, budget_against_field=budget_against_field # nosec
+ )
else:
condition2 = """and exists(select name from `tab{doctype}`
where name=gle.{budget_against} and
- gle.{budget_against} = %({budget_against})s)""".format(doctype=args.budget_against_doctype,
- budget_against = budget_against_field)
+ gle.{budget_against} = %({budget_against})s)""".format(
+ doctype=args.budget_against_doctype, budget_against=budget_against_field
+ )
- amount = flt(frappe.db.sql("""
+ amount = flt(
+ frappe.db.sql(
+ """
select sum(gle.debit) - sum(gle.credit)
from `tabGL Entry` gle
where gle.account=%(account)s
@@ -282,46 +360,59 @@ def get_actual_expense(args):
and gle.company=%(company)s
and gle.docstatus=1
{condition2}
- """.format(condition1=condition1, condition2=condition2), (args))[0][0]) #nosec
+ """.format(
+ condition1=condition1, condition2=condition2
+ ),
+ (args),
+ )[0][0]
+ ) # nosec
return amount
+
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
distribution = {}
if monthly_distribution:
- for d in frappe.db.sql("""select mdp.month, mdp.percentage_allocation
+ for d in frappe.db.sql(
+ """select mdp.month, mdp.percentage_allocation
from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md
- where mdp.parent=md.name and md.fiscal_year=%s""", fiscal_year, as_dict=1):
- distribution.setdefault(d.month, d.percentage_allocation)
+ where mdp.parent=md.name and md.fiscal_year=%s""",
+ fiscal_year,
+ as_dict=1,
+ ):
+ distribution.setdefault(d.month, d.percentage_allocation)
dt = frappe.db.get_value("Fiscal Year", fiscal_year, "year_start_date")
accumulated_percentage = 0.0
- while(dt <= getdate(posting_date)):
+ while dt <= getdate(posting_date):
if monthly_distribution:
accumulated_percentage += distribution.get(getdate(dt).strftime("%B"), 0)
else:
- accumulated_percentage += 100.0/12
+ accumulated_percentage += 100.0 / 12
dt = add_months(dt, 1)
return annual_budget * accumulated_percentage / 100
+
def get_item_details(args):
cost_center, expense_account = None, None
- if not args.get('company'):
+ if not args.get("company"):
return cost_center, expense_account
if args.item_code:
- item_defaults = frappe.db.get_value('Item Default',
- {'parent': args.item_code, 'company': args.get('company')},
- ['buying_cost_center', 'expense_account'])
+ item_defaults = frappe.db.get_value(
+ "Item Default",
+ {"parent": args.item_code, "company": args.get("company")},
+ ["buying_cost_center", "expense_account"],
+ )
if item_defaults:
cost_center, expense_account = item_defaults
if not (cost_center and expense_account):
- for doctype in ['Item Group', 'Company']:
+ for doctype in ["Item Group", "Company"]:
data = get_expense_cost_center(doctype, args)
if not cost_center and data:
@@ -335,11 +426,15 @@ def get_item_details(args):
return cost_center, expense_account
+
def get_expense_cost_center(doctype, args):
- if doctype == 'Item Group':
- return frappe.db.get_value('Item Default',
- {'parent': args.get(frappe.scrub(doctype)), 'company': args.get('company')},
- ['buying_cost_center', 'expense_account'])
+ if doctype == "Item Group":
+ return frappe.db.get_value(
+ "Item Default",
+ {"parent": args.get(frappe.scrub(doctype)), "company": args.get("company")},
+ ["buying_cost_center", "expense_account"],
+ )
else:
- return frappe.db.get_value(doctype, args.get(frappe.scrub(doctype)),\
- ['cost_center', 'default_expense_account'])
+ return frappe.db.get_value(
+ doctype, args.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"]
+ )
diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py
index 9a83a0aa9a6..c48c7d97a2a 100644
--- a/erpnext/accounts/doctype/budget/test_budget.py
+++ b/erpnext/accounts/doctype/budget/test_budget.py
@@ -11,7 +11,8 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ
from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
-test_dependencies = ['Monthly Distribution']
+test_dependencies = ["Monthly Distribution"]
+
class TestBudget(unittest.TestCase):
def test_monthly_budget_crossed_ignore(self):
@@ -19,11 +20,18 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center")
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 40000,
+ "_Test Cost Center - _TC",
+ posting_date=nowdate(),
+ submit=True,
+ )
- self.assertTrue(frappe.db.get_value("GL Entry",
- {"voucher_type": "Journal Entry", "voucher_no": jv.name}))
+ self.assertTrue(
+ frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})
+ )
budget.cancel()
jv.cancel()
@@ -33,10 +41,17 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center")
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate())
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 40000,
+ "_Test Cost Center - _TC",
+ posting_date=nowdate(),
+ )
self.assertRaises(BudgetError, jv.submit)
@@ -48,49 +63,65 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center")
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate())
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 40000,
+ "_Test Cost Center - _TC",
+ posting_date=nowdate(),
+ )
self.assertRaises(BudgetError, jv.submit)
- frappe.db.set_value('Company', budget.company, 'exception_budget_approver_role', 'Accounts User')
+ frappe.db.set_value("Company", budget.company, "exception_budget_approver_role", "Accounts User")
jv.submit()
- self.assertEqual(frappe.db.get_value('Journal Entry', jv.name, 'docstatus'), 1)
+ self.assertEqual(frappe.db.get_value("Journal Entry", jv.name, "docstatus"), 1)
jv.cancel()
- frappe.db.set_value('Company', budget.company, 'exception_budget_approver_role', '')
+ frappe.db.set_value("Company", budget.company, "exception_budget_approver_role", "")
budget.load_from_db()
budget.cancel()
def test_monthly_budget_crossed_for_mr(self):
- budget = make_budget(applicable_on_material_request=1,
- applicable_on_purchase_order=1, action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
- budget_against="Cost Center")
+ budget = make_budget(
+ applicable_on_material_request=1,
+ applicable_on_purchase_order=1,
+ action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
+ budget_against="Cost Center",
+ )
fiscal_year = get_fiscal_year(nowdate())[0]
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
- mr = frappe.get_doc({
- "doctype": "Material Request",
- "material_request_type": "Purchase",
- "transaction_date": nowdate(),
- "company": budget.company,
- "items": [{
- 'item_code': '_Test Item',
- 'qty': 1,
- 'uom': "_Test UOM",
- 'warehouse': '_Test Warehouse - _TC',
- 'schedule_date': nowdate(),
- 'rate': 100000,
- 'expense_account': '_Test Account Cost for Goods Sold - _TC',
- 'cost_center': '_Test Cost Center - _TC'
- }]
- })
+ mr = frappe.get_doc(
+ {
+ "doctype": "Material Request",
+ "material_request_type": "Purchase",
+ "transaction_date": nowdate(),
+ "company": budget.company,
+ "items": [
+ {
+ "item_code": "_Test Item",
+ "qty": 1,
+ "uom": "_Test UOM",
+ "warehouse": "_Test Warehouse - _TC",
+ "schedule_date": nowdate(),
+ "rate": 100000,
+ "expense_account": "_Test Account Cost for Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ }
+ ],
+ }
+ )
mr.set_missing_values()
@@ -100,11 +131,16 @@ class TestBudget(unittest.TestCase):
budget.cancel()
def test_monthly_budget_crossed_for_po(self):
- budget = make_budget(applicable_on_purchase_order=1,
- action_if_accumulated_monthly_budget_exceeded_on_po="Stop", budget_against="Cost Center")
+ budget = make_budget(
+ applicable_on_purchase_order=1,
+ action_if_accumulated_monthly_budget_exceeded_on_po="Stop",
+ budget_against="Cost Center",
+ )
fiscal_year = get_fiscal_year(nowdate())[0]
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
po = create_purchase_order(transaction_date=nowdate(), do_not_submit=True)
@@ -122,12 +158,20 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Project")
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
project = frappe.get_value("Project", {"project_name": "_Test Project"})
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project=project, posting_date=nowdate())
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 40000,
+ "_Test Cost Center - _TC",
+ project=project,
+ posting_date=nowdate(),
+ )
self.assertRaises(BudgetError, jv.submit)
@@ -139,8 +183,13 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center")
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 250000, "_Test Cost Center - _TC", posting_date=nowdate())
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 250000,
+ "_Test Cost Center - _TC",
+ posting_date=nowdate(),
+ )
self.assertRaises(BudgetError, jv.submit)
@@ -153,9 +202,14 @@ class TestBudget(unittest.TestCase):
project = frappe.get_value("Project", {"project_name": "_Test Project"})
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 250000, "_Test Cost Center - _TC",
- project=project, posting_date=nowdate())
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 250000,
+ "_Test Cost Center - _TC",
+ project=project,
+ posting_date=nowdate(),
+ )
self.assertRaises(BudgetError, jv.submit)
@@ -169,14 +223,23 @@ class TestBudget(unittest.TestCase):
if month > 9:
month = 9
- for i in range(month+1):
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
+ for i in range(month + 1):
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 20000,
+ "_Test Cost Center - _TC",
+ posting_date=nowdate(),
+ submit=True,
+ )
- self.assertTrue(frappe.db.get_value("GL Entry",
- {"voucher_type": "Journal Entry", "voucher_no": jv.name}))
+ self.assertTrue(
+ frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})
+ )
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
self.assertRaises(BudgetError, jv.cancel)
@@ -193,14 +256,23 @@ class TestBudget(unittest.TestCase):
project = frappe.get_value("Project", {"project_name": "_Test Project"})
for i in range(month + 1):
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True,
- project=project)
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 20000,
+ "_Test Cost Center - _TC",
+ posting_date=nowdate(),
+ submit=True,
+ project=project,
+ )
- self.assertTrue(frappe.db.get_value("GL Entry",
- {"voucher_type": "Journal Entry", "voucher_no": jv.name}))
+ self.assertTrue(
+ frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})
+ )
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
self.assertRaises(BudgetError, jv.cancel)
@@ -212,10 +284,17 @@ class TestBudget(unittest.TestCase):
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC")
budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC")
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 40000, "_Test Cost Center 2 - _TC", posting_date=nowdate())
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 40000,
+ "_Test Cost Center 2 - _TC",
+ posting_date=nowdate(),
+ )
self.assertRaises(BudgetError, jv.submit)
@@ -226,19 +305,28 @@ class TestBudget(unittest.TestCase):
cost_center = "_Test Cost Center 3 - _TC"
if not frappe.db.exists("Cost Center", cost_center):
- frappe.get_doc({
- 'doctype': 'Cost Center',
- 'cost_center_name': '_Test Cost Center 3',
- 'parent_cost_center': "_Test Company - _TC",
- 'company': '_Test Company',
- 'is_group': 0
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Cost Center",
+ "cost_center_name": "_Test Cost Center 3",
+ "parent_cost_center": "_Test Company - _TC",
+ "company": "_Test Company",
+ "is_group": 0,
+ }
+ ).insert(ignore_permissions=True)
budget = make_budget(budget_against="Cost Center", cost_center=cost_center)
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 40000, cost_center, posting_date=nowdate())
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 40000,
+ cost_center,
+ posting_date=nowdate(),
+ )
self.assertRaises(BudgetError, jv.submit)
@@ -255,14 +343,16 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
fiscal_year = get_fiscal_year(nowdate())[0]
- args = frappe._dict({
- "account": "_Test Account Cost for Goods Sold - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "monthly_end_date": posting_date,
- "company": "_Test Company",
- "fiscal_year": fiscal_year,
- "budget_against_field": budget_against_field,
- })
+ args = frappe._dict(
+ {
+ "account": "_Test Account Cost for Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "monthly_end_date": posting_date,
+ "company": "_Test Company",
+ "fiscal_year": fiscal_year,
+ "budget_against_field": budget_against_field,
+ }
+ )
if not args.get(budget_against_field):
args[budget_against_field] = budget_against
@@ -271,26 +361,42 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
if existing_expense:
if budget_against_field == "cost_center":
- make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
+ make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ -existing_expense,
+ "_Test Cost Center - _TC",
+ posting_date=nowdate(),
+ submit=True,
+ )
elif budget_against_field == "project":
- make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project=budget_against, posting_date=nowdate())
+ make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ -existing_expense,
+ "_Test Cost Center - _TC",
+ submit=True,
+ project=budget_against,
+ posting_date=nowdate(),
+ )
+
def make_budget(**args):
args = frappe._dict(args)
- budget_against=args.budget_against
- cost_center=args.cost_center
+ budget_against = args.budget_against
+ cost_center = args.cost_center
fiscal_year = get_fiscal_year(nowdate())[0]
if budget_against == "Project":
project_name = "{0}%".format("_Test Project/" + fiscal_year)
- budget_list = frappe.get_all("Budget", fields=["name"], filters = {"name": ("like", project_name)})
+ budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", project_name)})
else:
cost_center_name = "{0}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year)
- budget_list = frappe.get_all("Budget", fields=["name"], filters = {"name": ("like", cost_center_name)})
+ budget_list = frappe.get_all(
+ "Budget", fields=["name"], filters={"name": ("like", cost_center_name)}
+ )
for d in budget_list:
frappe.db.sql("delete from `tabBudget` where name = %(name)s", d)
frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d)
@@ -300,7 +406,7 @@ def make_budget(**args):
if budget_against == "Project":
budget.project = frappe.get_value("Project", {"project_name": "_Test Project"})
else:
- budget.cost_center =cost_center or "_Test Cost Center - _TC"
+ budget.cost_center = cost_center or "_Test Cost Center - _TC"
monthly_distribution = frappe.get_doc("Monthly Distribution", "_Test Distribution")
monthly_distribution.fiscal_year = fiscal_year
@@ -312,20 +418,27 @@ def make_budget(**args):
budget.action_if_annual_budget_exceeded = "Stop"
budget.action_if_accumulated_monthly_budget_exceeded = "Ignore"
budget.budget_against = budget_against
- budget.append("accounts", {
- "account": "_Test Account Cost for Goods Sold - _TC",
- "budget_amount": 200000
- })
+ budget.append(
+ "accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000}
+ )
if args.applicable_on_material_request:
budget.applicable_on_material_request = 1
- budget.action_if_annual_budget_exceeded_on_mr = args.action_if_annual_budget_exceeded_on_mr or 'Warn'
- budget.action_if_accumulated_monthly_budget_exceeded_on_mr = args.action_if_accumulated_monthly_budget_exceeded_on_mr or 'Warn'
+ budget.action_if_annual_budget_exceeded_on_mr = (
+ args.action_if_annual_budget_exceeded_on_mr or "Warn"
+ )
+ budget.action_if_accumulated_monthly_budget_exceeded_on_mr = (
+ args.action_if_accumulated_monthly_budget_exceeded_on_mr or "Warn"
+ )
if args.applicable_on_purchase_order:
budget.applicable_on_purchase_order = 1
- budget.action_if_annual_budget_exceeded_on_po = args.action_if_annual_budget_exceeded_on_po or 'Warn'
- budget.action_if_accumulated_monthly_budget_exceeded_on_po = args.action_if_accumulated_monthly_budget_exceeded_on_po or 'Warn'
+ budget.action_if_annual_budget_exceeded_on_po = (
+ args.action_if_annual_budget_exceeded_on_po or "Warn"
+ )
+ budget.action_if_accumulated_monthly_budget_exceeded_on_po = (
+ args.action_if_accumulated_monthly_budget_exceeded_on_po or "Warn"
+ )
budget.insert()
budget.submit()
diff --git a/erpnext/accounts/doctype/c_form/c_form.py b/erpnext/accounts/doctype/c_form/c_form.py
index 61331d32d8e..0de75c78697 100644
--- a/erpnext/accounts/doctype/c_form/c_form.py
+++ b/erpnext/accounts/doctype/c_form/c_form.py
@@ -11,28 +11,42 @@ from frappe.utils import flt
class CForm(Document):
def validate(self):
"""Validate invoice that c-form is applicable
- and no other c-form is received for that"""
+ and no other c-form is received for that"""
- for d in self.get('invoices'):
+ for d in self.get("invoices"):
if d.invoice_no:
- inv = frappe.db.sql("""select c_form_applicable, c_form_no from
- `tabSales Invoice` where name = %s and docstatus = 1""", d.invoice_no)
+ inv = frappe.db.sql(
+ """select c_form_applicable, c_form_no from
+ `tabSales Invoice` where name = %s and docstatus = 1""",
+ d.invoice_no,
+ )
- if inv and inv[0][0] != 'Yes':
+ if inv and inv[0][0] != "Yes":
frappe.throw(_("C-form is not applicable for Invoice: {0}").format(d.invoice_no))
elif inv and inv[0][1] and inv[0][1] != self.name:
- frappe.throw(_("""Invoice {0} is tagged in another C-form: {1}.
+ frappe.throw(
+ _(
+ """Invoice {0} is tagged in another C-form: {1}.
If you want to change C-form no for this invoice,
- please remove invoice no from the previous c-form and then try again"""\
- .format(d.invoice_no, inv[0][1])))
+ please remove invoice no from the previous c-form and then try again""".format(
+ d.invoice_no, inv[0][1]
+ )
+ )
+ )
elif not inv:
- frappe.throw(_("Row {0}: Invoice {1} is invalid, it might be cancelled / does not exist. \
- Please enter a valid Invoice".format(d.idx, d.invoice_no)))
+ frappe.throw(
+ _(
+ "Row {0}: Invoice {1} is invalid, it might be cancelled / does not exist. \
+ Please enter a valid Invoice".format(
+ d.idx, d.invoice_no
+ )
+ )
+ )
def on_update(self):
- """ Update C-Form No on invoices"""
+ """Update C-Form No on invoices"""
self.set_total_invoiced_amount()
def on_submit(self):
@@ -43,30 +57,40 @@ class CForm(Document):
frappe.db.sql("""update `tabSales Invoice` set c_form_no=null where c_form_no=%s""", self.name)
def set_cform_in_sales_invoices(self):
- inv = [d.invoice_no for d in self.get('invoices')]
+ inv = [d.invoice_no for d in self.get("invoices")]
if inv:
- frappe.db.sql("""update `tabSales Invoice` set c_form_no=%s, modified=%s where name in (%s)""" %
- ('%s', '%s', ', '.join(['%s'] * len(inv))), tuple([self.name, self.modified] + inv))
+ frappe.db.sql(
+ """update `tabSales Invoice` set c_form_no=%s, modified=%s where name in (%s)"""
+ % ("%s", "%s", ", ".join(["%s"] * len(inv))),
+ tuple([self.name, self.modified] + inv),
+ )
- frappe.db.sql("""update `tabSales Invoice` set c_form_no = null, modified = %s
- where name not in (%s) and ifnull(c_form_no, '') = %s""" %
- ('%s', ', '.join(['%s']*len(inv)), '%s'), tuple([self.modified] + inv + [self.name]))
+ frappe.db.sql(
+ """update `tabSales Invoice` set c_form_no = null, modified = %s
+ where name not in (%s) and ifnull(c_form_no, '') = %s"""
+ % ("%s", ", ".join(["%s"] * len(inv)), "%s"),
+ tuple([self.modified] + inv + [self.name]),
+ )
else:
frappe.throw(_("Please enter atleast 1 invoice in the table"))
def set_total_invoiced_amount(self):
- total = sum(flt(d.grand_total) for d in self.get('invoices'))
- frappe.db.set(self, 'total_invoiced_amount', total)
+ total = sum(flt(d.grand_total) for d in self.get("invoices"))
+ frappe.db.set(self, "total_invoiced_amount", total)
@frappe.whitelist()
def get_invoice_details(self, invoice_no):
- """ Pull details from invoices for referrence """
+ """Pull details from invoices for referrence"""
if invoice_no:
- inv = frappe.db.get_value("Sales Invoice", invoice_no,
- ["posting_date", "territory", "base_net_total", "base_grand_total"], as_dict=True)
+ inv = frappe.db.get_value(
+ "Sales Invoice",
+ invoice_no,
+ ["posting_date", "territory", "base_net_total", "base_grand_total"],
+ as_dict=True,
+ )
return {
- 'invoice_date' : inv.posting_date,
- 'territory' : inv.territory,
- 'net_total' : inv.base_net_total,
- 'grand_total' : inv.base_grand_total
+ "invoice_date": inv.posting_date,
+ "territory": inv.territory,
+ "net_total": inv.base_net_total,
+ "grand_total": inv.base_grand_total,
}
diff --git a/erpnext/accounts/doctype/c_form/test_c_form.py b/erpnext/accounts/doctype/c_form/test_c_form.py
index fa34c255c66..87ad60fddac 100644
--- a/erpnext/accounts/doctype/c_form/test_c_form.py
+++ b/erpnext/accounts/doctype/c_form/test_c_form.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('C-Form')
+
class TestCForm(unittest.TestCase):
pass
diff --git a/erpnext/accounts/doctype/cash_flow_mapper/default_cash_flow_mapper.py b/erpnext/accounts/doctype/cash_flow_mapper/default_cash_flow_mapper.py
index 4465ec6024b..79feb2dae23 100644
--- a/erpnext/accounts/doctype/cash_flow_mapper/default_cash_flow_mapper.py
+++ b/erpnext/accounts/doctype/cash_flow_mapper/default_cash_flow_mapper.py
@@ -1,26 +1,25 @@
-
DEFAULT_MAPPERS = [
- {
- 'doctype': 'Cash Flow Mapper',
- 'section_footer': 'Net cash generated by operating activities',
- 'section_header': 'Cash flows from operating activities',
- 'section_leader': 'Adjustments for',
- 'section_name': 'Operating Activities',
- 'position': 0,
- 'section_subtotal': 'Cash generated from operations',
- },
- {
- 'doctype': 'Cash Flow Mapper',
- 'position': 1,
- 'section_footer': 'Net cash used in investing activities',
- 'section_header': 'Cash flows from investing activities',
- 'section_name': 'Investing Activities'
- },
- {
- 'doctype': 'Cash Flow Mapper',
- 'position': 2,
- 'section_footer': 'Net cash used in financing activites',
- 'section_header': 'Cash flows from financing activities',
- 'section_name': 'Financing Activities',
- }
+ {
+ "doctype": "Cash Flow Mapper",
+ "section_footer": "Net cash generated by operating activities",
+ "section_header": "Cash flows from operating activities",
+ "section_leader": "Adjustments for",
+ "section_name": "Operating Activities",
+ "position": 0,
+ "section_subtotal": "Cash generated from operations",
+ },
+ {
+ "doctype": "Cash Flow Mapper",
+ "position": 1,
+ "section_footer": "Net cash used in investing activities",
+ "section_header": "Cash flows from investing activities",
+ "section_name": "Investing Activities",
+ },
+ {
+ "doctype": "Cash Flow Mapper",
+ "position": 2,
+ "section_footer": "Net cash used in financing activites",
+ "section_header": "Cash flows from financing activities",
+ "section_name": "Financing Activities",
+ },
]
diff --git a/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py b/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py
index cd8381a4bd3..3bce4d51c7a 100644
--- a/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py
+++ b/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py
@@ -11,9 +11,11 @@ class CashFlowMapping(Document):
self.validate_checked_options()
def validate_checked_options(self):
- checked_fields = [d for d in self.meta.fields if d.fieldtype == 'Check' and self.get(d.fieldname) == 1]
+ checked_fields = [
+ d for d in self.meta.fields if d.fieldtype == "Check" and self.get(d.fieldname) == 1
+ ]
if len(checked_fields) > 1:
frappe.throw(
- frappe._('You can only select a maximum of one option from the list of check boxes.'),
- title='Error'
+ frappe._("You can only select a maximum of one option from the list of check boxes."),
+ title="Error",
)
diff --git a/erpnext/accounts/doctype/cash_flow_mapping/test_cash_flow_mapping.py b/erpnext/accounts/doctype/cash_flow_mapping/test_cash_flow_mapping.py
index abb25670467..19f2425b4ce 100644
--- a/erpnext/accounts/doctype/cash_flow_mapping/test_cash_flow_mapping.py
+++ b/erpnext/accounts/doctype/cash_flow_mapping/test_cash_flow_mapping.py
@@ -9,19 +9,16 @@ import frappe
class TestCashFlowMapping(unittest.TestCase):
def setUp(self):
if frappe.db.exists("Cash Flow Mapping", "Test Mapping"):
- frappe.delete_doc('Cash Flow Mappping', 'Test Mapping')
+ frappe.delete_doc("Cash Flow Mappping", "Test Mapping")
def tearDown(self):
- frappe.delete_doc('Cash Flow Mapping', 'Test Mapping')
+ frappe.delete_doc("Cash Flow Mapping", "Test Mapping")
def test_multiple_selections_not_allowed(self):
- doc = frappe.new_doc('Cash Flow Mapping')
- doc.mapping_name = 'Test Mapping'
- doc.label = 'Test label'
- doc.append(
- 'accounts',
- {'account': 'Accounts Receivable - _TC'}
- )
+ doc = frappe.new_doc("Cash Flow Mapping")
+ doc.mapping_name = "Test Mapping"
+ doc.label = "Test label"
+ doc.append("accounts", {"account": "Accounts Receivable - _TC"})
doc.is_working_capital = 1
doc.is_finance_cost = 1
diff --git a/erpnext/accounts/doctype/cashier_closing/cashier_closing.py b/erpnext/accounts/doctype/cashier_closing/cashier_closing.py
index 9fbd0c97c1e..60138077286 100644
--- a/erpnext/accounts/doctype/cashier_closing/cashier_closing.py
+++ b/erpnext/accounts/doctype/cashier_closing/cashier_closing.py
@@ -17,11 +17,14 @@ class CashierClosing(Document):
self.make_calculations()
def get_outstanding(self):
- values = frappe.db.sql("""
+ values = frappe.db.sql(
+ """
select sum(outstanding_amount)
from `tabSales Invoice`
where posting_date=%s and posting_time>=%s and posting_time<=%s and owner=%s
- """, (self.date, self.from_time, self.time, self.user))
+ """,
+ (self.date, self.from_time, self.time, self.user),
+ )
self.outstanding_amount = flt(values[0][0] if values else 0)
def make_calculations(self):
@@ -29,7 +32,9 @@ class CashierClosing(Document):
for i in self.payments:
total += flt(i.amount)
- self.net_amount = total + self.outstanding_amount + flt(self.expense) - flt(self.custody) + flt(self.returns)
+ self.net_amount = (
+ total + self.outstanding_amount + flt(self.expense) - flt(self.custody) + flt(self.returns)
+ )
def validate_time(self):
if self.from_time >= self.time:
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index aaacce4eb9d..01bf1c23e92 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -25,33 +25,41 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
class ChartofAccountsImporter(Document):
def validate(self):
if self.import_file:
- get_coa('Chart of Accounts Importer', 'All Accounts', file_name=self.import_file, for_validate=1)
+ get_coa(
+ "Chart of Accounts Importer", "All Accounts", file_name=self.import_file, for_validate=1
+ )
+
def validate_columns(data):
if not data:
- frappe.throw(_('No data found. Seems like you uploaded a blank file'))
+ frappe.throw(_("No data found. Seems like you uploaded a blank file"))
no_of_columns = max([len(d) for d in data])
if no_of_columns > 7:
- frappe.throw(_('More columns found than expected. Please compare the uploaded file with standard template'),
- title=(_("Wrong Template")))
+ frappe.throw(
+ _("More columns found than expected. Please compare the uploaded file with standard template"),
+ title=(_("Wrong Template")),
+ )
+
@frappe.whitelist()
def validate_company(company):
- parent_company, allow_account_creation_against_child_company = frappe.db.get_value('Company',
- {'name': company}, ['parent_company',
- 'allow_account_creation_against_child_company'])
+ parent_company, allow_account_creation_against_child_company = frappe.db.get_value(
+ "Company", {"name": company}, ["parent_company", "allow_account_creation_against_child_company"]
+ )
if parent_company and (not allow_account_creation_against_child_company):
msg = _("{} is a child company.").format(frappe.bold(company)) + " "
msg += _("Please import accounts against parent company or enable {} in company master.").format(
- frappe.bold('Allow Account Creation Against Child Company'))
- frappe.throw(msg, title=_('Wrong Company'))
+ frappe.bold("Allow Account Creation Against Child Company")
+ )
+ frappe.throw(msg, title=_("Wrong Company"))
- if frappe.db.get_all('GL Entry', {"company": company}, "name", limit=1):
+ if frappe.db.get_all("GL Entry", {"company": company}, "name", limit=1):
return False
+
@frappe.whitelist()
def import_coa(file_name, company):
# delete existing data for accounts
@@ -60,7 +68,7 @@ def import_coa(file_name, company):
# create accounts
file_doc, extension = get_file(file_name)
- if extension == 'csv':
+ if extension == "csv":
data = generate_data_from_csv(file_doc)
else:
data = generate_data_from_excel(file_doc, extension)
@@ -72,27 +80,33 @@ def import_coa(file_name, company):
# trigger on_update for company to reset default accounts
set_default_accounts(company)
+
def get_file(file_name):
file_doc = frappe.get_doc("File", {"file_url": file_name})
parts = file_doc.get_extension()
extension = parts[1]
extension = extension.lstrip(".")
- if extension not in ('csv', 'xlsx', 'xls'):
- frappe.throw(_("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload"))
+ if extension not in ("csv", "xlsx", "xls"):
+ frappe.throw(
+ _(
+ "Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload"
+ )
+ )
+
+ return file_doc, extension
- return file_doc, extension
def generate_data_from_csv(file_doc, as_dict=False):
- ''' read csv file and return the generated nested tree '''
+ """read csv file and return the generated nested tree"""
file_path = file_doc.get_full_path()
data = []
- with open(file_path, 'r') as in_file:
+ with open(file_path, "r") as in_file:
csv_reader = list(csv.reader(in_file))
headers = csv_reader[0]
- del csv_reader[0] # delete top row and headers row
+ del csv_reader[0] # delete top row and headers row
for row in csv_reader:
if as_dict:
@@ -106,6 +120,7 @@ def generate_data_from_csv(file_doc, as_dict=False):
# convert csv data
return data
+
def generate_data_from_excel(file_doc, extension, as_dict=False):
content = file_doc.get_content()
@@ -123,20 +138,21 @@ def generate_data_from_excel(file_doc, extension, as_dict=False):
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
else:
if not row[1]:
- row[1] = row[0]
- row[3] = row[2]
+ row[1] = row[0]
+ row[3] = row[2]
data.append(row)
return data
+
@frappe.whitelist()
def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
- ''' called by tree view (to fetch node's children) '''
+ """called by tree view (to fetch node's children)"""
file_doc, extension = get_file(file_name)
- parent = None if parent==_('All Accounts') else parent
+ parent = None if parent == _("All Accounts") else parent
- if extension == 'csv':
+ if extension == "csv":
data = generate_data_from_csv(file_doc)
else:
data = generate_data_from_excel(file_doc, extension)
@@ -146,32 +162,33 @@ def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
if not for_validate:
forest = build_forest(data)
- accounts = build_tree_from_json("", chart_data=forest, from_coa_importer=True) # returns a list of dict in a tree render-able form
+ accounts = build_tree_from_json(
+ "", chart_data=forest, from_coa_importer=True
+ ) # returns a list of dict in a tree render-able form
# filter out to show data for the selected node only
- accounts = [d for d in accounts if d['parent_account']==parent]
+ accounts = [d for d in accounts if d["parent_account"] == parent]
return accounts
else:
- return {
- 'show_import_button': 1
- }
+ return {"show_import_button": 1}
+
def build_forest(data):
- '''
- converts list of list into a nested tree
- if a = [[1,1], [1,2], [3,2], [4,4], [5,4]]
- tree = {
- 1: {
- 2: {
- 3: {}
- }
- },
- 4: {
- 5: {}
- }
- }
- '''
+ """
+ converts list of list into a nested tree
+ if a = [[1,1], [1,2], [3,2], [4,4], [5,4]]
+ tree = {
+ 1: {
+ 2: {
+ 3: {}
+ }
+ },
+ 4: {
+ 5: {}
+ }
+ }
+ """
# set the value of nested dictionary
def set_nested(d, path, value):
@@ -195,8 +212,11 @@ def build_forest(data):
elif account_name == child:
parent_account_list = return_parent(data, parent_account)
if not parent_account_list and parent_account:
- frappe.throw(_("The parent account {0} does not exists in the uploaded template").format(
- frappe.bold(parent_account)))
+ frappe.throw(
+ _("The parent account {0} does not exists in the uploaded template").format(
+ frappe.bold(parent_account)
+ )
+ )
return [child] + parent_account_list
charts_map, paths = {}, []
@@ -205,7 +225,15 @@ def build_forest(data):
error_messages = []
for i in data:
- account_name, parent_account, account_number, parent_account_number, is_group, account_type, root_type = i
+ (
+ account_name,
+ parent_account,
+ account_number,
+ parent_account_number,
+ is_group,
+ account_type,
+ root_type,
+ ) = i
if not account_name:
error_messages.append("Row {0}: Please enter Account Name".format(line_no))
@@ -216,13 +244,17 @@ def build_forest(data):
account_name = "{} - {}".format(account_number, account_name)
charts_map[account_name] = {}
- charts_map[account_name]['account_name'] = name
- if account_number: charts_map[account_name]["account_number"] = account_number
- if cint(is_group) == 1: charts_map[account_name]["is_group"] = is_group
- if account_type: charts_map[account_name]["account_type"] = account_type
- if root_type: charts_map[account_name]["root_type"] = root_type
+ charts_map[account_name]["account_name"] = name
+ if account_number:
+ charts_map[account_name]["account_number"] = account_number
+ if cint(is_group) == 1:
+ charts_map[account_name]["is_group"] = is_group
+ if account_type:
+ charts_map[account_name]["account_type"] = account_type
+ if root_type:
+ charts_map[account_name]["root_type"] = root_type
path = return_parent(data, account_name)[::-1]
- paths.append(path) # List of path is created
+ paths.append(path) # List of path is created
line_no += 1
if error_messages:
@@ -231,27 +263,32 @@ def build_forest(data):
out = {}
for path in paths:
for n, account_name in enumerate(path):
- set_nested(out, path[:n+1], charts_map[account_name]) # setting the value of nested dictionary.
+ set_nested(
+ out, path[: n + 1], charts_map[account_name]
+ ) # setting the value of nested dictionary.
return out
+
def build_response_as_excel(writer):
filename = frappe.generate_hash("", 10)
- with open(filename, 'wb') as f:
- f.write(cstr(writer.getvalue()).encode('utf-8'))
+ with open(filename, "wb") as f:
+ f.write(cstr(writer.getvalue()).encode("utf-8"))
f = open(filename)
reader = csv.reader(f)
from frappe.utils.xlsxutils import make_xlsx
+
xlsx_file = make_xlsx(reader, "Chart of Accounts Importer Template")
f.close()
os.remove(filename)
# write out response as a xlsx type
- frappe.response['filename'] = 'coa_importer_template.xlsx'
- frappe.response['filecontent'] = xlsx_file.getvalue()
- frappe.response['type'] = 'binary'
+ frappe.response["filename"] = "coa_importer_template.xlsx"
+ frappe.response["filecontent"] = xlsx_file.getvalue()
+ frappe.response["type"] = "binary"
+
@frappe.whitelist()
def download_template(file_type, template_type):
@@ -259,34 +296,46 @@ def download_template(file_type, template_type):
writer = get_template(template_type)
- if file_type == 'CSV':
+ if file_type == "CSV":
# download csv file
- frappe.response['result'] = cstr(writer.getvalue())
- frappe.response['type'] = 'csv'
- frappe.response['doctype'] = 'Chart of Accounts Importer'
+ frappe.response["result"] = cstr(writer.getvalue())
+ frappe.response["type"] = "csv"
+ frappe.response["doctype"] = "Chart of Accounts Importer"
else:
build_response_as_excel(writer)
+
def get_template(template_type):
- fields = ["Account Name", "Parent Account", "Account Number", "Parent Account Number", "Is Group", "Account Type", "Root Type"]
+ fields = [
+ "Account Name",
+ "Parent Account",
+ "Account Number",
+ "Parent Account Number",
+ "Is Group",
+ "Account Type",
+ "Root Type",
+ ]
writer = UnicodeWriter()
writer.writerow(fields)
- if template_type == 'Blank Template':
- for root_type in get_root_types():
- writer.writerow(['', '', '', 1, '', root_type])
+ if template_type == "Blank Template":
+ for root_type in get_root_types():
+ writer.writerow(["", "", "", 1, "", root_type])
for account in get_mandatory_group_accounts():
- writer.writerow(['', '', '', 1, account, "Asset"])
+ writer.writerow(["", "", "", 1, account, "Asset"])
for account_type in get_mandatory_account_types():
- writer.writerow(['', '', '', 0, account_type.get('account_type'), account_type.get('root_type')])
+ writer.writerow(
+ ["", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
+ )
else:
writer = get_sample_template(writer)
return writer
+
def get_sample_template(writer):
template = [
["Application Of Funds(Assets)", "", "", "", 1, "", "Asset"],
@@ -316,7 +365,7 @@ def get_sample_template(writer):
@frappe.whitelist()
def validate_accounts(file_doc, extension):
- if extension == 'csv':
+ if extension == "csv":
accounts = generate_data_from_csv(file_doc, as_dict=True)
else:
accounts = generate_data_from_excel(file_doc, extension, as_dict=True)
@@ -325,7 +374,9 @@ def validate_accounts(file_doc, extension):
for account in accounts:
accounts_dict.setdefault(account["account_name"], account)
if "parent_account" not in account:
- msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.")
+ msg = _(
+ "Please make sure the file you are using has 'Parent Account' column present in the header."
+ )
msg += "
"
msg += _("Alternatively, you can download the template and fill your data in.")
frappe.throw(msg, title=_("Parent Account Missing"))
@@ -336,77 +387,106 @@ def validate_accounts(file_doc, extension):
return [True, len(accounts)]
+
def validate_root(accounts):
- roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
+ roots = [accounts[d] for d in accounts if not accounts[d].get("parent_account")]
error_messages = []
for account in roots:
if not account.get("root_type") and account.get("account_name"):
- error_messages.append(_("Please enter Root Type for account- {0}").format(account.get("account_name")))
+ error_messages.append(
+ _("Please enter Root Type for account- {0}").format(account.get("account_name"))
+ )
elif account.get("root_type") not in get_root_types() and account.get("account_name"):
- error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name")))
+ error_messages.append(
+ _("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(
+ account.get("account_name")
+ )
+ )
validate_missing_roots(roots)
if error_messages:
frappe.throw(" ".join(error_messages))
+
def validate_missing_roots(roots):
- root_types_added = set(d.get('root_type') for d in roots)
+ root_types_added = set(d.get("root_type") for d in roots)
missing = list(set(get_root_types()) - root_types_added)
if missing:
- frappe.throw(_("Please add Root Account for - {0}").format(' , '.join(missing)))
+ frappe.throw(_("Please add Root Account for - {0}").format(" , ".join(missing)))
+
def get_root_types():
- return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
+ return ("Asset", "Liability", "Expense", "Income", "Equity")
+
def get_report_type(root_type):
- if root_type in ('Asset', 'Liability', 'Equity'):
- return 'Balance Sheet'
+ if root_type in ("Asset", "Liability", "Equity"):
+ return "Balance Sheet"
else:
- return 'Profit and Loss'
+ return "Profit and Loss"
+
def get_mandatory_group_accounts():
- return ('Bank', 'Cash', 'Stock')
+ return ("Bank", "Cash", "Stock")
+
def get_mandatory_account_types():
return [
- {'account_type': 'Cost of Goods Sold', 'root_type': 'Expense'},
- {'account_type': 'Depreciation', 'root_type': 'Expense'},
- {'account_type': 'Fixed Asset', 'root_type': 'Asset'},
- {'account_type': 'Payable', 'root_type': 'Liability'},
- {'account_type': 'Receivable', 'root_type': 'Asset'},
- {'account_type': 'Stock Adjustment', 'root_type': 'Expense'},
- {'account_type': 'Bank', 'root_type': 'Asset'},
- {'account_type': 'Cash', 'root_type': 'Asset'},
- {'account_type': 'Stock', 'root_type': 'Asset'}
+ {"account_type": "Cost of Goods Sold", "root_type": "Expense"},
+ {"account_type": "Depreciation", "root_type": "Expense"},
+ {"account_type": "Fixed Asset", "root_type": "Asset"},
+ {"account_type": "Payable", "root_type": "Liability"},
+ {"account_type": "Receivable", "root_type": "Asset"},
+ {"account_type": "Stock Adjustment", "root_type": "Expense"},
+ {"account_type": "Bank", "root_type": "Asset"},
+ {"account_type": "Cash", "root_type": "Asset"},
+ {"account_type": "Stock", "root_type": "Asset"},
]
+
def unset_existing_data(company):
- linked = frappe.db.sql('''select fieldname from tabDocField
- where fieldtype="Link" and options="Account" and parent="Company"''', as_dict=True)
+ linked = frappe.db.sql(
+ '''select fieldname from tabDocField
+ where fieldtype="Link" and options="Account" and parent="Company"''',
+ as_dict=True,
+ )
# remove accounts data from company
- update_values = {d.fieldname: '' for d in linked}
- frappe.db.set_value('Company', company, update_values, update_values)
+ update_values = {d.fieldname: "" for d in linked}
+ frappe.db.set_value("Company", company, update_values, update_values)
# remove accounts data from various doctypes
- for doctype in ["Account", "Party Account", "Mode of Payment Account", "Tax Withholding Account",
- "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]:
- frappe.db.sql('''delete from `tab{0}` where `company`="%s"''' # nosec
- .format(doctype) % (company))
+ for doctype in [
+ "Account",
+ "Party Account",
+ "Mode of Payment Account",
+ "Tax Withholding Account",
+ "Sales Taxes and Charges Template",
+ "Purchase Taxes and Charges Template",
+ ]:
+ frappe.db.sql(
+ '''delete from `tab{0}` where `company`="%s"'''.format(doctype) % (company) # nosec
+ )
+
def set_default_accounts(company):
from erpnext.setup.doctype.company.company import install_country_fixtures
- company = frappe.get_doc('Company', company)
- company.update({
- "default_receivable_account": frappe.db.get_value("Account",
- {"company": company.name, "account_type": "Receivable", "is_group": 0}),
- "default_payable_account": frappe.db.get_value("Account",
- {"company": company.name, "account_type": "Payable", "is_group": 0})
- })
+
+ company = frappe.get_doc("Company", company)
+ company.update(
+ {
+ "default_receivable_account": frappe.db.get_value(
+ "Account", {"company": company.name, "account_type": "Receivable", "is_group": 0}
+ ),
+ "default_payable_account": frappe.db.get_value(
+ "Account", {"company": company.name, "account_type": "Payable", "is_group": 0}
+ ),
+ }
+ )
company.save()
install_country_fixtures(company.name, company.country)
diff --git a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py
index 20cb42c109c..f8ac66444b4 100644
--- a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py
+++ b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py
@@ -10,17 +10,20 @@ from frappe.model.document import Document
class ChequePrintTemplate(Document):
pass
+
@frappe.whitelist()
def create_or_update_cheque_print_format(template_name):
if not frappe.db.exists("Print Format", template_name):
cheque_print = frappe.new_doc("Print Format")
- cheque_print.update({
- "doc_type": "Payment Entry",
- "standard": "No",
- "custom_format": 1,
- "print_format_type": "Jinja",
- "name": template_name
- })
+ cheque_print.update(
+ {
+ "doc_type": "Payment Entry",
+ "standard": "No",
+ "custom_format": 1,
+ "print_format_type": "Jinja",
+ "name": template_name,
+ }
+ )
else:
cheque_print = frappe.get_doc("Print Format", template_name)
@@ -69,10 +72,12 @@ def create_or_update_cheque_print_format(template_name):
{{doc.company}}
-"""%{
- "starting_position_from_top_edge": doc.starting_position_from_top_edge \
- if doc.cheque_size == "A4" else 0.0,
- "cheque_width": doc.cheque_width, "cheque_height": doc.cheque_height,
+""" % {
+ "starting_position_from_top_edge": doc.starting_position_from_top_edge
+ if doc.cheque_size == "A4"
+ else 0.0,
+ "cheque_width": doc.cheque_width,
+ "cheque_height": doc.cheque_height,
"acc_pay_dist_from_top_edge": doc.acc_pay_dist_from_top_edge,
"acc_pay_dist_from_left_edge": doc.acc_pay_dist_from_left_edge,
"message_to_show": doc.message_to_show if doc.message_to_show else _("Account Pay Only"),
@@ -89,7 +94,7 @@ def create_or_update_cheque_print_format(template_name):
"amt_in_figures_from_top_edge": doc.amt_in_figures_from_top_edge,
"amt_in_figures_from_left_edge": doc.amt_in_figures_from_left_edge,
"signatory_from_top_edge": doc.signatory_from_top_edge,
- "signatory_from_left_edge": doc.signatory_from_left_edge
+ "signatory_from_left_edge": doc.signatory_from_left_edge,
}
cheque_print.save(ignore_permissions=True)
diff --git a/erpnext/accounts/doctype/cheque_print_template/test_cheque_print_template.py b/erpnext/accounts/doctype/cheque_print_template/test_cheque_print_template.py
index 2b323a9bf62..9b003ceaa3e 100644
--- a/erpnext/accounts/doctype/cheque_print_template/test_cheque_print_template.py
+++ b/erpnext/accounts/doctype/cheque_print_template/test_cheque_print_template.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Cheque Print Template')
+
class TestChequePrintTemplate(unittest.TestCase):
pass
diff --git a/erpnext/accounts/doctype/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py
index 7ae0a72e3d1..2d18a2409a2 100644
--- a/erpnext/accounts/doctype/cost_center/cost_center.py
+++ b/erpnext/accounts/doctype/cost_center/cost_center.py
@@ -11,11 +11,14 @@ from erpnext.accounts.utils import validate_field_number
class CostCenter(NestedSet):
- nsm_parent_field = 'parent_cost_center'
+ nsm_parent_field = "parent_cost_center"
def autoname(self):
from erpnext.accounts.utils import get_autoname_with_number
- self.name = get_autoname_with_number(self.cost_center_number, self.cost_center_name, None, self.company)
+
+ self.name = get_autoname_with_number(
+ self.cost_center_number, self.cost_center_name, None, self.company
+ )
def validate(self):
self.validate_mandatory()
@@ -27,15 +30,31 @@ class CostCenter(NestedSet):
if not self.distributed_cost_center:
frappe.throw(_("Please enter distributed cost center"))
if sum(x.percentage_allocation for x in self.distributed_cost_center) != 100:
- frappe.throw(_("Total percentage allocation for distributed cost center should be equal to 100"))
- if not self.get('__islocal'):
- if not cint(frappe.get_cached_value("Cost Center", {"name": self.name}, "enable_distributed_cost_center")) \
- and self.check_if_part_of_distributed_cost_center():
- frappe.throw(_("Cannot enable Distributed Cost Center for a Cost Center already allocated in another Distributed Cost Center"))
+ frappe.throw(
+ _("Total percentage allocation for distributed cost center should be equal to 100")
+ )
+ if not self.get("__islocal"):
+ if (
+ not cint(
+ frappe.get_cached_value("Cost Center", {"name": self.name}, "enable_distributed_cost_center")
+ )
+ and self.check_if_part_of_distributed_cost_center()
+ ):
+ frappe.throw(
+ _(
+ "Cannot enable Distributed Cost Center for a Cost Center already allocated in another Distributed Cost Center"
+ )
+ )
if next((True for x in self.distributed_cost_center if x.cost_center == x.parent), False):
frappe.throw(_("Parent Cost Center cannot be added in Distributed Cost Center"))
- if check_if_distributed_cost_center_enabled(list(x.cost_center for x in self.distributed_cost_center)):
- frappe.throw(_("A Distributed Cost Center cannot be added in the Distributed Cost Center allocation table."))
+ if check_if_distributed_cost_center_enabled(
+ list(x.cost_center for x in self.distributed_cost_center)
+ ):
+ frappe.throw(
+ _(
+ "A Distributed Cost Center cannot be added in the Distributed Cost Center allocation table."
+ )
+ )
else:
self.distributed_cost_center = []
@@ -47,9 +66,12 @@ class CostCenter(NestedSet):
def validate_parent_cost_center(self):
if self.parent_cost_center:
- if not frappe.db.get_value('Cost Center', self.parent_cost_center, 'is_group'):
- frappe.throw(_("{0} is not a group node. Please select a group node as parent cost center").format(
- frappe.bold(self.parent_cost_center)))
+ if not frappe.db.get_value("Cost Center", self.parent_cost_center, "is_group"):
+ frappe.throw(
+ _("{0} is not a group node. Please select a group node as parent cost center").format(
+ frappe.bold(self.parent_cost_center)
+ )
+ )
@frappe.whitelist()
def convert_group_to_ledger(self):
@@ -65,9 +87,13 @@ class CostCenter(NestedSet):
@frappe.whitelist()
def convert_ledger_to_group(self):
if cint(self.enable_distributed_cost_center):
- frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group"))
+ frappe.throw(
+ _("Cost Center with enabled distributed cost center can not be converted to group")
+ )
if self.check_if_part_of_distributed_cost_center():
- frappe.throw(_("Cost Center Already Allocated in a Distributed Cost Center cannot be converted to group"))
+ frappe.throw(
+ _("Cost Center Already Allocated in a Distributed Cost Center cannot be converted to group")
+ )
if self.check_gle_exists():
frappe.throw(_("Cost Center with existing transactions can not be converted to group"))
self.is_group = 1
@@ -78,8 +104,11 @@ class CostCenter(NestedSet):
return frappe.db.get_value("GL Entry", {"cost_center": self.name})
def check_if_child_exists(self):
- return frappe.db.sql("select name from `tabCost Center` where \
- parent_cost_center = %s and docstatus != 2", self.name)
+ return frappe.db.sql(
+ "select name from `tabCost Center` where \
+ parent_cost_center = %s and docstatus != 2",
+ self.name,
+ )
def check_if_part_of_distributed_cost_center(self):
return frappe.db.get_value("Distributed Cost Center", {"cost_center": self.name})
@@ -87,6 +116,7 @@ class CostCenter(NestedSet):
def before_rename(self, olddn, newdn, merge=False):
# Add company abbr if not provided
from erpnext.setup.doctype.company.company import get_name_with_abbr
+
new_cost_center = get_name_with_abbr(newdn, self.company)
# Validate properties before merging
@@ -100,7 +130,9 @@ class CostCenter(NestedSet):
super(CostCenter, self).after_rename(olddn, newdn, merge)
if not merge:
- new_cost_center = frappe.db.get_value("Cost Center", newdn, ["cost_center_name", "cost_center_number"], as_dict=1)
+ new_cost_center = frappe.db.get_value(
+ "Cost Center", newdn, ["cost_center_name", "cost_center_number"], as_dict=1
+ )
# exclude company abbr
new_parts = newdn.split(" - ")[:-1]
@@ -109,7 +141,9 @@ class CostCenter(NestedSet):
if len(new_parts) == 1:
new_parts = newdn.split(" ")
if new_cost_center.cost_center_number != new_parts[0]:
- validate_field_number("Cost Center", self.name, new_parts[0], self.company, "cost_center_number")
+ validate_field_number(
+ "Cost Center", self.name, new_parts[0], self.company, "cost_center_number"
+ )
self.cost_center_number = new_parts[0]
self.db_set("cost_center_number", new_parts[0])
new_parts = new_parts[1:]
@@ -120,14 +154,19 @@ class CostCenter(NestedSet):
self.cost_center_name = cost_center_name
self.db_set("cost_center_name", cost_center_name)
+
def on_doctype_update():
frappe.db.add_index("Cost Center", ["lft", "rgt"])
+
def get_name_with_number(new_account, account_number):
if account_number and not new_account[0].isdigit():
new_account = account_number + " - " + new_account
return new_account
+
def check_if_distributed_cost_center_enabled(cost_center_list):
- value_list = frappe.get_list("Cost Center", {"name": ["in", cost_center_list]}, "enable_distributed_cost_center", as_list=1)
+ value_list = frappe.get_list(
+ "Cost Center", {"name": ["in", cost_center_list]}, "enable_distributed_cost_center", as_list=1
+ )
return next((True for x in value_list if x[0]), False)
diff --git a/erpnext/accounts/doctype/cost_center/cost_center_dashboard.py b/erpnext/accounts/doctype/cost_center/cost_center_dashboard.py
index 0bae8fe1456..5059dc3cc0e 100644
--- a/erpnext/accounts/doctype/cost_center/cost_center_dashboard.py
+++ b/erpnext/accounts/doctype/cost_center/cost_center_dashboard.py
@@ -1,14 +1,8 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'cost_center',
- 'reports': [
- {
- 'label': _('Reports'),
- 'items': ['Budget Variance Report', 'General Ledger']
- }
- ]
+ "fieldname": "cost_center",
+ "reports": [{"label": _("Reports"), "items": ["Budget Variance Report", "General Ledger"]}],
}
diff --git a/erpnext/accounts/doctype/cost_center/test_cost_center.py b/erpnext/accounts/doctype/cost_center/test_cost_center.py
index f8615ec03a5..a5a2d15ca93 100644
--- a/erpnext/accounts/doctype/cost_center/test_cost_center.py
+++ b/erpnext/accounts/doctype/cost_center/test_cost_center.py
@@ -5,51 +5,53 @@ import unittest
import frappe
-test_records = frappe.get_test_records('Cost Center')
+test_records = frappe.get_test_records("Cost Center")
+
class TestCostCenter(unittest.TestCase):
def test_cost_center_creation_against_child_node(self):
- if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}):
+ if not frappe.db.get_value("Cost Center", {"name": "_Test Cost Center 2 - _TC"}):
frappe.get_doc(test_records[1]).insert()
- cost_center = frappe.get_doc({
- 'doctype': 'Cost Center',
- 'cost_center_name': '_Test Cost Center 3',
- 'parent_cost_center': '_Test Cost Center 2 - _TC',
- 'is_group': 0,
- 'company': '_Test Company'
- })
+ cost_center = frappe.get_doc(
+ {
+ "doctype": "Cost Center",
+ "cost_center_name": "_Test Cost Center 3",
+ "parent_cost_center": "_Test Cost Center 2 - _TC",
+ "is_group": 0,
+ "company": "_Test Company",
+ }
+ )
self.assertRaises(frappe.ValidationError, cost_center.save)
def test_validate_distributed_cost_center(self):
- if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center - _TC'}):
+ if not frappe.db.get_value("Cost Center", {"name": "_Test Cost Center - _TC"}):
frappe.get_doc(test_records[0]).insert()
- if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}):
+ if not frappe.db.get_value("Cost Center", {"name": "_Test Cost Center 2 - _TC"}):
frappe.get_doc(test_records[1]).insert()
- invalid_distributed_cost_center = frappe.get_doc({
- "company": "_Test Company",
- "cost_center_name": "_Test Distributed Cost Center",
- "doctype": "Cost Center",
- "is_group": 0,
- "parent_cost_center": "_Test Company - _TC",
- "enable_distributed_cost_center": 1,
- "distributed_cost_center": [{
- "cost_center": "_Test Cost Center - _TC",
- "percentage_allocation": 40
- }, {
- "cost_center": "_Test Cost Center 2 - _TC",
- "percentage_allocation": 50
- }
- ]
- })
+ invalid_distributed_cost_center = frappe.get_doc(
+ {
+ "company": "_Test Company",
+ "cost_center_name": "_Test Distributed Cost Center",
+ "doctype": "Cost Center",
+ "is_group": 0,
+ "parent_cost_center": "_Test Company - _TC",
+ "enable_distributed_cost_center": 1,
+ "distributed_cost_center": [
+ {"cost_center": "_Test Cost Center - _TC", "percentage_allocation": 40},
+ {"cost_center": "_Test Cost Center 2 - _TC", "percentage_allocation": 50},
+ ],
+ }
+ )
self.assertRaises(frappe.ValidationError, invalid_distributed_cost_center.save)
+
def create_cost_center(**args):
args = frappe._dict(args)
if args.cost_center_name:
diff --git a/erpnext/accounts/doctype/coupon_code/coupon_code.py b/erpnext/accounts/doctype/coupon_code/coupon_code.py
index ee32de1cd28..6a0cdf91c0a 100644
--- a/erpnext/accounts/doctype/coupon_code/coupon_code.py
+++ b/erpnext/accounts/doctype/coupon_code/coupon_code.py
@@ -15,7 +15,7 @@ class CouponCode(Document):
if not self.coupon_code:
if self.coupon_type == "Promotional":
- self.coupon_code =''.join(i for i in self.coupon_name if not i.isdigit())[0:8].upper()
+ self.coupon_code = "".join(i for i in self.coupon_name if not i.isdigit())[0:8].upper()
elif self.coupon_type == "Gift Card":
self.coupon_code = frappe.generate_hash()[:10].upper()
diff --git a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
index ca482c8c4ec..b897546b036 100644
--- a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
+++ b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
@@ -7,92 +7,110 @@ import frappe
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
-test_dependencies = ['Item']
+test_dependencies = ["Item"]
+
def test_create_test_data():
frappe.set_user("Administrator")
# create test item
- if not frappe.db.exists("Item","_Test Tesla Car"):
- item = frappe.get_doc({
- "description": "_Test Tesla Car",
- "doctype": "Item",
- "has_batch_no": 0,
- "has_serial_no": 0,
- "inspection_required": 0,
- "is_stock_item": 1,
- "opening_stock":100,
- "is_sub_contracted_item": 0,
- "item_code": "_Test Tesla Car",
- "item_group": "_Test Item Group",
- "item_name": "_Test Tesla Car",
- "apply_warehouse_wise_reorder_level": 0,
- "warehouse":"Stores - _TC",
- "gst_hsn_code": "999800",
- "valuation_rate": 5000,
- "standard_rate":5000,
- "item_defaults": [{
- "company": "_Test Company",
- "default_warehouse": "Stores - _TC",
- "default_price_list":"_Test Price List",
- "expense_account": "Cost of Goods Sold - _TC",
- "buying_cost_center": "Main - _TC",
- "selling_cost_center": "Main - _TC",
- "income_account": "Sales - _TC"
- }],
- })
+ if not frappe.db.exists("Item", "_Test Tesla Car"):
+ item = frappe.get_doc(
+ {
+ "description": "_Test Tesla Car",
+ "doctype": "Item",
+ "has_batch_no": 0,
+ "has_serial_no": 0,
+ "inspection_required": 0,
+ "is_stock_item": 1,
+ "opening_stock": 100,
+ "is_sub_contracted_item": 0,
+ "item_code": "_Test Tesla Car",
+ "item_group": "_Test Item Group",
+ "item_name": "_Test Tesla Car",
+ "apply_warehouse_wise_reorder_level": 0,
+ "warehouse": "Stores - _TC",
+ "gst_hsn_code": "999800",
+ "valuation_rate": 5000,
+ "standard_rate": 5000,
+ "item_defaults": [
+ {
+ "company": "_Test Company",
+ "default_warehouse": "Stores - _TC",
+ "default_price_list": "_Test Price List",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "buying_cost_center": "Main - _TC",
+ "selling_cost_center": "Main - _TC",
+ "income_account": "Sales - _TC",
+ }
+ ],
+ }
+ )
item.insert()
# create test item price
- item_price = frappe.get_list('Item Price', filters={'item_code': '_Test Tesla Car', 'price_list': '_Test Price List'}, fields=['name'])
- if len(item_price)==0:
- item_price = frappe.get_doc({
- "doctype": "Item Price",
- "item_code": "_Test Tesla Car",
- "price_list": "_Test Price List",
- "price_list_rate": 5000
- })
+ item_price = frappe.get_list(
+ "Item Price",
+ filters={"item_code": "_Test Tesla Car", "price_list": "_Test Price List"},
+ fields=["name"],
+ )
+ if len(item_price) == 0:
+ item_price = frappe.get_doc(
+ {
+ "doctype": "Item Price",
+ "item_code": "_Test Tesla Car",
+ "price_list": "_Test Price List",
+ "price_list_rate": 5000,
+ }
+ )
item_price.insert()
# create test item pricing rule
if not frappe.db.exists("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}):
- item_pricing_rule = frappe.get_doc({
- "doctype": "Pricing Rule",
- "title": "_Test Pricing Rule for _Test Item",
- "apply_on": "Item Code",
- "items": [{
- "item_code": "_Test Tesla Car"
- }],
- "warehouse":"Stores - _TC",
- "coupon_code_based":1,
- "selling": 1,
- "rate_or_discount": "Discount Percentage",
- "discount_percentage": 30,
- "company": "_Test Company",
- "currency":"INR",
- "for_price_list":"_Test Price List"
- })
+ item_pricing_rule = frappe.get_doc(
+ {
+ "doctype": "Pricing Rule",
+ "title": "_Test Pricing Rule for _Test Item",
+ "apply_on": "Item Code",
+ "items": [{"item_code": "_Test Tesla Car"}],
+ "warehouse": "Stores - _TC",
+ "coupon_code_based": 1,
+ "selling": 1,
+ "rate_or_discount": "Discount Percentage",
+ "discount_percentage": 30,
+ "company": "_Test Company",
+ "currency": "INR",
+ "for_price_list": "_Test Price List",
+ }
+ )
item_pricing_rule.insert()
# create test item sales partner
- if not frappe.db.exists("Sales Partner","_Test Coupon Partner"):
- sales_partner = frappe.get_doc({
- "doctype": "Sales Partner",
- "partner_name":"_Test Coupon Partner",
- "commission_rate":2,
- "referral_code": "COPART"
- })
+ if not frappe.db.exists("Sales Partner", "_Test Coupon Partner"):
+ sales_partner = frappe.get_doc(
+ {
+ "doctype": "Sales Partner",
+ "partner_name": "_Test Coupon Partner",
+ "commission_rate": 2,
+ "referral_code": "COPART",
+ }
+ )
sales_partner.insert()
# create test item coupon code
if not frappe.db.exists("Coupon Code", "SAVE30"):
- pricing_rule = frappe.db.get_value("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}, ['name'])
- coupon_code = frappe.get_doc({
- "doctype": "Coupon Code",
- "coupon_name":"SAVE30",
- "coupon_code":"SAVE30",
- "pricing_rule": pricing_rule,
- "valid_from": "2014-01-01",
- "maximum_use":1,
- "used":0
- })
+ pricing_rule = frappe.db.get_value(
+ "Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}, ["name"]
+ )
+ coupon_code = frappe.get_doc(
+ {
+ "doctype": "Coupon Code",
+ "coupon_name": "SAVE30",
+ "coupon_code": "SAVE30",
+ "pricing_rule": pricing_rule,
+ "valid_from": "2014-01-01",
+ "maximum_use": 1,
+ "used": 0,
+ }
+ )
coupon_code.insert()
+
class TestCouponCode(unittest.TestCase):
def setUp(self):
test_create_test_data()
@@ -103,15 +121,21 @@ class TestCouponCode(unittest.TestCase):
def test_sales_order_with_coupon_code(self):
frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)
- so = make_sales_order(company='_Test Company', warehouse='Stores - _TC',
- customer="_Test Customer", selling_price_list="_Test Price List",
- item_code="_Test Tesla Car", rate=5000, qty=1,
- do_not_submit=True)
+ so = make_sales_order(
+ company="_Test Company",
+ warehouse="Stores - _TC",
+ customer="_Test Customer",
+ selling_price_list="_Test Price List",
+ item_code="_Test Tesla Car",
+ rate=5000,
+ qty=1,
+ do_not_submit=True,
+ )
self.assertEqual(so.items[0].rate, 5000)
- so.coupon_code='SAVE30'
- so.sales_partner='_Test Coupon Partner'
+ so.coupon_code = "SAVE30"
+ so.sales_partner = "_Test Coupon Partner"
so.save()
# check item price after coupon code is applied
diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py
index 02377cd5659..b675bde58af 100644
--- a/erpnext/accounts/doctype/dunning/dunning.py
+++ b/erpnext/accounts/doctype/dunning/dunning.py
@@ -20,78 +20,99 @@ class Dunning(AccountsController):
self.validate_overdue_days()
self.validate_amount()
if not self.income_account:
- self.income_account = frappe.db.get_value('Company', self.company, 'default_income_account')
+ self.income_account = frappe.db.get_value("Company", self.company, "default_income_account")
def validate_overdue_days(self):
self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0
def validate_amount(self):
amounts = calculate_interest_and_amount(
- self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days)
- if self.interest_amount != amounts.get('interest_amount'):
- self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount'))
- if self.dunning_amount != amounts.get('dunning_amount'):
- self.dunning_amount = flt(amounts.get('dunning_amount'), self.precision('dunning_amount'))
- if self.grand_total != amounts.get('grand_total'):
- self.grand_total = flt(amounts.get('grand_total'), self.precision('grand_total'))
+ self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days
+ )
+ if self.interest_amount != amounts.get("interest_amount"):
+ self.interest_amount = flt(amounts.get("interest_amount"), self.precision("interest_amount"))
+ if self.dunning_amount != amounts.get("dunning_amount"):
+ self.dunning_amount = flt(amounts.get("dunning_amount"), self.precision("dunning_amount"))
+ if self.grand_total != amounts.get("grand_total"):
+ self.grand_total = flt(amounts.get("grand_total"), self.precision("grand_total"))
def on_submit(self):
self.make_gl_entries()
def on_cancel(self):
if self.dunning_amount:
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
def make_gl_entries(self):
if not self.dunning_amount:
return
gl_entries = []
- invoice_fields = ["project", "cost_center", "debit_to", "party_account_currency", "conversion_rate", "cost_center"]
+ invoice_fields = [
+ "project",
+ "cost_center",
+ "debit_to",
+ "party_account_currency",
+ "conversion_rate",
+ "cost_center",
+ ]
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
accounting_dimensions = get_accounting_dimensions()
invoice_fields.extend(accounting_dimensions)
dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
- default_cost_center = frappe.get_cached_value('Company', self.company, 'cost_center')
+ default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
gl_entries.append(
- self.get_gl_dict({
- "account": inv.debit_to,
- "party_type": "Customer",
- "party": self.customer,
- "due_date": self.due_date,
- "against": self.income_account,
- "debit": dunning_in_company_currency,
- "debit_in_account_currency": self.dunning_amount,
- "against_voucher": self.name,
- "against_voucher_type": "Dunning",
- "cost_center": inv.cost_center or default_cost_center,
- "project": inv.project
- }, inv.party_account_currency, item=inv)
+ self.get_gl_dict(
+ {
+ "account": inv.debit_to,
+ "party_type": "Customer",
+ "party": self.customer,
+ "due_date": self.due_date,
+ "against": self.income_account,
+ "debit": dunning_in_company_currency,
+ "debit_in_account_currency": self.dunning_amount,
+ "against_voucher": self.name,
+ "against_voucher_type": "Dunning",
+ "cost_center": inv.cost_center or default_cost_center,
+ "project": inv.project,
+ },
+ inv.party_account_currency,
+ item=inv,
+ )
)
gl_entries.append(
- self.get_gl_dict({
- "account": self.income_account,
- "against": self.customer,
- "credit": dunning_in_company_currency,
- "cost_center": inv.cost_center or default_cost_center,
- "credit_in_account_currency": self.dunning_amount,
- "project": inv.project
- }, item=inv)
+ self.get_gl_dict(
+ {
+ "account": self.income_account,
+ "against": self.customer,
+ "credit": dunning_in_company_currency,
+ "cost_center": inv.cost_center or default_cost_center,
+ "credit_in_account_currency": self.dunning_amount,
+ "project": inv.project,
+ },
+ item=inv,
+ )
+ )
+ make_gl_entries(
+ gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False
)
- make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False)
def resolve_dunning(doc, state):
for reference in doc.references:
- if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0:
- dunnings = frappe.get_list('Dunning', filters={
- 'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}, ignore_permissions=True)
+ if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0:
+ dunnings = frappe.get_list(
+ "Dunning",
+ filters={"sales_invoice": reference.reference_name, "status": ("!=", "Resolved")},
+ ignore_permissions=True,
+ )
for dunning in dunnings:
- frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')
+ frappe.db.set_value("Dunning", dunning.name, "status", "Resolved")
+
def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
interest_amount = 0
@@ -102,23 +123,26 @@ def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_
grand_total += flt(interest_amount)
dunning_amount = flt(interest_amount) + flt(dunning_fee)
return {
- 'interest_amount': interest_amount,
- 'grand_total': grand_total,
- 'dunning_amount': dunning_amount}
+ "interest_amount": interest_amount,
+ "grand_total": grand_total,
+ "dunning_amount": dunning_amount,
+ }
+
@frappe.whitelist()
def get_dunning_letter_text(dunning_type, doc, language=None):
if isinstance(doc, string_types):
doc = json.loads(doc)
if language:
- filters = {'parent': dunning_type, 'language': language}
+ filters = {"parent": dunning_type, "language": language}
else:
- filters = {'parent': dunning_type, 'is_default_language': 1}
- letter_text = frappe.db.get_value('Dunning Letter Text', filters,
- ['body_text', 'closing_text', 'language'], as_dict=1)
+ filters = {"parent": dunning_type, "is_default_language": 1}
+ letter_text = frappe.db.get_value(
+ "Dunning Letter Text", filters, ["body_text", "closing_text", "language"], as_dict=1
+ )
if letter_text:
return {
- 'body_text': frappe.render_template(letter_text.body_text, doc),
- 'closing_text': frappe.render_template(letter_text.closing_text, doc),
- 'language': letter_text.language
+ "body_text": frappe.render_template(letter_text.body_text, doc),
+ "closing_text": frappe.render_template(letter_text.closing_text, doc),
+ "language": letter_text.language,
}
diff --git a/erpnext/accounts/doctype/dunning/dunning_dashboard.py b/erpnext/accounts/doctype/dunning/dunning_dashboard.py
index ebe4efb0985..d1d40314104 100644
--- a/erpnext/accounts/doctype/dunning/dunning_dashboard.py
+++ b/erpnext/accounts/doctype/dunning/dunning_dashboard.py
@@ -1,18 +1,12 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'dunning',
- 'non_standard_fieldnames': {
- 'Journal Entry': 'reference_name',
- 'Payment Entry': 'reference_name'
+ "fieldname": "dunning",
+ "non_standard_fieldnames": {
+ "Journal Entry": "reference_name",
+ "Payment Entry": "reference_name",
},
- 'transactions': [
- {
- 'label': _('Payment'),
- 'items': ['Payment Entry', 'Journal Entry']
- }
- ]
+ "transactions": [{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}],
}
diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py
index 27543a89fc0..e1fd1e984f5 100644
--- a/erpnext/accounts/doctype/dunning/test_dunning.py
+++ b/erpnext/accounts/doctype/dunning/test_dunning.py
@@ -30,31 +30,35 @@ class TestDunning(unittest.TestCase):
def test_dunning(self):
dunning = create_dunning()
amounts = calculate_interest_and_amount(
- dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
- self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44)
- self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44)
- self.assertEqual(round(amounts.get('grand_total'), 2), 120.44)
+ dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
+ )
+ self.assertEqual(round(amounts.get("interest_amount"), 2), 0.44)
+ self.assertEqual(round(amounts.get("dunning_amount"), 2), 20.44)
+ self.assertEqual(round(amounts.get("grand_total"), 2), 120.44)
def test_dunning_with_zero_interest_rate(self):
dunning = create_dunning_with_zero_interest_rate()
amounts = calculate_interest_and_amount(
- dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
- self.assertEqual(round(amounts.get('interest_amount'), 2), 0)
- self.assertEqual(round(amounts.get('dunning_amount'), 2), 20)
- self.assertEqual(round(amounts.get('grand_total'), 2), 120)
-
+ dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
+ )
+ self.assertEqual(round(amounts.get("interest_amount"), 2), 0)
+ self.assertEqual(round(amounts.get("dunning_amount"), 2), 20)
+ self.assertEqual(round(amounts.get("grand_total"), 2), 120)
def test_gl_entries(self):
dunning = create_dunning()
dunning.submit()
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit
from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s
- order by account asc""", dunning.name, as_dict=1)
+ order by account asc""",
+ dunning.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
- expected_values = dict((d[0], d) for d in [
- ['Debtors - _TC', 20.44, 0.0],
- ['Sales - _TC', 0.0, 20.44]
- ])
+ expected_values = dict(
+ (d[0], d) for d in [["Debtors - _TC", 20.44, 0.0], ["Sales - _TC", 0.0, 20.44]]
+ )
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
@@ -72,7 +76,7 @@ class TestDunning(unittest.TestCase):
pe.target_exchange_rate = 1
pe.insert()
pe.submit()
- si_doc = frappe.get_doc('Sales Invoice', dunning.sales_invoice)
+ si_doc = frappe.get_doc("Sales Invoice", dunning.sales_invoice)
self.assertEqual(si_doc.outstanding_amount, 0)
@@ -80,8 +84,9 @@ def create_dunning():
posting_date = add_days(today(), -20)
due_date = add_days(today(), -15)
sales_invoice = create_sales_invoice_against_cost_center(
- posting_date=posting_date, due_date=due_date, status='Overdue')
- dunning_type = frappe.get_doc("Dunning Type", 'First Notice')
+ posting_date=posting_date, due_date=due_date, status="Overdue"
+ )
+ dunning_type = frappe.get_doc("Dunning Type", "First Notice")
dunning = frappe.new_doc("Dunning")
dunning.sales_invoice = sales_invoice.name
dunning.customer_name = sales_invoice.customer_name
@@ -91,18 +96,20 @@ def create_dunning():
dunning.company = sales_invoice.company
dunning.posting_date = nowdate()
dunning.due_date = sales_invoice.due_date
- dunning.dunning_type = 'First Notice'
+ dunning.dunning_type = "First Notice"
dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.dunning_fee = dunning_type.dunning_fee
dunning.save()
return dunning
+
def create_dunning_with_zero_interest_rate():
posting_date = add_days(today(), -20)
due_date = add_days(today(), -15)
sales_invoice = create_sales_invoice_against_cost_center(
- posting_date=posting_date, due_date=due_date, status='Overdue')
- dunning_type = frappe.get_doc("Dunning Type", 'First Notice with 0% Rate of Interest')
+ posting_date=posting_date, due_date=due_date, status="Overdue"
+ )
+ dunning_type = frappe.get_doc("Dunning Type", "First Notice with 0% Rate of Interest")
dunning = frappe.new_doc("Dunning")
dunning.sales_invoice = sales_invoice.name
dunning.customer_name = sales_invoice.customer_name
@@ -112,40 +119,44 @@ def create_dunning_with_zero_interest_rate():
dunning.company = sales_invoice.company
dunning.posting_date = nowdate()
dunning.due_date = sales_invoice.due_date
- dunning.dunning_type = 'First Notice with 0% Rate of Interest'
+ dunning.dunning_type = "First Notice with 0% Rate of Interest"
dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.dunning_fee = dunning_type.dunning_fee
dunning.save()
return dunning
+
def create_dunning_type():
dunning_type = frappe.new_doc("Dunning Type")
- dunning_type.dunning_type = 'First Notice'
+ dunning_type.dunning_type = "First Notice"
dunning_type.start_day = 10
dunning_type.end_day = 20
dunning_type.dunning_fee = 20
dunning_type.rate_of_interest = 8
dunning_type.append(
- "dunning_letter_text", {
- 'language': 'en',
- 'body_text': 'We have still not received payment for our invoice ',
- 'closing_text': 'We kindly request that you pay the outstanding amount immediately, including interest and late fees.'
- }
+ "dunning_letter_text",
+ {
+ "language": "en",
+ "body_text": "We have still not received payment for our invoice ",
+ "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.",
+ },
)
dunning_type.save()
+
def create_dunning_type_with_zero_interest_rate():
dunning_type = frappe.new_doc("Dunning Type")
- dunning_type.dunning_type = 'First Notice with 0% Rate of Interest'
+ dunning_type.dunning_type = "First Notice with 0% Rate of Interest"
dunning_type.start_day = 10
dunning_type.end_day = 20
dunning_type.dunning_fee = 20
dunning_type.rate_of_interest = 0
dunning_type.append(
- "dunning_letter_text", {
- 'language': 'en',
- 'body_text': 'We have still not received payment for our invoice ',
- 'closing_text': 'We kindly request that you pay the outstanding amount immediately, and late fees.'
- }
+ "dunning_letter_text",
+ {
+ "language": "en",
+ "body_text": "We have still not received payment for our invoice ",
+ "closing_text": "We kindly request that you pay the outstanding amount immediately, and late fees.",
+ },
)
dunning_type.save()
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
index 1b13195ce98..2f81c5fb750 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
@@ -20,8 +20,9 @@ class ExchangeRateRevaluation(Document):
def set_total_gain_loss(self):
total_gain_loss = 0
for d in self.accounts:
- d.gain_loss = flt(d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency")) \
- - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency"))
+ d.gain_loss = flt(
+ d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency")
+ ) - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency"))
total_gain_loss += flt(d.gain_loss, d.precision("gain_loss"))
self.total_gain_loss = flt(total_gain_loss, self.precision("total_gain_loss"))
@@ -30,15 +31,15 @@ class ExchangeRateRevaluation(Document):
frappe.throw(_("Please select Company and Posting Date to getting entries"))
def on_cancel(self):
- self.ignore_linked_doctypes = ('GL Entry')
+ self.ignore_linked_doctypes = "GL Entry"
@frappe.whitelist()
def check_journal_entry_condition(self):
- total_debit = frappe.db.get_value("Journal Entry Account", {
- 'reference_type': 'Exchange Rate Revaluation',
- 'reference_name': self.name,
- 'docstatus': 1
- }, "sum(debit) as sum")
+ total_debit = frappe.db.get_value(
+ "Journal Entry Account",
+ {"reference_type": "Exchange Rate Revaluation", "reference_name": self.name, "docstatus": 1},
+ "sum(debit) as sum",
+ )
total_amt = 0
for d in self.accounts:
@@ -54,28 +55,33 @@ class ExchangeRateRevaluation(Document):
accounts = []
self.validate_mandatory()
company_currency = erpnext.get_company_currency(self.company)
- precision = get_field_precision(frappe.get_meta("Exchange Rate Revaluation Account")
- .get_field("new_balance_in_base_currency"), company_currency)
+ precision = get_field_precision(
+ frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
+ company_currency,
+ )
account_details = self.get_accounts_from_gle()
for d in account_details:
- current_exchange_rate = d.balance / d.balance_in_account_currency \
- if d.balance_in_account_currency else 0
+ current_exchange_rate = (
+ d.balance / d.balance_in_account_currency if d.balance_in_account_currency else 0
+ )
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, self.posting_date)
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
if gain_loss:
- accounts.append({
- "account": d.account,
- "party_type": d.party_type,
- "party": d.party,
- "account_currency": d.account_currency,
- "balance_in_base_currency": d.balance,
- "balance_in_account_currency": d.balance_in_account_currency,
- "current_exchange_rate": current_exchange_rate,
- "new_exchange_rate": new_exchange_rate,
- "new_balance_in_base_currency": new_balance_in_base_currency
- })
+ accounts.append(
+ {
+ "account": d.account,
+ "party_type": d.party_type,
+ "party": d.party,
+ "account_currency": d.account_currency,
+ "balance_in_base_currency": d.balance,
+ "balance_in_account_currency": d.balance_in_account_currency,
+ "current_exchange_rate": current_exchange_rate,
+ "new_exchange_rate": new_exchange_rate,
+ "new_balance_in_base_currency": new_balance_in_base_currency,
+ }
+ )
if not accounts:
self.throw_invalid_response_message(account_details)
@@ -84,7 +90,8 @@ class ExchangeRateRevaluation(Document):
def get_accounts_from_gle(self):
company_currency = erpnext.get_company_currency(self.company)
- accounts = frappe.db.sql_list("""
+ accounts = frappe.db.sql_list(
+ """
select name
from tabAccount
where is_group = 0
@@ -93,11 +100,14 @@ class ExchangeRateRevaluation(Document):
and account_type != 'Stock'
and company=%s
and account_currency != %s
- order by name""",(self.company, company_currency))
+ order by name""",
+ (self.company, company_currency),
+ )
account_details = []
if accounts:
- account_details = frappe.db.sql("""
+ account_details = frappe.db.sql(
+ """
select
account, party_type, party, account_currency,
sum(debit_in_account_currency) - sum(credit_in_account_currency) as balance_in_account_currency,
@@ -109,7 +119,11 @@ class ExchangeRateRevaluation(Document):
group by account, NULLIF(party_type,''), NULLIF(party,'')
having sum(debit) != sum(credit)
order by account
- """ % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1)
+ """
+ % (", ".join(["%s"] * len(accounts)), "%s"),
+ tuple(accounts + [self.posting_date]),
+ as_dict=1,
+ )
return account_details
@@ -125,77 +139,107 @@ class ExchangeRateRevaluation(Document):
if self.total_gain_loss == 0:
return
- unrealized_exchange_gain_loss_account = frappe.get_cached_value('Company', self.company,
- "unrealized_exchange_gain_loss_account")
+ unrealized_exchange_gain_loss_account = frappe.get_cached_value(
+ "Company", self.company, "unrealized_exchange_gain_loss_account"
+ )
if not unrealized_exchange_gain_loss_account:
- frappe.throw(_("Please set Unrealized Exchange Gain/Loss Account in Company {0}")
- .format(self.company))
+ frappe.throw(
+ _("Please set Unrealized Exchange Gain/Loss Account in Company {0}").format(self.company)
+ )
- journal_entry = frappe.new_doc('Journal Entry')
- journal_entry.voucher_type = 'Exchange Rate Revaluation'
+ journal_entry = frappe.new_doc("Journal Entry")
+ journal_entry.voucher_type = "Exchange Rate Revaluation"
journal_entry.company = self.company
journal_entry.posting_date = self.posting_date
journal_entry.multi_currency = 1
journal_entry_accounts = []
for d in self.accounts:
- dr_or_cr = "debit_in_account_currency" \
- if d.get("balance_in_account_currency") > 0 else "credit_in_account_currency"
+ dr_or_cr = (
+ "debit_in_account_currency"
+ if d.get("balance_in_account_currency") > 0
+ else "credit_in_account_currency"
+ )
- reverse_dr_or_cr = "debit_in_account_currency" \
- if dr_or_cr=="credit_in_account_currency" else "credit_in_account_currency"
+ reverse_dr_or_cr = (
+ "debit_in_account_currency"
+ if dr_or_cr == "credit_in_account_currency"
+ else "credit_in_account_currency"
+ )
- journal_entry_accounts.append({
- "account": d.get("account"),
- "party_type": d.get("party_type"),
- "party": d.get("party"),
- "account_currency": d.get("account_currency"),
- "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
- dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
- "exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
+ journal_entry_accounts.append(
+ {
+ "account": d.get("account"),
+ "party_type": d.get("party_type"),
+ "party": d.get("party"),
+ "account_currency": d.get("account_currency"),
+ "balance": flt(
+ d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")
+ ),
+ dr_or_cr: flt(
+ abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
+ ),
+ "exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
+ "reference_type": "Exchange Rate Revaluation",
+ "reference_name": self.name,
+ }
+ )
+ journal_entry_accounts.append(
+ {
+ "account": d.get("account"),
+ "party_type": d.get("party_type"),
+ "party": d.get("party"),
+ "account_currency": d.get("account_currency"),
+ "balance": flt(
+ d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")
+ ),
+ reverse_dr_or_cr: flt(
+ abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
+ ),
+ "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
+ "reference_type": "Exchange Rate Revaluation",
+ "reference_name": self.name,
+ }
+ )
+
+ journal_entry_accounts.append(
+ {
+ "account": unrealized_exchange_gain_loss_account,
+ "balance": get_balance_on(unrealized_exchange_gain_loss_account),
+ "debit_in_account_currency": abs(self.total_gain_loss) if self.total_gain_loss < 0 else 0,
+ "credit_in_account_currency": self.total_gain_loss if self.total_gain_loss > 0 else 0,
+ "exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
- })
- journal_entry_accounts.append({
- "account": d.get("account"),
- "party_type": d.get("party_type"),
- "party": d.get("party"),
- "account_currency": d.get("account_currency"),
- "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
- reverse_dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
- "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
- "reference_type": "Exchange Rate Revaluation",
- "reference_name": self.name
- })
-
- journal_entry_accounts.append({
- "account": unrealized_exchange_gain_loss_account,
- "balance": get_balance_on(unrealized_exchange_gain_loss_account),
- "debit_in_account_currency": abs(self.total_gain_loss) if self.total_gain_loss < 0 else 0,
- "credit_in_account_currency": self.total_gain_loss if self.total_gain_loss > 0 else 0,
- "exchange_rate": 1,
- "reference_type": "Exchange Rate Revaluation",
- "reference_name": self.name,
- })
+ }
+ )
journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_amounts_in_company_currency()
journal_entry.set_total_debit_credit()
return journal_entry.as_dict()
+
@frappe.whitelist()
def get_account_details(account, company, posting_date, party_type=None, party=None):
- account_currency, account_type = frappe.db.get_value("Account", account,
- ["account_currency", "account_type"])
+ account_currency, account_type = frappe.db.get_value(
+ "Account", account, ["account_currency", "account_type"]
+ )
if account_type in ["Receivable", "Payable"] and not (party_type and party):
frappe.throw(_("Party Type and Party is mandatory for {0} account").format(account_type))
account_details = {}
company_currency = erpnext.get_company_currency(company)
- balance = get_balance_on(account, date=posting_date, 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:
- 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
+ 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
+ )
new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate
account_details = {
@@ -204,7 +248,7 @@ def get_account_details(account, company, posting_date, party_type=None, party=N
"balance_in_account_currency": balance_in_account_currency,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
- "new_balance_in_base_currency": new_balance_in_base_currency
+ "new_balance_in_base_currency": new_balance_in_base_currency,
}
return account_details
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation_dashboard.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation_dashboard.py
index 0efe291d217..7eca9703c24 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation_dashboard.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation_dashboard.py
@@ -1,11 +1,2 @@
-
-
def get_data():
- return {
- 'fieldname': 'reference_name',
- 'transactions': [
- {
- 'items': ['Journal Entry']
- }
- ]
- }
+ return {"fieldname": "reference_name", "transactions": [{"items": ["Journal Entry"]}]}
diff --git a/erpnext/accounts/doctype/finance_book/finance_book_dashboard.py b/erpnext/accounts/doctype/finance_book/finance_book_dashboard.py
index 4a56cd39d00..24e6c0c8721 100644
--- a/erpnext/accounts/doctype/finance_book/finance_book_dashboard.py
+++ b/erpnext/accounts/doctype/finance_book/finance_book_dashboard.py
@@ -1,24 +1,13 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'finance_book',
- 'non_standard_fieldnames': {
- 'Asset': 'default_finance_book',
- 'Company': 'default_finance_book'
- },
- 'transactions': [
- {
- 'label': _('Assets'),
- 'items': ['Asset', 'Asset Value Adjustment']
- },
- {
- 'items': ['Company']
- },
- {
- 'items': ['Journal Entry']
- }
- ]
+ "fieldname": "finance_book",
+ "non_standard_fieldnames": {"Asset": "default_finance_book", "Company": "default_finance_book"},
+ "transactions": [
+ {"label": _("Assets"), "items": ["Asset", "Asset Value Adjustment"]},
+ {"items": ["Company"]},
+ {"items": ["Journal Entry"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/finance_book/test_finance_book.py b/erpnext/accounts/doctype/finance_book/test_finance_book.py
index 8fbf72d7c1c..7b2575d2c32 100644
--- a/erpnext/accounts/doctype/finance_book/test_finance_book.py
+++ b/erpnext/accounts/doctype/finance_book/test_finance_book.py
@@ -13,31 +13,30 @@ class TestFinanceBook(unittest.TestCase):
finance_book = create_finance_book()
# create jv entry
- jv = make_journal_entry("_Test Bank - _TC",
- "_Test Receivable - _TC", 100, save=False)
+ jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable - _TC", 100, save=False)
- jv.accounts[1].update({
- "party_type": "Customer",
- "party": "_Test Customer"
- })
+ jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer"})
jv.finance_book = finance_book.finance_book_name
jv.submit()
# check the Finance Book in the GL Entry
- gl_entries = frappe.get_all("GL Entry", fields=["name", "finance_book"],
- filters={"voucher_type": "Journal Entry", "voucher_no": jv.name})
+ gl_entries = frappe.get_all(
+ "GL Entry",
+ fields=["name", "finance_book"],
+ filters={"voucher_type": "Journal Entry", "voucher_no": jv.name},
+ )
for gl_entry in gl_entries:
self.assertEqual(gl_entry.finance_book, finance_book.name)
+
def create_finance_book():
if not frappe.db.exists("Finance Book", "_Test Finance Book"):
- finance_book = frappe.get_doc({
- "doctype": "Finance Book",
- "finance_book_name": "_Test Finance Book"
- }).insert()
+ finance_book = frappe.get_doc(
+ {"doctype": "Finance Book", "finance_book_name": "_Test Finance Book"}
+ ).insert()
else:
finance_book = frappe.get_doc("Finance Book", "_Test Finance Book")
- return finance_book
\ No newline at end of file
+ return finance_book
diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
index dd893f9fc80..069ab5ea843 100644
--- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
+++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
@@ -9,7 +9,9 @@ from frappe.model.document import Document
from frappe.utils import add_days, add_years, cstr, getdate
-class FiscalYearIncorrectDate(frappe.ValidationError): pass
+class FiscalYearIncorrectDate(frappe.ValidationError):
+ pass
+
class FiscalYear(Document):
@frappe.whitelist()
@@ -22,19 +24,33 @@ class FiscalYear(Document):
# clear cache
frappe.clear_cache()
- msgprint(_("{0} is now the default Fiscal Year. Please refresh your browser for the change to take effect.").format(self.name))
+ msgprint(
+ _(
+ "{0} is now the default Fiscal Year. Please refresh your browser for the change to take effect."
+ ).format(self.name)
+ )
def validate(self):
self.validate_dates()
self.validate_overlap()
if not self.is_new():
- year_start_end_dates = frappe.db.sql("""select year_start_date, year_end_date
- from `tabFiscal Year` where name=%s""", (self.name))
+ year_start_end_dates = frappe.db.sql(
+ """select year_start_date, year_end_date
+ from `tabFiscal Year` where name=%s""",
+ (self.name),
+ )
if year_start_end_dates:
- if getdate(self.year_start_date) != year_start_end_dates[0][0] or getdate(self.year_end_date) != year_start_end_dates[0][1]:
- frappe.throw(_("Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."))
+ if (
+ getdate(self.year_start_date) != year_start_end_dates[0][0]
+ or getdate(self.year_end_date) != year_start_end_dates[0][1]
+ ):
+ frappe.throw(
+ _(
+ "Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."
+ )
+ )
def validate_dates(self):
if self.is_short_year:
@@ -43,14 +59,18 @@ class FiscalYear(Document):
return
if getdate(self.year_start_date) > getdate(self.year_end_date):
- frappe.throw(_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"),
- FiscalYearIncorrectDate)
+ frappe.throw(
+ _("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"),
+ FiscalYearIncorrectDate,
+ )
date = getdate(self.year_start_date) + relativedelta(years=1) - relativedelta(days=1)
if getdate(self.year_end_date) != date:
- frappe.throw(_("Fiscal Year End Date should be one year after Fiscal Year Start Date"),
- FiscalYearIncorrectDate)
+ frappe.throw(
+ _("Fiscal Year End Date should be one year after Fiscal Year Start Date"),
+ FiscalYearIncorrectDate,
+ )
def on_update(self):
check_duplicate_fiscal_year(self)
@@ -59,11 +79,16 @@ class FiscalYear(Document):
def on_trash(self):
global_defaults = frappe.get_doc("Global Defaults")
if global_defaults.current_fiscal_year == self.name:
- frappe.throw(_("You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings").format(self.name))
+ frappe.throw(
+ _(
+ "You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings"
+ ).format(self.name)
+ )
frappe.cache().delete_value("fiscal_years")
def validate_overlap(self):
- existing_fiscal_years = frappe.db.sql("""select name from `tabFiscal Year`
+ existing_fiscal_years = frappe.db.sql(
+ """select name from `tabFiscal Year`
where (
(%(year_start_date)s between year_start_date and year_end_date)
or (%(year_end_date)s between year_start_date and year_end_date)
@@ -73,13 +98,18 @@ class FiscalYear(Document):
{
"year_start_date": self.year_start_date,
"year_end_date": self.year_end_date,
- "name": self.name or "No Name"
- }, as_dict=True)
+ "name": self.name or "No Name",
+ },
+ as_dict=True,
+ )
if existing_fiscal_years:
for existing in existing_fiscal_years:
- company_for_existing = frappe.db.sql_list("""select company from `tabFiscal Year Company`
- where parent=%s""", existing.name)
+ company_for_existing = frappe.db.sql_list(
+ """select company from `tabFiscal Year Company`
+ where parent=%s""",
+ existing.name,
+ )
overlap = False
if not self.get("companies") or not company_for_existing:
@@ -90,20 +120,36 @@ class FiscalYear(Document):
overlap = True
if overlap:
- frappe.throw(_("Year start date or end date is overlapping with {0}. To avoid please set company")
- .format(existing.name), frappe.NameError)
+ frappe.throw(
+ _("Year start date or end date is overlapping with {0}. To avoid please set company").format(
+ existing.name
+ ),
+ frappe.NameError,
+ )
+
@frappe.whitelist()
def check_duplicate_fiscal_year(doc):
- year_start_end_dates = frappe.db.sql("""select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""", (doc.name))
+ year_start_end_dates = frappe.db.sql(
+ """select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""",
+ (doc.name),
+ )
for fiscal_year, ysd, yed in year_start_end_dates:
- if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (not frappe.flags.in_test):
- frappe.throw(_("Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}").format(fiscal_year))
+ if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (
+ not frappe.flags.in_test
+ ):
+ frappe.throw(
+ _("Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}").format(
+ fiscal_year
+ )
+ )
@frappe.whitelist()
def auto_create_fiscal_year():
- for d in frappe.db.sql("""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""):
+ for d in frappe.db.sql(
+ """select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""
+ ):
try:
current_fy = frappe.get_doc("Fiscal Year", d[0])
@@ -114,16 +160,14 @@ def auto_create_fiscal_year():
start_year = cstr(new_fy.year_start_date.year)
end_year = cstr(new_fy.year_end_date.year)
- new_fy.year = start_year if start_year==end_year else (start_year + "-" + end_year)
+ new_fy.year = start_year if start_year == end_year else (start_year + "-" + end_year)
new_fy.auto_created = 1
new_fy.insert(ignore_permissions=True)
except frappe.NameError:
pass
+
def get_from_and_to_date(fiscal_year):
- fields = [
- "year_start_date as from_date",
- "year_end_date as to_date"
- ]
+ fields = ["year_start_date as from_date", "year_end_date as to_date"]
return frappe.db.get_value("Fiscal Year", fiscal_year, fields, as_dict=1)
diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py
index 3ede25f19d4..bc966916ef3 100644
--- a/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py
+++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py
@@ -1,22 +1,15 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'fiscal_year',
- 'transactions': [
+ "fieldname": "fiscal_year",
+ "transactions": [
+ {"label": _("Budgets"), "items": ["Budget"]},
+ {"label": _("References"), "items": ["Period Closing Voucher"]},
{
- 'label': _('Budgets'),
- 'items': ['Budget']
+ "label": _("Target Details"),
+ "items": ["Sales Person", "Sales Partner", "Territory", "Monthly Distribution"],
},
- {
- 'label': _('References'),
- 'items': ['Period Closing Voucher']
- },
- {
- 'label': _('Target Details'),
- 'items': ['Sales Person', 'Sales Partner', 'Territory', 'Monthly Distribution']
- }
- ]
+ ],
}
diff --git a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py
index 69e13a407de..6e946f74660 100644
--- a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py
+++ b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py
@@ -11,43 +11,48 @@ from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrect
test_ignore = ["Company"]
-class TestFiscalYear(unittest.TestCase):
+class TestFiscalYear(unittest.TestCase):
def test_extra_year(self):
if frappe.db.exists("Fiscal Year", "_Test Fiscal Year 2000"):
frappe.delete_doc("Fiscal Year", "_Test Fiscal Year 2000")
- fy = frappe.get_doc({
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2000",
- "year_end_date": "2002-12-31",
- "year_start_date": "2000-04-01"
- })
+ fy = frappe.get_doc(
+ {
+ "doctype": "Fiscal Year",
+ "year": "_Test Fiscal Year 2000",
+ "year_end_date": "2002-12-31",
+ "year_start_date": "2000-04-01",
+ }
+ )
self.assertRaises(FiscalYearIncorrectDate, fy.insert)
def test_record_generator():
test_records = [
- {
- "doctype": "Fiscal Year",
- "year": "_Test Short Fiscal Year 2011",
- "is_short_year": 1,
- "year_end_date": "2011-04-01",
- "year_start_date": "2011-12-31"
- }
+ {
+ "doctype": "Fiscal Year",
+ "year": "_Test Short Fiscal Year 2011",
+ "is_short_year": 1,
+ "year_end_date": "2011-04-01",
+ "year_start_date": "2011-12-31",
+ }
]
start = 2012
end = now_datetime().year + 5
for year in range(start, end):
- test_records.append({
- "doctype": "Fiscal Year",
- "year": f"_Test Fiscal Year {year}",
- "year_start_date": f"{year}-01-01",
- "year_end_date": f"{year}-12-31"
- })
+ test_records.append(
+ {
+ "doctype": "Fiscal Year",
+ "year": f"_Test Fiscal Year {year}",
+ "year_start_date": f"{year}-01-01",
+ "year_end_date": f"{year}-12-31",
+ }
+ )
return test_records
+
test_records = test_record_generator()
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index f184b95a92d..e590beb0261 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -26,6 +26,8 @@ from erpnext.exceptions import (
)
exclude_from_linked_with = True
+
+
class GLEntry(Document):
def autoname(self):
"""
@@ -33,6 +35,8 @@ class GLEntry(Document):
name will be changed using autoname options (in a scheduled job)
"""
self.name = frappe.generate_hash(txt="", length=10)
+ if self.meta.autoname == "hash":
+ self.to_rename = 0
def validate(self):
self.flags.ignore_submit_comment = True
@@ -56,14 +60,18 @@ class GLEntry(Document):
validate_frozen_account(self.account, adv_adj)
# Update outstanding amt on against voucher
- if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees']
- and self.against_voucher and self.flags.update_outstanding == 'Yes'
- and not frappe.flags.is_reverse_depr_entry):
- update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
- self.against_voucher)
+ if (
+ self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
+ and self.against_voucher
+ and self.flags.update_outstanding == "Yes"
+ and not frappe.flags.is_reverse_depr_entry
+ ):
+ update_outstanding_amt(
+ self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher
+ )
def check_mandatory(self):
- mandatory = ['account','voucher_type','voucher_no','company']
+ mandatory = ["account", "voucher_type", "voucher_no", "company"]
for k in mandatory:
if not self.get(k):
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
@@ -71,29 +79,40 @@ class GLEntry(Document):
if not (self.party_type and self.party):
account_type = frappe.get_cached_value("Account", self.account, "account_type")
if account_type == "Receivable":
- frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}")
- .format(self.voucher_type, self.voucher_no, self.account))
+ frappe.throw(
+ _("{0} {1}: Customer is required against Receivable account {2}").format(
+ self.voucher_type, self.voucher_no, self.account
+ )
+ )
elif account_type == "Payable":
- frappe.throw(_("{0} {1}: Supplier is required against Payable account {2}")
- .format(self.voucher_type, self.voucher_no, self.account))
+ frappe.throw(
+ _("{0} {1}: Supplier is required against Payable account {2}").format(
+ self.voucher_type, self.voucher_no, self.account
+ )
+ )
# Zero value transaction is not allowed
if not (flt(self.debit, self.precision("debit")) or flt(self.credit, self.precision("credit"))):
- frappe.throw(_("{0} {1}: Either debit or credit amount is required for {2}")
- .format(self.voucher_type, self.voucher_no, self.account))
+ frappe.throw(
+ _("{0} {1}: Either debit or credit amount is required for {2}").format(
+ self.voucher_type, self.voucher_no, self.account
+ )
+ )
def pl_must_have_cost_center(self):
"""Validate that profit and loss type account GL entries have a cost center."""
- if self.cost_center or self.voucher_type == 'Period Closing Voucher':
+ if self.cost_center or self.voucher_type == "Period Closing Voucher":
return
if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format(
- self.voucher_type, self.voucher_no, self.account)
+ self.voucher_type, self.voucher_no, self.account
+ )
msg += " "
- msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format(
- self.voucher_type)
+ msg += _(
+ "Please set the cost center field in {0} or setup a default Cost Center for the Company."
+ ).format(self.voucher_type)
frappe.throw(msg, title=_("Missing Cost Center"))
@@ -101,17 +120,31 @@ class GLEntry(Document):
account_type = frappe.db.get_value("Account", self.account, "report_type")
for dimension in get_checks_for_pl_and_bs_accounts():
- if account_type == "Profit and Loss" \
- and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled:
+ if (
+ account_type == "Profit and Loss"
+ and self.company == dimension.company
+ and dimension.mandatory_for_pl
+ and not dimension.disabled
+ ):
if not self.get(dimension.fieldname):
- frappe.throw(_("Accounting Dimension {0} is required for 'Profit and Loss' account {1}.")
- .format(dimension.label, self.account))
+ frappe.throw(
+ _("Accounting Dimension {0} is required for 'Profit and Loss' account {1}.").format(
+ dimension.label, self.account
+ )
+ )
- if account_type == "Balance Sheet" \
- and self.company == dimension.company and dimension.mandatory_for_bs and not dimension.disabled:
+ if (
+ account_type == "Balance Sheet"
+ and self.company == dimension.company
+ and dimension.mandatory_for_bs
+ and not dimension.disabled
+ ):
if not self.get(dimension.fieldname):
- frappe.throw(_("Accounting Dimension {0} is required for 'Balance Sheet' account {1}.")
- .format(dimension.label, self.account))
+ frappe.throw(
+ _("Accounting Dimension {0} is required for 'Balance Sheet' account {1}.").format(
+ dimension.label, self.account
+ )
+ )
def validate_allowed_dimensions(self):
dimension_filter_map = get_dimension_filter_map()
@@ -120,56 +153,97 @@ class GLEntry(Document):
account = key[1]
if self.account == account:
- if value['is_mandatory'] and not self.get(dimension):
- frappe.throw(_("{0} is mandatory for account {1}").format(
- frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), MandatoryAccountDimensionError)
+ if value["is_mandatory"] and not self.get(dimension):
+ frappe.throw(
+ _("{0} is mandatory for account {1}").format(
+ frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)
+ ),
+ MandatoryAccountDimensionError,
+ )
- if value['allow_or_restrict'] == 'Allow':
- if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']:
- frappe.throw(_("Invalid value {0} for {1} against account {2}").format(
- frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
+ if value["allow_or_restrict"] == "Allow":
+ if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]:
+ frappe.throw(
+ _("Invalid value {0} for {1} against account {2}").format(
+ frappe.bold(self.get(dimension)),
+ frappe.bold(frappe.unscrub(dimension)),
+ frappe.bold(self.account),
+ ),
+ InvalidAccountDimensionError,
+ )
else:
- if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']:
- frappe.throw(_("Invalid value {0} for {1} against account {2}").format(
- frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
+ if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]:
+ frappe.throw(
+ _("Invalid value {0} for {1} against account {2}").format(
+ frappe.bold(self.get(dimension)),
+ frappe.bold(frappe.unscrub(dimension)),
+ frappe.bold(self.account),
+ ),
+ InvalidAccountDimensionError,
+ )
def check_pl_account(self):
- if self.is_opening=='Yes' and \
- frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
- frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
- .format(self.voucher_type, self.voucher_no, self.account))
+ if (
+ self.is_opening == "Yes"
+ and frappe.db.get_value("Account", self.account, "report_type") == "Profit and Loss"
+ and not self.is_cancelled
+ ):
+ frappe.throw(
+ _("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry").format(
+ self.voucher_type, self.voucher_no, self.account
+ )
+ )
def validate_account_details(self, adv_adj):
"""Account must be ledger, active and not freezed"""
- ret = frappe.db.sql("""select is_group, docstatus, company
- from tabAccount where name=%s""", self.account, as_dict=1)[0]
+ ret = frappe.db.sql(
+ """select is_group, docstatus, company
+ from tabAccount where name=%s""",
+ self.account,
+ as_dict=1,
+ )[0]
- if ret.is_group==1:
- frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions''')
- .format(self.voucher_type, self.voucher_no, self.account))
+ if ret.is_group == 1:
+ frappe.throw(
+ _(
+ """{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
+ ).format(self.voucher_type, self.voucher_no, self.account)
+ )
- if ret.docstatus==2:
- frappe.throw(_("{0} {1}: Account {2} is inactive")
- .format(self.voucher_type, self.voucher_no, self.account))
+ if ret.docstatus == 2:
+ frappe.throw(
+ _("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
+ )
if ret.company != self.company:
- frappe.throw(_("{0} {1}: Account {2} does not belong to Company {3}")
- .format(self.voucher_type, self.voucher_no, self.account, self.company))
+ frappe.throw(
+ _("{0} {1}: Account {2} does not belong to Company {3}").format(
+ self.voucher_type, self.voucher_no, self.account, self.company
+ )
+ )
def validate_cost_center(self):
- if not self.cost_center: return
+ if not self.cost_center:
+ return
- is_group, company = frappe.get_cached_value('Cost Center',
- self.cost_center, ['is_group', 'company'])
+ is_group, company = frappe.get_cached_value(
+ "Cost Center", self.cost_center, ["is_group", "company"]
+ )
if company != self.company:
- frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}")
- .format(self.voucher_type, self.voucher_no, self.cost_center, self.company))
+ frappe.throw(
+ _("{0} {1}: Cost Center {2} does not belong to Company {3}").format(
+ self.voucher_type, self.voucher_no, self.cost_center, self.company
+ )
+ )
- if (self.voucher_type != 'Period Closing Voucher' and is_group):
- frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""").format(
- self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
+ if self.voucher_type != "Period Closing Voucher" and is_group:
+ frappe.throw(
+ _(
+ """{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions"""
+ ).format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))
+ )
def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party)
@@ -182,9 +256,12 @@ class GLEntry(Document):
self.account_currency = account_currency or company_currency
if account_currency != self.account_currency:
- frappe.throw(_("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}")
- .format(self.voucher_type, self.voucher_no, self.account,
- (account_currency or company_currency)), InvalidAccountCurrency)
+ frappe.throw(
+ _("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}").format(
+ self.voucher_type, self.voucher_no, self.account, (account_currency or company_currency)
+ ),
+ InvalidAccountCurrency,
+ )
if self.party_type and self.party:
validate_party_gle_currency(self.party_type, self.party, self.company, self.account_currency)
@@ -193,51 +270,80 @@ class GLEntry(Document):
if not self.fiscal_year:
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
+
def validate_balance_type(account, adv_adj=False):
if not adv_adj and account:
balance_must_be = frappe.db.get_value("Account", account, "balance_must_be")
if balance_must_be:
- balance = frappe.db.sql("""select sum(debit) - sum(credit)
- from `tabGL Entry` where account = %s""", account)[0][0]
+ balance = frappe.db.sql(
+ """select sum(debit) - sum(credit)
+ from `tabGL Entry` where account = %s""",
+ account,
+ )[0][0]
- if (balance_must_be=="Debit" and flt(balance) < 0) or \
- (balance_must_be=="Credit" and flt(balance) > 0):
- frappe.throw(_("Balance for Account {0} must always be {1}").format(account, _(balance_must_be)))
+ if (balance_must_be == "Debit" and flt(balance) < 0) or (
+ balance_must_be == "Credit" and flt(balance) > 0
+ ):
+ frappe.throw(
+ _("Balance for Account {0} must always be {1}").format(account, _(balance_must_be))
+ )
-def update_outstanding_amt(account, party_type, party, against_voucher_type, against_voucher, on_cancel=False):
+
+def update_outstanding_amt(
+ account, party_type, party, against_voucher_type, against_voucher, on_cancel=False
+):
if party_type and party:
- party_condition = " and party_type={0} and party={1}"\
- .format(frappe.db.escape(party_type), frappe.db.escape(party))
+ party_condition = " and party_type={0} and party={1}".format(
+ frappe.db.escape(party_type), frappe.db.escape(party)
+ )
else:
party_condition = ""
if against_voucher_type == "Sales Invoice":
party_account = frappe.db.get_value(against_voucher_type, against_voucher, "debit_to")
- account_condition = "and account in ({0}, {1})".format(frappe.db.escape(account), frappe.db.escape(party_account))
+ account_condition = "and account in ({0}, {1})".format(
+ frappe.db.escape(account), frappe.db.escape(party_account)
+ )
else:
account_condition = " and account = {0}".format(frappe.db.escape(account))
# get final outstanding amt
- bal = flt(frappe.db.sql("""
+ bal = flt(
+ frappe.db.sql(
+ """
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry`
where against_voucher_type=%s and against_voucher=%s
and voucher_type != 'Invoice Discounting'
- {0} {1}""".format(party_condition, account_condition),
- (against_voucher_type, against_voucher))[0][0] or 0.0)
+ {0} {1}""".format(
+ party_condition, account_condition
+ ),
+ (against_voucher_type, against_voucher),
+ )[0][0]
+ or 0.0
+ )
- if against_voucher_type == 'Purchase Invoice':
+ if against_voucher_type == "Purchase Invoice":
bal = -bal
elif against_voucher_type == "Journal Entry":
- against_voucher_amount = flt(frappe.db.sql("""
+ against_voucher_amount = flt(
+ frappe.db.sql(
+ """
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry` where voucher_type = 'Journal Entry' and voucher_no = %s
- and account = %s and (against_voucher is null or against_voucher='') {0}"""
- .format(party_condition), (against_voucher, account))[0][0])
+ and account = %s and (against_voucher is null or against_voucher='') {0}""".format(
+ party_condition
+ ),
+ (against_voucher, account),
+ )[0][0]
+ )
if not against_voucher_amount:
- frappe.throw(_("Against Journal Entry {0} is already adjusted against some other voucher")
- .format(against_voucher))
+ frappe.throw(
+ _("Against Journal Entry {0} is already adjusted against some other voucher").format(
+ against_voucher
+ )
+ )
bal = against_voucher_amount + bal
if against_voucher_amount < 0:
@@ -245,44 +351,51 @@ def update_outstanding_amt(account, party_type, party, against_voucher_type, aga
# Validation : Outstanding can not be negative for JV
if bal < 0 and not on_cancel:
- frappe.throw(_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal)))
+ frappe.throw(
+ _("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal))
+ )
if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]:
ref_doc = frappe.get_doc(against_voucher_type, against_voucher)
# Didn't use db_set for optimisation purpose
ref_doc.outstanding_amount = bal
- frappe.db.set_value(against_voucher_type, against_voucher, 'outstanding_amount', bal)
+ frappe.db.set_value(against_voucher_type, against_voucher, "outstanding_amount", bal)
ref_doc.set_status(update=True)
def validate_frozen_account(account, adv_adj=None):
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
- if frozen_account == 'Yes' and not adv_adj:
- frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,
- 'frozen_accounts_modifier')
+ if frozen_account == "Yes" and not adv_adj:
+ frozen_accounts_modifier = frappe.db.get_value(
+ "Accounts Settings", None, "frozen_accounts_modifier"
+ )
if not frozen_accounts_modifier:
frappe.throw(_("Account {0} is frozen").format(account))
elif frozen_accounts_modifier not in frappe.get_roles():
frappe.throw(_("Not authorized to edit frozen Account {0}").format(account))
+
def update_against_account(voucher_type, voucher_no):
- entries = frappe.db.get_all("GL Entry",
+ entries = frappe.db.get_all(
+ "GL Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
- fields=["name", "party", "against", "debit", "credit", "account", "company"])
+ fields=["name", "party", "against", "debit", "credit", "account", "company"],
+ )
if not entries:
return
company_currency = erpnext.get_company_currency(entries[0].company)
- precision = get_field_precision(frappe.get_meta("GL Entry")
- .get_field("debit"), company_currency)
+ precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
accounts_debited, accounts_credited = [], []
for d in entries:
- if flt(d.debit, precision) > 0: accounts_debited.append(d.party or d.account)
- if flt(d.credit, precision) > 0: accounts_credited.append(d.party or d.account)
+ if flt(d.debit, precision) > 0:
+ accounts_debited.append(d.party or d.account)
+ if flt(d.credit, precision) > 0:
+ accounts_credited.append(d.party or d.account)
for d in entries:
if flt(d.debit, precision) > 0:
@@ -293,14 +406,17 @@ def update_against_account(voucher_type, voucher_no):
if d.against != new_against:
frappe.db.set_value("GL Entry", d.name, "against", new_against)
+
def on_doctype_update():
frappe.db.add_index("GL Entry", ["against_voucher_type", "against_voucher"])
frappe.db.add_index("GL Entry", ["voucher_type", "voucher_no"])
+
def rename_gle_sle_docs():
for doctype in ["GL Entry", "Stock Ledger Entry"]:
rename_temporarily_named_docs(doctype)
+
def rename_temporarily_named_docs(doctype):
"""Rename temporarily named docs using autoname options"""
docs_to_rename = frappe.get_all(doctype, {"to_rename": "1"}, order_by="creation", limit=50000)
@@ -311,5 +427,5 @@ def rename_temporarily_named_docs(doctype):
frappe.db.sql(
"UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype),
(newname, oldname),
- auto_commit=True
+ auto_commit=True,
)
diff --git a/erpnext/accounts/doctype/gl_entry/test_gl_entry.py b/erpnext/accounts/doctype/gl_entry/test_gl_entry.py
index 3de23946892..b188b09843a 100644
--- a/erpnext/accounts/doctype/gl_entry/test_gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/test_gl_entry.py
@@ -14,48 +14,68 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ
class TestGLEntry(unittest.TestCase):
def test_round_off_entry(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "_Test Write Off - _TC")
- frappe.db.set_value("Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC")
+ frappe.db.set_value(
+ "Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC"
+ )
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 100, "_Test Cost Center - _TC", submit=False)
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 100,
+ "_Test Cost Center - _TC",
+ submit=False,
+ )
jv.get("accounts")[0].debit = 100.01
jv.flags.ignore_validate = True
jv.submit()
- round_off_entry = frappe.db.sql("""select name from `tabGL Entry`
+ round_off_entry = frappe.db.sql(
+ """select name from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_no = %s
and account='_Test Write Off - _TC' and cost_center='_Test Cost Center - _TC'
- and debit = 0 and credit = '.01'""", jv.name)
+ and debit = 0 and credit = '.01'""",
+ jv.name,
+ )
self.assertTrue(round_off_entry)
def test_rename_entries(self):
- je = make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True)
+ je = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True
+ )
rename_gle_sle_docs()
naming_series = parse_naming_series(parts=frappe.get_meta("GL Entry").autoname.split(".")[:-1])
- je = make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True)
+ je = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True
+ )
- gl_entries = frappe.get_all("GL Entry",
+ gl_entries = frappe.get_all(
+ "GL Entry",
fields=["name", "to_rename"],
filters={"voucher_type": "Journal Entry", "voucher_no": je.name},
- order_by="creation"
+ order_by="creation",
)
self.assertTrue(all(entry.to_rename == 1 for entry in gl_entries))
- old_naming_series_current_value = frappe.db.sql("SELECT current from tabSeries where name = %s", naming_series)[0][0]
+ old_naming_series_current_value = frappe.db.sql(
+ "SELECT current from tabSeries where name = %s", naming_series
+ )[0][0]
rename_gle_sle_docs()
- new_gl_entries = frappe.get_all("GL Entry",
+ new_gl_entries = frappe.get_all(
+ "GL Entry",
fields=["name", "to_rename"],
filters={"voucher_type": "Journal Entry", "voucher_no": je.name},
- order_by="creation"
+ order_by="creation",
)
self.assertTrue(all(entry.to_rename == 0 for entry in new_gl_entries))
self.assertTrue(all(new.name != old.name for new, old in zip(gl_entries, new_gl_entries)))
- new_naming_series_current_value = frappe.db.sql("SELECT current from tabSeries where name = %s", naming_series)[0][0]
+ new_naming_series_current_value = frappe.db.sql(
+ "SELECT current from tabSeries where name = %s", naming_series
+ )[0][0]
self.assertEqual(old_naming_series_current_value + 2, new_naming_series_current_value)
diff --git a/erpnext/accounts/doctype/gst_account/gst_account.json b/erpnext/accounts/doctype/gst_account/gst_account.json
index b6ec8844e18..be5124c2d4d 100644
--- a/erpnext/accounts/doctype/gst_account/gst_account.json
+++ b/erpnext/accounts/doctype/gst_account/gst_account.json
@@ -10,6 +10,7 @@
"sgst_account",
"igst_account",
"cess_account",
+ "utgst_account",
"is_reverse_charge_account"
],
"fields": [
@@ -64,12 +65,18 @@
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Reverse Charge Account"
+ },
+ {
+ "fieldname": "utgst_account",
+ "fieldtype": "Link",
+ "label": "UTGST Account",
+ "options": "Account"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-04-09 12:30:25.889993",
+ "modified": "2022-04-07 12:59:14.039768",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GST Account",
@@ -78,5 +85,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
index 09c389de738..5bd4585a9a8 100644
--- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
+++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
@@ -33,19 +33,32 @@ class InvoiceDiscounting(AccountsController):
frappe.throw(_("Loan Start Date and Loan Period are mandatory to save the Invoice Discounting"))
def validate_invoices(self):
- discounted_invoices = [record.sales_invoice for record in
- frappe.get_all("Discounted Invoice",fields=["sales_invoice"], filters={"docstatus":1})]
+ discounted_invoices = [
+ record.sales_invoice
+ for record in frappe.get_all(
+ "Discounted Invoice", fields=["sales_invoice"], filters={"docstatus": 1}
+ )
+ ]
for record in self.invoices:
if record.sales_invoice in discounted_invoices:
- frappe.throw(_("Row({0}): {1} is already discounted in {2}")
- .format(record.idx, frappe.bold(record.sales_invoice), frappe.bold(record.parent)))
+ frappe.throw(
+ _("Row({0}): {1} is already discounted in {2}").format(
+ record.idx, frappe.bold(record.sales_invoice), frappe.bold(record.parent)
+ )
+ )
- actual_outstanding = frappe.db.get_value("Sales Invoice", record.sales_invoice,"outstanding_amount")
- if record.outstanding_amount > actual_outstanding :
- frappe.throw(_
- ("Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2}").format(
- record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice)))
+ actual_outstanding = frappe.db.get_value(
+ "Sales Invoice", record.sales_invoice, "outstanding_amount"
+ )
+ if record.outstanding_amount > actual_outstanding:
+ frappe.throw(
+ _(
+ "Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2}"
+ ).format(
+ record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice)
+ )
+ )
def calculate_total_amount(self):
self.total_amount = sum(flt(d.outstanding_amount) for d in self.invoices)
@@ -73,24 +86,21 @@ class InvoiceDiscounting(AccountsController):
self.status = "Cancelled"
if cancel:
- self.db_set('status', self.status, update_modified = True)
+ self.db_set("status", self.status, update_modified=True)
def update_sales_invoice(self):
for d in self.invoices:
if self.docstatus == 1:
is_discounted = 1
else:
- discounted_invoice = frappe.db.exists({
- "doctype": "Discounted Invoice",
- "sales_invoice": d.sales_invoice,
- "docstatus": 1
- })
+ discounted_invoice = frappe.db.exists(
+ {"doctype": "Discounted Invoice", "sales_invoice": d.sales_invoice, "docstatus": 1}
+ )
is_discounted = 1 if discounted_invoice else 0
frappe.db.set_value("Sales Invoice", d.sales_invoice, "is_discounted", is_discounted)
def make_gl_entries(self):
- company_currency = frappe.get_cached_value('Company', self.company, "default_currency")
-
+ company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
gl_entries = []
invoice_fields = ["debit_to", "party_account_currency", "conversion_rate", "cost_center"]
@@ -102,135 +112,182 @@ class InvoiceDiscounting(AccountsController):
inv = frappe.db.get_value("Sales Invoice", d.sales_invoice, invoice_fields, as_dict=1)
if d.outstanding_amount:
- outstanding_in_company_currency = flt(d.outstanding_amount * inv.conversion_rate,
- d.precision("outstanding_amount"))
- ar_credit_account_currency = frappe.get_cached_value("Account", self.accounts_receivable_credit, "currency")
+ outstanding_in_company_currency = flt(
+ d.outstanding_amount * inv.conversion_rate, d.precision("outstanding_amount")
+ )
+ ar_credit_account_currency = frappe.get_cached_value(
+ "Account", self.accounts_receivable_credit, "currency"
+ )
- gl_entries.append(self.get_gl_dict({
- "account": inv.debit_to,
- "party_type": "Customer",
- "party": d.customer,
- "against": self.accounts_receivable_credit,
- "credit": outstanding_in_company_currency,
- "credit_in_account_currency": outstanding_in_company_currency \
- if inv.party_account_currency==company_currency else d.outstanding_amount,
- "cost_center": inv.cost_center,
- "against_voucher": d.sales_invoice,
- "against_voucher_type": "Sales Invoice"
- }, inv.party_account_currency, item=inv))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": inv.debit_to,
+ "party_type": "Customer",
+ "party": d.customer,
+ "against": self.accounts_receivable_credit,
+ "credit": outstanding_in_company_currency,
+ "credit_in_account_currency": outstanding_in_company_currency
+ if inv.party_account_currency == company_currency
+ else d.outstanding_amount,
+ "cost_center": inv.cost_center,
+ "against_voucher": d.sales_invoice,
+ "against_voucher_type": "Sales Invoice",
+ },
+ inv.party_account_currency,
+ item=inv,
+ )
+ )
- gl_entries.append(self.get_gl_dict({
- "account": self.accounts_receivable_credit,
- "party_type": "Customer",
- "party": d.customer,
- "against": inv.debit_to,
- "debit": outstanding_in_company_currency,
- "debit_in_account_currency": outstanding_in_company_currency \
- if ar_credit_account_currency==company_currency else d.outstanding_amount,
- "cost_center": inv.cost_center,
- "against_voucher": d.sales_invoice,
- "against_voucher_type": "Sales Invoice"
- }, ar_credit_account_currency, item=inv))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": self.accounts_receivable_credit,
+ "party_type": "Customer",
+ "party": d.customer,
+ "against": inv.debit_to,
+ "debit": outstanding_in_company_currency,
+ "debit_in_account_currency": outstanding_in_company_currency
+ if ar_credit_account_currency == company_currency
+ else d.outstanding_amount,
+ "cost_center": inv.cost_center,
+ "against_voucher": d.sales_invoice,
+ "against_voucher_type": "Sales Invoice",
+ },
+ ar_credit_account_currency,
+ item=inv,
+ )
+ )
- make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding='No')
+ make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding="No")
@frappe.whitelist()
def create_disbursement_entry(self):
je = frappe.new_doc("Journal Entry")
- je.voucher_type = 'Journal Entry'
+ je.voucher_type = "Journal Entry"
je.company = self.company
- je.remark = 'Loan Disbursement entry against Invoice Discounting: ' + self.name
+ je.remark = "Loan Disbursement entry against Invoice Discounting: " + self.name
- je.append("accounts", {
- "account": self.bank_account,
- "debit_in_account_currency": flt(self.total_amount) - flt(self.bank_charges),
- "cost_center": erpnext.get_default_cost_center(self.company)
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.bank_account,
+ "debit_in_account_currency": flt(self.total_amount) - flt(self.bank_charges),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ },
+ )
if self.bank_charges:
- je.append("accounts", {
- "account": self.bank_charges_account,
- "debit_in_account_currency": flt(self.bank_charges),
- "cost_center": erpnext.get_default_cost_center(self.company)
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.bank_charges_account,
+ "debit_in_account_currency": flt(self.bank_charges),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ },
+ )
- je.append("accounts", {
- "account": self.short_term_loan,
- "credit_in_account_currency": flt(self.total_amount),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "reference_type": "Invoice Discounting",
- "reference_name": self.name
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.short_term_loan,
+ "credit_in_account_currency": flt(self.total_amount),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "reference_type": "Invoice Discounting",
+ "reference_name": self.name,
+ },
+ )
for d in self.invoices:
- je.append("accounts", {
- "account": self.accounts_receivable_discounted,
- "debit_in_account_currency": flt(d.outstanding_amount),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "reference_type": "Invoice Discounting",
- "reference_name": self.name,
- "party_type": "Customer",
- "party": d.customer
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.accounts_receivable_discounted,
+ "debit_in_account_currency": flt(d.outstanding_amount),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "reference_type": "Invoice Discounting",
+ "reference_name": self.name,
+ "party_type": "Customer",
+ "party": d.customer,
+ },
+ )
- je.append("accounts", {
- "account": self.accounts_receivable_credit,
- "credit_in_account_currency": flt(d.outstanding_amount),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "reference_type": "Invoice Discounting",
- "reference_name": self.name,
- "party_type": "Customer",
- "party": d.customer
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.accounts_receivable_credit,
+ "credit_in_account_currency": flt(d.outstanding_amount),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "reference_type": "Invoice Discounting",
+ "reference_name": self.name,
+ "party_type": "Customer",
+ "party": d.customer,
+ },
+ )
return je
@frappe.whitelist()
def close_loan(self):
je = frappe.new_doc("Journal Entry")
- je.voucher_type = 'Journal Entry'
+ je.voucher_type = "Journal Entry"
je.company = self.company
- je.remark = 'Loan Settlement entry against Invoice Discounting: ' + self.name
+ je.remark = "Loan Settlement entry against Invoice Discounting: " + self.name
- je.append("accounts", {
- "account": self.short_term_loan,
- "debit_in_account_currency": flt(self.total_amount),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "reference_type": "Invoice Discounting",
- "reference_name": self.name,
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.short_term_loan,
+ "debit_in_account_currency": flt(self.total_amount),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "reference_type": "Invoice Discounting",
+ "reference_name": self.name,
+ },
+ )
- je.append("accounts", {
- "account": self.bank_account,
- "credit_in_account_currency": flt(self.total_amount),
- "cost_center": erpnext.get_default_cost_center(self.company)
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.bank_account,
+ "credit_in_account_currency": flt(self.total_amount),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ },
+ )
if getdate(self.loan_end_date) > getdate(nowdate()):
for d in self.invoices:
- outstanding_amount = frappe.db.get_value("Sales Invoice", d.sales_invoice, "outstanding_amount")
+ outstanding_amount = frappe.db.get_value(
+ "Sales Invoice", d.sales_invoice, "outstanding_amount"
+ )
if flt(outstanding_amount) > 0:
- je.append("accounts", {
- "account": self.accounts_receivable_discounted,
- "credit_in_account_currency": flt(outstanding_amount),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "reference_type": "Invoice Discounting",
- "reference_name": self.name,
- "party_type": "Customer",
- "party": d.customer
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.accounts_receivable_discounted,
+ "credit_in_account_currency": flt(outstanding_amount),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "reference_type": "Invoice Discounting",
+ "reference_name": self.name,
+ "party_type": "Customer",
+ "party": d.customer,
+ },
+ )
- je.append("accounts", {
- "account": self.accounts_receivable_unpaid,
- "debit_in_account_currency": flt(outstanding_amount),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "reference_type": "Invoice Discounting",
- "reference_name": self.name,
- "party_type": "Customer",
- "party": d.customer
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.accounts_receivable_unpaid,
+ "debit_in_account_currency": flt(outstanding_amount),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "reference_type": "Invoice Discounting",
+ "reference_name": self.name,
+ "party_type": "Customer",
+ "party": d.customer,
+ },
+ )
return je
+
@frappe.whitelist()
def get_invoices(filters):
filters = frappe._dict(json.loads(filters))
@@ -250,7 +307,8 @@ def get_invoices(filters):
if cond:
where_condition += " and " + " and ".join(cond)
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
select
name as sales_invoice,
customer,
@@ -264,17 +322,26 @@ def get_invoices(filters):
%s
and not exists(select di.name from `tabDiscounted Invoice` di
where di.docstatus=1 and di.sales_invoice=si.name)
- """ % where_condition, filters, as_dict=1)
+ """
+ % where_condition,
+ filters,
+ as_dict=1,
+ )
+
def get_party_account_based_on_invoice_discounting(sales_invoice):
party_account = None
- invoice_discounting = frappe.db.sql("""
+ invoice_discounting = frappe.db.sql(
+ """
select par.accounts_receivable_discounted, par.accounts_receivable_unpaid, par.status
from `tabInvoice Discounting` par, `tabDiscounted Invoice` ch
where par.name=ch.parent
and par.docstatus=1
and ch.sales_invoice = %s
- """, (sales_invoice), as_dict=1)
+ """,
+ (sales_invoice),
+ as_dict=1,
+ )
if invoice_discounting:
if invoice_discounting[0].status == "Disbursed":
party_account = invoice_discounting[0].accounts_receivable_discounted
diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_dashboard.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_dashboard.py
index 771846e508a..a442231d9a4 100644
--- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_dashboard.py
+++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_dashboard.py
@@ -1,21 +1,12 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'reference_name',
- 'internal_links': {
- 'Sales Invoice': ['invoices', 'sales_invoice']
- },
- 'transactions': [
- {
- 'label': _('Reference'),
- 'items': ['Sales Invoice']
- },
- {
- 'label': _('Payment'),
- 'items': ['Payment Entry', 'Journal Entry']
- }
- ]
+ "fieldname": "reference_name",
+ "internal_links": {"Sales Invoice": ["invoices", "sales_invoice"]},
+ "transactions": [
+ {"label": _("Reference"), "items": ["Sales Invoice"]},
+ {"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py
index d1d4be36f17..a85fdfcad7f 100644
--- a/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py
+++ b/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py
@@ -14,52 +14,74 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_
class TestInvoiceDiscounting(unittest.TestCase):
def setUp(self):
- self.ar_credit = create_account(account_name="_Test Accounts Receivable Credit", parent_account = "Accounts Receivable - _TC", company="_Test Company")
- self.ar_discounted = create_account(account_name="_Test Accounts Receivable Discounted", parent_account = "Accounts Receivable - _TC", company="_Test Company")
- self.ar_unpaid = create_account(account_name="_Test Accounts Receivable Unpaid", parent_account = "Accounts Receivable - _TC", company="_Test Company")
- self.short_term_loan = create_account(account_name="_Test Short Term Loan", parent_account = "Source of Funds (Liabilities) - _TC", company="_Test Company")
- self.bank_account = create_account(account_name="_Test Bank 2", parent_account = "Bank Accounts - _TC", company="_Test Company")
- self.bank_charges_account = create_account(account_name="_Test Bank Charges Account", parent_account = "Expenses - _TC", company="_Test Company")
+ self.ar_credit = create_account(
+ account_name="_Test Accounts Receivable Credit",
+ parent_account="Accounts Receivable - _TC",
+ company="_Test Company",
+ )
+ self.ar_discounted = create_account(
+ account_name="_Test Accounts Receivable Discounted",
+ parent_account="Accounts Receivable - _TC",
+ company="_Test Company",
+ )
+ self.ar_unpaid = create_account(
+ account_name="_Test Accounts Receivable Unpaid",
+ parent_account="Accounts Receivable - _TC",
+ company="_Test Company",
+ )
+ self.short_term_loan = create_account(
+ account_name="_Test Short Term Loan",
+ parent_account="Source of Funds (Liabilities) - _TC",
+ company="_Test Company",
+ )
+ self.bank_account = create_account(
+ account_name="_Test Bank 2", parent_account="Bank Accounts - _TC", company="_Test Company"
+ )
+ self.bank_charges_account = create_account(
+ account_name="_Test Bank Charges Account",
+ parent_account="Expenses - _TC",
+ company="_Test Company",
+ )
frappe.db.set_value("Company", "_Test Company", "default_bank_account", self.bank_account)
def test_total_amount(self):
inv1 = create_sales_invoice(rate=200)
inv2 = create_sales_invoice(rate=500)
- inv_disc = create_invoice_discounting([inv1.name, inv2.name],
+ inv_disc = create_invoice_discounting(
+ [inv1.name, inv2.name],
do_not_submit=True,
accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid,
short_term_loan=self.short_term_loan,
bank_charges_account=self.bank_charges_account,
- bank_account=self.bank_account
- )
+ bank_account=self.bank_account,
+ )
self.assertEqual(inv_disc.total_amount, 700)
def test_gl_entries_in_base_currency(self):
inv = create_sales_invoice(rate=200)
- inv_disc = create_invoice_discounting([inv.name],
+ inv_disc = create_invoice_discounting(
+ [inv.name],
accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid,
short_term_loan=self.short_term_loan,
bank_charges_account=self.bank_charges_account,
- bank_account=self.bank_account
- )
+ bank_account=self.bank_account,
+ )
gle = get_gl_entries("Invoice Discounting", inv_disc.name)
- expected_gle = {
- inv.debit_to: [0.0, 200],
- self.ar_credit: [200, 0.0]
- }
+ expected_gle = {inv.debit_to: [0.0, 200], self.ar_credit: [200, 0.0]}
for i, gle in enumerate(gle):
self.assertEqual([gle.debit, gle.credit], expected_gle.get(gle.account))
def test_loan_on_submit(self):
inv = create_sales_invoice(rate=300)
- inv_disc = create_invoice_discounting([inv.name],
+ inv_disc = create_invoice_discounting(
+ [inv.name],
accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid,
@@ -67,28 +89,33 @@ class TestInvoiceDiscounting(unittest.TestCase):
bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account,
start=nowdate(),
- period=60
- )
+ period=60,
+ )
self.assertEqual(inv_disc.status, "Sanctioned")
- self.assertEqual(inv_disc.loan_end_date, add_days(inv_disc.loan_start_date, inv_disc.loan_period))
-
+ self.assertEqual(
+ inv_disc.loan_end_date, add_days(inv_disc.loan_start_date, inv_disc.loan_period)
+ )
def test_on_disbursed(self):
inv = create_sales_invoice(rate=500)
- inv_disc = create_invoice_discounting([inv.name],
+ inv_disc = create_invoice_discounting(
+ [inv.name],
accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid,
short_term_loan=self.short_term_loan,
bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account,
- bank_charges=100
- )
+ bank_charges=100,
+ )
je = inv_disc.create_disbursement_entry()
self.assertEqual(je.accounts[0].account, self.bank_account)
- self.assertEqual(je.accounts[0].debit_in_account_currency, flt(inv_disc.total_amount) - flt(inv_disc.bank_charges))
+ self.assertEqual(
+ je.accounts[0].debit_in_account_currency,
+ flt(inv_disc.total_amount) - flt(inv_disc.bank_charges),
+ )
self.assertEqual(je.accounts[1].account, self.bank_charges_account)
self.assertEqual(je.accounts[1].debit_in_account_currency, flt(inv_disc.bank_charges))
@@ -102,7 +129,6 @@ class TestInvoiceDiscounting(unittest.TestCase):
self.assertEqual(je.accounts[4].account, self.ar_credit)
self.assertEqual(je.accounts[4].credit_in_account_currency, flt(inv.outstanding_amount))
-
je.posting_date = nowdate()
je.submit()
@@ -114,7 +140,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
def test_on_close_after_loan_period(self):
inv = create_sales_invoice(rate=600)
- inv_disc = create_invoice_discounting([inv.name],
+ inv_disc = create_invoice_discounting(
+ [inv.name],
accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid,
@@ -122,8 +149,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account,
start=nowdate(),
- period=60
- )
+ period=60,
+ )
je1 = inv_disc.create_disbursement_entry()
je1.posting_date = nowdate()
@@ -151,7 +178,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
def test_on_close_after_loan_period_after_inv_payment(self):
inv = create_sales_invoice(rate=600)
- inv_disc = create_invoice_discounting([inv.name],
+ inv_disc = create_invoice_discounting(
+ [inv.name],
accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid,
@@ -159,8 +187,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account,
start=nowdate(),
- period=60
- )
+ period=60,
+ )
je1 = inv_disc.create_disbursement_entry()
je1.posting_date = nowdate()
@@ -183,7 +211,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
def test_on_close_before_loan_period(self):
inv = create_sales_invoice(rate=700)
- inv_disc = create_invoice_discounting([inv.name],
+ inv_disc = create_invoice_discounting(
+ [inv.name],
accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid,
@@ -191,7 +220,7 @@ class TestInvoiceDiscounting(unittest.TestCase):
bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account,
start=add_days(nowdate(), -80),
- period=60
+ period=60,
)
je1 = inv_disc.create_disbursement_entry()
@@ -209,16 +238,17 @@ class TestInvoiceDiscounting(unittest.TestCase):
self.assertEqual(je2.accounts[1].credit_in_account_currency, flt(inv_disc.total_amount))
def test_make_payment_before_loan_period(self):
- #it has problem
+ # it has problem
inv = create_sales_invoice(rate=700)
- inv_disc = create_invoice_discounting([inv.name],
- accounts_receivable_credit=self.ar_credit,
- accounts_receivable_discounted=self.ar_discounted,
- accounts_receivable_unpaid=self.ar_unpaid,
- short_term_loan=self.short_term_loan,
- bank_charges_account=self.bank_charges_account,
- bank_account=self.bank_account
- )
+ inv_disc = create_invoice_discounting(
+ [inv.name],
+ accounts_receivable_credit=self.ar_credit,
+ accounts_receivable_discounted=self.ar_discounted,
+ accounts_receivable_unpaid=self.ar_unpaid,
+ short_term_loan=self.short_term_loan,
+ bank_charges_account=self.bank_charges_account,
+ bank_account=self.bank_account,
+ )
je = inv_disc.create_disbursement_entry()
inv_disc.reload()
je.posting_date = nowdate()
@@ -232,26 +262,31 @@ class TestInvoiceDiscounting(unittest.TestCase):
je_on_payment.submit()
self.assertEqual(je_on_payment.accounts[0].account, self.ar_discounted)
- self.assertEqual(je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount))
+ self.assertEqual(
+ je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)
+ )
self.assertEqual(je_on_payment.accounts[1].account, self.bank_account)
- self.assertEqual(je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount))
+ self.assertEqual(
+ je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)
+ )
inv.reload()
self.assertEqual(inv.outstanding_amount, 0)
def test_make_payment_before_after_period(self):
- #it has problem
+ # it has problem
inv = create_sales_invoice(rate=700)
- inv_disc = create_invoice_discounting([inv.name],
- accounts_receivable_credit=self.ar_credit,
- accounts_receivable_discounted=self.ar_discounted,
- accounts_receivable_unpaid=self.ar_unpaid,
- short_term_loan=self.short_term_loan,
- bank_charges_account=self.bank_charges_account,
- bank_account=self.bank_account,
- loan_start_date=add_days(nowdate(), -10),
- period=5
- )
+ inv_disc = create_invoice_discounting(
+ [inv.name],
+ accounts_receivable_credit=self.ar_credit,
+ accounts_receivable_discounted=self.ar_discounted,
+ accounts_receivable_unpaid=self.ar_unpaid,
+ short_term_loan=self.short_term_loan,
+ bank_charges_account=self.bank_charges_account,
+ bank_account=self.bank_account,
+ loan_start_date=add_days(nowdate(), -10),
+ period=5,
+ )
je = inv_disc.create_disbursement_entry()
inv_disc.reload()
je.posting_date = nowdate()
@@ -269,9 +304,13 @@ class TestInvoiceDiscounting(unittest.TestCase):
je_on_payment.submit()
self.assertEqual(je_on_payment.accounts[0].account, self.ar_unpaid)
- self.assertEqual(je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount))
+ self.assertEqual(
+ je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)
+ )
self.assertEqual(je_on_payment.accounts[1].account, self.bank_account)
- self.assertEqual(je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount))
+ self.assertEqual(
+ je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)
+ )
inv.reload()
self.assertEqual(inv.outstanding_amount, 0)
@@ -287,17 +326,15 @@ def create_invoice_discounting(invoices, **args):
inv_disc.accounts_receivable_credit = args.accounts_receivable_credit
inv_disc.accounts_receivable_discounted = args.accounts_receivable_discounted
inv_disc.accounts_receivable_unpaid = args.accounts_receivable_unpaid
- inv_disc.short_term_loan=args.short_term_loan
- inv_disc.bank_charges_account=args.bank_charges_account
- inv_disc.bank_account=args.bank_account
+ inv_disc.short_term_loan = args.short_term_loan
+ inv_disc.bank_charges_account = args.bank_charges_account
+ inv_disc.bank_account = args.bank_account
inv_disc.loan_start_date = args.start or nowdate()
inv_disc.loan_period = args.period or 30
inv_disc.bank_charges = flt(args.bank_charges)
for d in invoices:
- inv_disc.append("invoices", {
- "sales_invoice": d
- })
+ inv_disc.append("invoices", {"sales_invoice": d})
inv_disc.insert()
if not args.do_not_submit:
diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.py b/erpnext/accounts/doctype/item_tax_template/item_tax_template.py
index 0ceb6a0bc23..23f36ec6d8d 100644
--- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.py
+++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.py
@@ -13,20 +13,28 @@ class ItemTaxTemplate(Document):
def autoname(self):
if self.company and self.title:
- abbr = frappe.get_cached_value('Company', self.company, 'abbr')
- self.name = '{0} - {1}'.format(self.title, abbr)
+ abbr = frappe.get_cached_value("Company", self.company, "abbr")
+ self.name = "{0} - {1}".format(self.title, abbr)
def validate_tax_accounts(self):
"""Check whether Tax Rate is not entered twice for same Tax Type"""
check_list = []
- for d in self.get('taxes'):
+ for d in self.get("taxes"):
if d.tax_type:
account_type = frappe.db.get_value("Account", d.tax_type, "account_type")
- if account_type not in ['Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation']:
+ if account_type not in [
+ "Tax",
+ "Chargeable",
+ "Income Account",
+ "Expense Account",
+ "Expenses Included In Valuation",
+ ]:
frappe.throw(
- _("Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable").format(
- d.idx))
+ _(
+ "Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable"
+ ).format(d.idx)
+ )
else:
if d.tax_type in check_list:
frappe.throw(_("{0} entered twice in Item Tax").format(d.tax_type))
diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py b/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py
index 71177c25318..5a2bd720dd3 100644
--- a/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py
+++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py
@@ -1,26 +1,13 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'item_tax_template',
- 'transactions': [
- {
- 'label': _('Pre Sales'),
- 'items': ['Quotation', 'Supplier Quotation']
- },
- {
- 'label': _('Sales'),
- 'items': ['Sales Invoice', 'Sales Order', 'Delivery Note']
- },
- {
- 'label': _('Purchase'),
- 'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt']
- },
- {
- 'label': _('Stock'),
- 'items': ['Item Groups', 'Item']
- }
- ]
+ "fieldname": "item_tax_template",
+ "transactions": [
+ {"label": _("Pre Sales"), "items": ["Quotation", "Supplier Quotation"]},
+ {"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Delivery Note"]},
+ {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]},
+ {"label": _("Stock"), "items": ["Item Groups", "Item"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index 335fd350def..4493c722544 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -3,7 +3,7 @@
"allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
- "creation": "2013-03-25 10:53:52",
+ "creation": "2022-01-25 10:29:58.717206",
"doctype": "DocType",
"document_type": "Document",
"engine": "InnoDB",
@@ -13,6 +13,7 @@
"voucher_type",
"naming_series",
"finance_book",
+ "process_deferred_accounting",
"reversal_of",
"tax_withholding_category",
"column_break1",
@@ -524,13 +525,20 @@
"label": "Reversal Of",
"options": "Journal Entry",
"read_only": 1
+ },
+ {
+ "fieldname": "process_deferred_accounting",
+ "fieldtype": "Link",
+ "label": "Process Deferred Accounting",
+ "options": "Process Deferred Accounting",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 176,
"is_submittable": 1,
"links": [],
- "modified": "2022-01-04 13:39:36.485954",
+ "modified": "2022-04-06 17:18:46.865259",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",
@@ -578,6 +586,7 @@
"search_fields": "voucher_type,posting_date, due_date, cheque_no",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "title",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 9c1710217dd..8660c18bf95 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -19,7 +19,6 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
)
from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import (
- check_if_stock_and_account_balance_synced,
get_account_currency,
get_balance_on,
get_stock_accounts,
@@ -29,7 +28,9 @@ from erpnext.controllers.accounts_controller import AccountsController
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
-class StockAccountInvalidTransaction(frappe.ValidationError): pass
+class StockAccountInvalidTransaction(frappe.ValidationError):
+ pass
+
class JournalEntry(AccountsController):
def __init__(self, *args, **kwargs):
@@ -39,11 +40,11 @@ class JournalEntry(AccountsController):
return self.voucher_type
def validate(self):
- if self.voucher_type == 'Opening Entry':
- self.is_opening = 'Yes'
+ if self.voucher_type == "Opening Entry":
+ self.is_opening = "Yes"
if not self.is_opening:
- self.is_opening='No'
+ self.is_opening = "No"
self.clearance_date = None
@@ -86,15 +87,14 @@ class JournalEntry(AccountsController):
self.update_expense_claim()
self.update_inter_company_jv()
self.update_invoice_discounting()
- check_if_stock_and_account_balance_synced(self.posting_date,
- self.company, self.doctype, self.name)
def on_cancel(self):
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
from erpnext.payroll.doctype.salary_slip.salary_slip import unlink_ref_doc_from_salary_slip
+
unlink_ref_doc_from_payment_entries(self)
unlink_ref_doc_from_salary_slip(self.name)
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.make_gl_entries(1)
self.update_advance_paid()
self.update_expense_claim()
@@ -119,10 +119,13 @@ class JournalEntry(AccountsController):
frappe.get_doc(voucher_type, voucher_no).set_total_advance_paid()
def validate_inter_company_accounts(self):
- if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
+ if (
+ self.voucher_type == "Inter Company Journal Entry"
+ and self.inter_company_journal_entry_reference
+ ):
doc = frappe.get_doc("Journal Entry", self.inter_company_journal_entry_reference)
- account_currency = frappe.get_cached_value('Company', self.company, "default_currency")
- previous_account_currency = frappe.get_cached_value('Company', doc.company, "default_currency")
+ account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
+ previous_account_currency = frappe.get_cached_value("Company", doc.company, "default_currency")
if account_currency == previous_account_currency:
if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit:
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
@@ -130,45 +133,63 @@ class JournalEntry(AccountsController):
def validate_stock_accounts(self):
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
for account in stock_accounts:
- account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
- self.posting_date, self.company)
+ account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
+ account, self.posting_date, self.company
+ )
if account_bal == stock_bal:
- frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
- .format(account), StockAccountInvalidTransaction)
+ frappe.throw(
+ _("Account: {0} can only be updated via Stock Transactions").format(account),
+ StockAccountInvalidTransaction,
+ )
def apply_tax_withholding(self):
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
- if not self.apply_tds or self.voucher_type not in ('Debit Note', 'Credit Note'):
+ if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"):
return
- parties = [d.party for d in self.get('accounts') if d.party]
+ parties = [d.party for d in self.get("accounts") if d.party]
parties = list(set(parties))
if len(parties) > 1:
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
account_type_map = get_account_type_map(self.company)
- party_type = 'supplier' if self.voucher_type == 'Credit Note' else 'customer'
- doctype = 'Purchase Invoice' if self.voucher_type == 'Credit Note' else 'Sales Invoice'
- debit_or_credit = 'debit_in_account_currency' if self.voucher_type == 'Credit Note' else 'credit_in_account_currency'
- rev_debit_or_credit = 'credit_in_account_currency' if debit_or_credit == 'debit_in_account_currency' else 'debit_in_account_currency'
+ party_type = "supplier" if self.voucher_type == "Credit Note" else "customer"
+ doctype = "Purchase Invoice" if self.voucher_type == "Credit Note" else "Sales Invoice"
+ debit_or_credit = (
+ "debit_in_account_currency"
+ if self.voucher_type == "Credit Note"
+ else "credit_in_account_currency"
+ )
+ rev_debit_or_credit = (
+ "credit_in_account_currency"
+ if debit_or_credit == "debit_in_account_currency"
+ else "debit_in_account_currency"
+ )
party_account = get_party_account(party_type.title(), parties[0], self.company)
- net_total = sum(d.get(debit_or_credit) for d in self.get('accounts') if account_type_map.get(d.account)
- not in ('Tax', 'Chargeable'))
+ net_total = sum(
+ d.get(debit_or_credit)
+ for d in self.get("accounts")
+ if account_type_map.get(d.account) not in ("Tax", "Chargeable")
+ )
- party_amount = sum(d.get(rev_debit_or_credit) for d in self.get('accounts') if d.account == party_account)
+ party_amount = sum(
+ d.get(rev_debit_or_credit) for d in self.get("accounts") if d.account == party_account
+ )
- inv = frappe._dict({
- party_type: parties[0],
- 'doctype': doctype,
- 'company': self.company,
- 'posting_date': self.posting_date,
- 'net_total': net_total
- })
+ inv = frappe._dict(
+ {
+ party_type: parties[0],
+ "doctype": doctype,
+ "company": self.company,
+ "posting_date": self.posting_date,
+ "net_total": net_total,
+ }
+ )
tax_withholding_details = get_party_tax_withholding_details(inv, self.tax_withholding_category)
@@ -176,45 +197,64 @@ class JournalEntry(AccountsController):
return
accounts = []
- for d in self.get('accounts'):
- if d.get('account') == tax_withholding_details.get("account_head"):
- d.update({
- 'account': tax_withholding_details.get("account_head"),
- debit_or_credit: tax_withholding_details.get('tax_amount')
- })
+ for d in self.get("accounts"):
+ if d.get("account") == tax_withholding_details.get("account_head"):
+ d.update(
+ {
+ "account": tax_withholding_details.get("account_head"),
+ debit_or_credit: tax_withholding_details.get("tax_amount"),
+ }
+ )
- accounts.append(d.get('account'))
+ accounts.append(d.get("account"))
- if d.get('account') == party_account:
- d.update({
- rev_debit_or_credit: party_amount - tax_withholding_details.get('tax_amount')
- })
+ if d.get("account") == party_account:
+ d.update({rev_debit_or_credit: party_amount - tax_withholding_details.get("tax_amount")})
if not accounts or tax_withholding_details.get("account_head") not in accounts:
- self.append("accounts", {
- 'account': tax_withholding_details.get("account_head"),
- rev_debit_or_credit: tax_withholding_details.get('tax_amount'),
- 'against_account': parties[0]
- })
+ self.append(
+ "accounts",
+ {
+ "account": tax_withholding_details.get("account_head"),
+ rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
+ "against_account": parties[0],
+ },
+ )
- to_remove = [d for d in self.get('accounts')
- if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")]
+ to_remove = [
+ d
+ for d in self.get("accounts")
+ if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")
+ ]
for d in to_remove:
self.remove(d)
def update_inter_company_jv(self):
- if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
- frappe.db.set_value("Journal Entry", self.inter_company_journal_entry_reference,\
- "inter_company_journal_entry_reference", self.name)
+ if (
+ self.voucher_type == "Inter Company Journal Entry"
+ and self.inter_company_journal_entry_reference
+ ):
+ frappe.db.set_value(
+ "Journal Entry",
+ self.inter_company_journal_entry_reference,
+ "inter_company_journal_entry_reference",
+ self.name,
+ )
def update_invoice_discounting(self):
def _validate_invoice_discounting_status(inv_disc, id_status, expected_status, row_id):
id_link = get_link_to_form("Invoice Discounting", inv_disc)
if id_status != expected_status:
- frappe.throw(_("Row #{0}: Status must be {1} for Invoice Discounting {2}").format(d.idx, expected_status, id_link))
+ frappe.throw(
+ _("Row #{0}: Status must be {1} for Invoice Discounting {2}").format(
+ d.idx, expected_status, id_link
+ )
+ )
- invoice_discounting_list = list(set([d.reference_name for d in self.accounts if d.reference_type=="Invoice Discounting"]))
+ invoice_discounting_list = list(
+ set([d.reference_name for d in self.accounts if d.reference_type == "Invoice Discounting"])
+ )
for inv_disc in invoice_discounting_list:
inv_disc_doc = frappe.get_doc("Invoice Discounting", inv_disc)
status = None
@@ -238,104 +278,147 @@ class JournalEntry(AccountsController):
if status:
inv_disc_doc.set_status(status=status)
-
def unlink_advance_entry_reference(self):
for d in self.get("accounts"):
if d.is_advance == "Yes" and d.reference_type in ("Sales Invoice", "Purchase Invoice"):
doc = frappe.get_doc(d.reference_type, d.reference_name)
doc.delink_advance_entries(self.name)
- d.reference_type = ''
- d.reference_name = ''
+ d.reference_type = ""
+ d.reference_name = ""
d.db_update()
def unlink_asset_reference(self):
for d in self.get("accounts"):
- if d.reference_type=="Asset" and d.reference_name:
+ if d.reference_type == "Asset" and d.reference_name:
asset = frappe.get_doc("Asset", d.reference_name)
for s in asset.get("schedules"):
if s.journal_entry == self.name:
s.db_set("journal_entry", None)
idx = cint(s.finance_book_id) or 1
- finance_books = asset.get('finance_books')[idx - 1]
+ finance_books = asset.get("finance_books")[idx - 1]
finance_books.value_after_depreciation += s.depreciation_amount
finance_books.db_update()
asset.set_status()
def unlink_inter_company_jv(self):
- if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
- frappe.db.set_value("Journal Entry", self.inter_company_journal_entry_reference,\
- "inter_company_journal_entry_reference", "")
- frappe.db.set_value("Journal Entry", self.name,\
- "inter_company_journal_entry_reference", "")
+ if (
+ self.voucher_type == "Inter Company Journal Entry"
+ and self.inter_company_journal_entry_reference
+ ):
+ frappe.db.set_value(
+ "Journal Entry",
+ self.inter_company_journal_entry_reference,
+ "inter_company_journal_entry_reference",
+ "",
+ )
+ frappe.db.set_value("Journal Entry", self.name, "inter_company_journal_entry_reference", "")
def unlink_asset_adjustment_entry(self):
- frappe.db.sql(""" update `tabAsset Value Adjustment`
- set journal_entry = null where journal_entry = %s""", self.name)
+ frappe.db.sql(
+ """ update `tabAsset Value Adjustment`
+ set journal_entry = null where journal_entry = %s""",
+ self.name,
+ )
def validate_party(self):
for d in self.get("accounts"):
account_type = frappe.db.get_value("Account", d.account, "account_type")
if account_type in ["Receivable", "Payable"]:
if not (d.party_type and d.party):
- frappe.throw(_("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(d.idx, d.account))
+ frappe.throw(
+ _("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(
+ d.idx, d.account
+ )
+ )
def check_credit_limit(self):
- customers = list(set(d.party for d in self.get("accounts")
- if d.party_type=="Customer" and d.party and flt(d.debit) > 0))
+ customers = list(
+ set(
+ d.party
+ for d in self.get("accounts")
+ if d.party_type == "Customer" and d.party and flt(d.debit) > 0
+ )
+ )
if customers:
from erpnext.selling.doctype.customer.customer import check_credit_limit
+
for customer in customers:
check_credit_limit(customer, self.company)
def validate_cheque_info(self):
- if self.voucher_type in ['Bank Entry']:
+ if self.voucher_type in ["Bank Entry"]:
if not self.cheque_no or not self.cheque_date:
- msgprint(_("Reference No & Reference Date is required for {0}").format(self.voucher_type),
- raise_exception=1)
+ msgprint(
+ _("Reference No & Reference Date is required for {0}").format(self.voucher_type),
+ raise_exception=1,
+ )
if self.cheque_date and not self.cheque_no:
msgprint(_("Reference No is mandatory if you entered Reference Date"), raise_exception=1)
def validate_entries_for_advance(self):
- for d in self.get('accounts'):
+ for d in self.get("accounts"):
if d.reference_type not in ("Sales Invoice", "Purchase Invoice", "Journal Entry"):
- if (d.party_type == 'Customer' and flt(d.credit) > 0) or \
- (d.party_type == 'Supplier' and flt(d.debit) > 0):
- if d.is_advance=="No":
- msgprint(_("Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry.").format(d.idx, d.account), alert=True)
+ if (d.party_type == "Customer" and flt(d.credit) > 0) or (
+ d.party_type == "Supplier" and flt(d.debit) > 0
+ ):
+ if d.is_advance == "No":
+ msgprint(
+ _(
+ "Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry."
+ ).format(d.idx, d.account),
+ alert=True,
+ )
elif d.reference_type in ("Sales Order", "Purchase Order") and d.is_advance != "Yes":
- frappe.throw(_("Row {0}: Payment against Sales/Purchase Order should always be marked as advance").format(d.idx))
+ frappe.throw(
+ _(
+ "Row {0}: Payment against Sales/Purchase Order should always be marked as advance"
+ ).format(d.idx)
+ )
if d.is_advance == "Yes":
- if d.party_type == 'Customer' and flt(d.debit) > 0:
+ if d.party_type == "Customer" and flt(d.debit) > 0:
frappe.throw(_("Row {0}: Advance against Customer must be credit").format(d.idx))
- elif d.party_type == 'Supplier' and flt(d.credit) > 0:
+ elif d.party_type == "Supplier" and flt(d.credit) > 0:
frappe.throw(_("Row {0}: Advance against Supplier must be debit").format(d.idx))
def validate_against_jv(self):
- for d in self.get('accounts'):
- if d.reference_type=="Journal Entry":
+ for d in self.get("accounts"):
+ if d.reference_type == "Journal Entry":
account_root_type = frappe.db.get_value("Account", d.account, "root_type")
if account_root_type == "Asset" and flt(d.debit) > 0:
- frappe.throw(_("Row #{0}: For {1}, you can select reference document only if account gets credited")
- .format(d.idx, d.account))
+ frappe.throw(
+ _(
+ "Row #{0}: For {1}, you can select reference document only if account gets credited"
+ ).format(d.idx, d.account)
+ )
elif account_root_type == "Liability" and flt(d.credit) > 0:
- frappe.throw(_("Row #{0}: For {1}, you can select reference document only if account gets debited")
- .format(d.idx, d.account))
+ frappe.throw(
+ _(
+ "Row #{0}: For {1}, you can select reference document only if account gets debited"
+ ).format(d.idx, d.account)
+ )
if d.reference_name == self.name:
frappe.throw(_("You can not enter current voucher in 'Against Journal Entry' column"))
- against_entries = frappe.db.sql("""select * from `tabJournal Entry Account`
+ against_entries = frappe.db.sql(
+ """select * from `tabJournal Entry Account`
where account = %s and docstatus = 1 and parent = %s
and (reference_type is null or reference_type in ("", "Sales Order", "Purchase Order"))
- """, (d.account, d.reference_name), as_dict=True)
+ """,
+ (d.account, d.reference_name),
+ as_dict=True,
+ )
if not against_entries:
- frappe.throw(_("Journal Entry {0} does not have account {1} or already matched against other voucher")
- .format(d.reference_name, d.account))
+ frappe.throw(
+ _(
+ "Journal Entry {0} does not have account {1} or already matched against other voucher"
+ ).format(d.reference_name, d.account)
+ )
else:
dr_or_cr = "debit" if d.credit > 0 else "credit"
valid = False
@@ -343,16 +426,19 @@ class JournalEntry(AccountsController):
if flt(jvd[dr_or_cr]) > 0:
valid = True
if not valid:
- frappe.throw(_("Against Journal Entry {0} does not have any unmatched {1} entry")
- .format(d.reference_name, dr_or_cr))
+ frappe.throw(
+ _("Against Journal Entry {0} does not have any unmatched {1} entry").format(
+ d.reference_name, dr_or_cr
+ )
+ )
def validate_reference_doc(self):
"""Validates reference document"""
field_dict = {
- 'Sales Invoice': ["Customer", "Debit To"],
- 'Purchase Invoice': ["Supplier", "Credit To"],
- 'Sales Order': ["Customer"],
- 'Purchase Order': ["Supplier"]
+ "Sales Invoice": ["Customer", "Debit To"],
+ "Purchase Invoice": ["Supplier", "Credit To"],
+ "Sales Order": ["Customer"],
+ "Purchase Order": ["Supplier"],
}
self.reference_totals = {}
@@ -365,56 +451,76 @@ class JournalEntry(AccountsController):
if not d.reference_name:
d.reference_type = None
if d.reference_type and d.reference_name and (d.reference_type in list(field_dict)):
- dr_or_cr = "credit_in_account_currency" \
- if d.reference_type in ("Sales Order", "Sales Invoice") else "debit_in_account_currency"
+ dr_or_cr = (
+ "credit_in_account_currency"
+ if d.reference_type in ("Sales Order", "Sales Invoice")
+ else "debit_in_account_currency"
+ )
# check debit or credit type Sales / Purchase Order
- if d.reference_type=="Sales Order" and flt(d.debit) > 0:
- frappe.throw(_("Row {0}: Debit entry can not be linked with a {1}").format(d.idx, d.reference_type))
+ if d.reference_type == "Sales Order" and flt(d.debit) > 0:
+ frappe.throw(
+ _("Row {0}: Debit entry can not be linked with a {1}").format(d.idx, d.reference_type)
+ )
if d.reference_type == "Purchase Order" and flt(d.credit) > 0:
- frappe.throw(_("Row {0}: Credit entry can not be linked with a {1}").format(d.idx, d.reference_type))
+ frappe.throw(
+ _("Row {0}: Credit entry can not be linked with a {1}").format(d.idx, d.reference_type)
+ )
# set totals
if not d.reference_name in self.reference_totals:
self.reference_totals[d.reference_name] = 0.0
- if self.voucher_type not in ('Deferred Revenue', 'Deferred Expense'):
+ if self.voucher_type not in ("Deferred Revenue", "Deferred Expense"):
self.reference_totals[d.reference_name] += flt(d.get(dr_or_cr))
self.reference_types[d.reference_name] = d.reference_type
self.reference_accounts[d.reference_name] = d.account
- against_voucher = frappe.db.get_value(d.reference_type, d.reference_name,
- [scrub(dt) for dt in field_dict.get(d.reference_type)])
+ against_voucher = frappe.db.get_value(
+ d.reference_type, d.reference_name, [scrub(dt) for dt in field_dict.get(d.reference_type)]
+ )
if not against_voucher:
frappe.throw(_("Row {0}: Invalid reference {1}").format(d.idx, d.reference_name))
# check if party and account match
if d.reference_type in ("Sales Invoice", "Purchase Invoice"):
- if self.voucher_type in ('Deferred Revenue', 'Deferred Expense') and d.reference_detail_no:
- debit_or_credit = 'Debit' if d.debit else 'Credit'
- party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no,
- debit_or_credit)
- against_voucher = ['', against_voucher[1]]
+ if self.voucher_type in ("Deferred Revenue", "Deferred Expense") and d.reference_detail_no:
+ debit_or_credit = "Debit" if d.debit else "Credit"
+ party_account = get_deferred_booking_accounts(
+ d.reference_type, d.reference_detail_no, debit_or_credit
+ )
+ against_voucher = ["", against_voucher[1]]
else:
if d.reference_type == "Sales Invoice":
- party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1]
+ party_account = (
+ get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1]
+ )
else:
party_account = against_voucher[1]
- if (against_voucher[0] != cstr(d.party) or party_account != d.account):
- frappe.throw(_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}")
- .format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1],
- d.reference_type, d.reference_name))
+ if against_voucher[0] != cstr(d.party) or party_account != d.account:
+ frappe.throw(
+ _("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}").format(
+ d.idx,
+ field_dict.get(d.reference_type)[0],
+ field_dict.get(d.reference_type)[1],
+ d.reference_type,
+ d.reference_name,
+ )
+ )
# check if party matches for Sales / Purchase Order
if d.reference_type in ("Sales Order", "Purchase Order"):
# set totals
if against_voucher != d.party:
- frappe.throw(_("Row {0}: {1} {2} does not match with {3}") \
- .format(d.idx, d.party_type, d.party, d.reference_type))
+ frappe.throw(
+ _("Row {0}: {1} {2} does not match with {3}").format(
+ d.idx, d.party_type, d.party, d.reference_type
+ )
+ )
self.validate_orders()
self.validate_invoices()
@@ -440,62 +546,79 @@ class JournalEntry(AccountsController):
account_currency = get_account_currency(account)
if account_currency == self.company_currency:
voucher_total = order.base_grand_total
- formatted_voucher_total = fmt_money(voucher_total, order.precision("base_grand_total"),
- currency=account_currency)
+ formatted_voucher_total = fmt_money(
+ voucher_total, order.precision("base_grand_total"), currency=account_currency
+ )
else:
voucher_total = order.grand_total
- formatted_voucher_total = fmt_money(voucher_total, order.precision("grand_total"),
- currency=account_currency)
+ formatted_voucher_total = fmt_money(
+ voucher_total, order.precision("grand_total"), currency=account_currency
+ )
if flt(voucher_total) < (flt(order.advance_paid) + total):
- frappe.throw(_("Advance paid against {0} {1} cannot be greater than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total))
+ frappe.throw(
+ _("Advance paid against {0} {1} cannot be greater than Grand Total {2}").format(
+ reference_type, reference_name, formatted_voucher_total
+ )
+ )
def validate_invoices(self):
"""Validate totals and docstatus for invoices"""
for reference_name, total in iteritems(self.reference_totals):
reference_type = self.reference_types[reference_name]
- if (reference_type in ("Sales Invoice", "Purchase Invoice") and
- self.voucher_type not in ['Debit Note', 'Credit Note']):
- invoice = frappe.db.get_value(reference_type, reference_name,
- ["docstatus", "outstanding_amount"], as_dict=1)
+ if reference_type in ("Sales Invoice", "Purchase Invoice") and self.voucher_type not in [
+ "Debit Note",
+ "Credit Note",
+ ]:
+ invoice = frappe.db.get_value(
+ reference_type, reference_name, ["docstatus", "outstanding_amount"], as_dict=1
+ )
if invoice.docstatus != 1:
frappe.throw(_("{0} {1} is not submitted").format(reference_type, reference_name))
if total and flt(invoice.outstanding_amount) < total:
- frappe.throw(_("Payment against {0} {1} cannot be greater than Outstanding Amount {2}")
- .format(reference_type, reference_name, invoice.outstanding_amount))
+ frappe.throw(
+ _("Payment against {0} {1} cannot be greater than Outstanding Amount {2}").format(
+ reference_type, reference_name, invoice.outstanding_amount
+ )
+ )
def set_against_account(self):
accounts_debited, accounts_credited = [], []
- if self.voucher_type in ('Deferred Revenue', 'Deferred Expense'):
- for d in self.get('accounts'):
- if d.reference_type == 'Sales Invoice':
- field = 'customer'
+ if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
+ for d in self.get("accounts"):
+ if d.reference_type == "Sales Invoice":
+ field = "customer"
else:
- field = 'supplier'
+ field = "supplier"
d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
else:
for d in self.get("accounts"):
- if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
- if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
+ if flt(d.debit > 0):
+ accounts_debited.append(d.party or d.account)
+ if flt(d.credit) > 0:
+ accounts_credited.append(d.party or d.account)
for d in self.get("accounts"):
- if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
- if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
+ if flt(d.debit > 0):
+ d.against_account = ", ".join(list(set(accounts_credited)))
+ if flt(d.credit > 0):
+ d.against_account = ", ".join(list(set(accounts_debited)))
def validate_debit_credit_amount(self):
- for d in self.get('accounts'):
+ for d in self.get("accounts"):
if not flt(d.debit) and not flt(d.credit):
frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
def validate_total_debit_and_credit(self):
self.set_total_debit_credit()
if self.difference:
- frappe.throw(_("Total Debit must be equal to Total Credit. The difference is {0}")
- .format(self.difference))
+ frappe.throw(
+ _("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference)
+ )
def set_total_debit_credit(self):
self.total_debit, self.total_credit, self.difference = 0, 0, 0
@@ -506,13 +629,16 @@ class JournalEntry(AccountsController):
self.total_debit = flt(self.total_debit) + flt(d.debit, d.precision("debit"))
self.total_credit = flt(self.total_credit) + flt(d.credit, d.precision("credit"))
- self.difference = flt(self.total_debit, self.precision("total_debit")) - \
- flt(self.total_credit, self.precision("total_credit"))
+ self.difference = flt(self.total_debit, self.precision("total_debit")) - flt(
+ self.total_credit, self.precision("total_credit")
+ )
def validate_multi_currency(self):
alternate_currency = []
for d in self.get("accounts"):
- account = frappe.db.get_value("Account", d.account, ["account_currency", "account_type"], as_dict=1)
+ account = frappe.db.get_value(
+ "Account", d.account, ["account_currency", "account_type"], as_dict=1
+ )
if account:
d.account_currency = account.account_currency
d.account_type = account.account_type
@@ -531,8 +657,12 @@ class JournalEntry(AccountsController):
def set_amounts_in_company_currency(self):
for d in self.get("accounts"):
- d.debit_in_account_currency = flt(d.debit_in_account_currency, d.precision("debit_in_account_currency"))
- d.credit_in_account_currency = flt(d.credit_in_account_currency, d.precision("credit_in_account_currency"))
+ d.debit_in_account_currency = flt(
+ d.debit_in_account_currency, d.precision("debit_in_account_currency")
+ )
+ d.credit_in_account_currency = flt(
+ d.credit_in_account_currency, d.precision("credit_in_account_currency")
+ )
d.debit = flt(d.debit_in_account_currency * flt(d.exchange_rate), d.precision("debit"))
d.credit = flt(d.credit_in_account_currency * flt(d.exchange_rate), d.precision("credit"))
@@ -541,13 +671,28 @@ class JournalEntry(AccountsController):
for d in self.get("accounts"):
if d.account_currency == self.company_currency:
d.exchange_rate = 1
- elif not d.exchange_rate or d.exchange_rate == 1 or \
- (d.reference_type in ("Sales Invoice", "Purchase Invoice")
- and d.reference_name and self.posting_date):
+ elif (
+ not d.exchange_rate
+ or d.exchange_rate == 1
+ or (
+ d.reference_type in ("Sales Invoice", "Purchase Invoice")
+ and d.reference_name
+ and self.posting_date
+ )
+ ):
- # Modified to include the posting date for which to retreive the exchange rate
- d.exchange_rate = get_exchange_rate(self.posting_date, d.account, d.account_currency,
- self.company, d.reference_type, d.reference_name, d.debit, d.credit, d.exchange_rate)
+ # Modified to include the posting date for which to retreive the exchange rate
+ d.exchange_rate = get_exchange_rate(
+ self.posting_date,
+ d.account,
+ d.account_currency,
+ self.company,
+ d.reference_type,
+ d.reference_name,
+ d.debit,
+ d.credit,
+ d.exchange_rate,
+ )
if not d.exchange_rate:
frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx))
@@ -560,55 +705,76 @@ class JournalEntry(AccountsController):
if self.cheque_no:
if self.cheque_date:
- r.append(_('Reference #{0} dated {1}').format(self.cheque_no, formatdate(self.cheque_date)))
+ r.append(_("Reference #{0} dated {1}").format(self.cheque_no, formatdate(self.cheque_date)))
else:
msgprint(_("Please enter Reference date"), raise_exception=frappe.MandatoryError)
- for d in self.get('accounts'):
- if d.reference_type=="Sales Invoice" and d.credit:
- r.append(_("{0} against Sales Invoice {1}").format(fmt_money(flt(d.credit), currency = self.company_currency), \
- d.reference_name))
+ for d in self.get("accounts"):
+ if d.reference_type == "Sales Invoice" and d.credit:
+ r.append(
+ _("{0} against Sales Invoice {1}").format(
+ fmt_money(flt(d.credit), currency=self.company_currency), d.reference_name
+ )
+ )
- if d.reference_type=="Sales Order" and d.credit:
- r.append(_("{0} against Sales Order {1}").format(fmt_money(flt(d.credit), currency = self.company_currency), \
- d.reference_name))
+ if d.reference_type == "Sales Order" and d.credit:
+ r.append(
+ _("{0} against Sales Order {1}").format(
+ fmt_money(flt(d.credit), currency=self.company_currency), d.reference_name
+ )
+ )
if d.reference_type == "Purchase Invoice" and d.debit:
- bill_no = frappe.db.sql("""select bill_no, bill_date
- from `tabPurchase Invoice` where name=%s""", d.reference_name)
- if bill_no and bill_no[0][0] and bill_no[0][0].lower().strip() \
- not in ['na', 'not applicable', 'none']:
- r.append(_('{0} against Bill {1} dated {2}').format(fmt_money(flt(d.debit), currency=self.company_currency), bill_no[0][0],
- bill_no[0][1] and formatdate(bill_no[0][1].strftime('%Y-%m-%d'))))
+ bill_no = frappe.db.sql(
+ """select bill_no, bill_date
+ from `tabPurchase Invoice` where name=%s""",
+ d.reference_name,
+ )
+ if (
+ bill_no
+ and bill_no[0][0]
+ and bill_no[0][0].lower().strip() not in ["na", "not applicable", "none"]
+ ):
+ r.append(
+ _("{0} against Bill {1} dated {2}").format(
+ fmt_money(flt(d.debit), currency=self.company_currency),
+ bill_no[0][0],
+ bill_no[0][1] and formatdate(bill_no[0][1].strftime("%Y-%m-%d")),
+ )
+ )
if d.reference_type == "Purchase Order" and d.debit:
- r.append(_("{0} against Purchase Order {1}").format(fmt_money(flt(d.credit), currency = self.company_currency), \
- d.reference_name))
+ r.append(
+ _("{0} against Purchase Order {1}").format(
+ fmt_money(flt(d.credit), currency=self.company_currency), d.reference_name
+ )
+ )
if r:
- self.remark = ("\n").join(r) #User Remarks is not mandatory
+ self.remark = ("\n").join(r) # User Remarks is not mandatory
def set_print_format_fields(self):
bank_amount = party_amount = total_amount = 0.0
- currency = bank_account_currency = party_account_currency = pay_to_recd_from= None
+ currency = bank_account_currency = party_account_currency = pay_to_recd_from = None
party_type = None
- for d in self.get('accounts'):
- if d.party_type in ['Customer', 'Supplier'] and d.party:
+ for d in self.get("accounts"):
+ if d.party_type in ["Customer", "Supplier"] and d.party:
party_type = d.party_type
if not pay_to_recd_from:
pay_to_recd_from = d.party
if pay_to_recd_from and pay_to_recd_from == d.party:
- party_amount += (d.debit_in_account_currency or d.credit_in_account_currency)
+ party_amount += d.debit_in_account_currency or d.credit_in_account_currency
party_account_currency = d.account_currency
elif frappe.db.get_value("Account", d.account, "account_type") in ["Bank", "Cash"]:
- bank_amount += (d.debit_in_account_currency or d.credit_in_account_currency)
+ bank_amount += d.debit_in_account_currency or d.credit_in_account_currency
bank_account_currency = d.account_currency
if party_type and pay_to_recd_from:
- self.pay_to_recd_from = frappe.db.get_value(party_type, pay_to_recd_from,
- "customer_name" if party_type=="Customer" else "supplier_name")
+ self.pay_to_recd_from = frappe.db.get_value(
+ party_type, pay_to_recd_from, "customer_name" if party_type == "Customer" else "supplier_name"
+ )
if bank_amount:
total_amount = bank_amount
currency = bank_account_currency
@@ -622,6 +788,7 @@ class JournalEntry(AccountsController):
self.total_amount = amt
self.total_amount_currency = currency
from frappe.utils import money_in_words
+
self.total_amount_in_words = money_in_words(amt, currency)
def make_gl_entries(self, cancel=0, adv_adj=0):
@@ -635,38 +802,45 @@ class JournalEntry(AccountsController):
remarks = "\n".join(r)
gl_map.append(
- self.get_gl_dict({
- "account": d.account,
- "party_type": d.party_type,
- "due_date": self.due_date,
- "party": d.party,
- "against": d.against_account,
- "debit": flt(d.debit, d.precision("debit")),
- "credit": flt(d.credit, d.precision("credit")),
- "account_currency": d.account_currency,
- "debit_in_account_currency": flt(d.debit_in_account_currency, d.precision("debit_in_account_currency")),
- "credit_in_account_currency": flt(d.credit_in_account_currency, d.precision("credit_in_account_currency")),
- "against_voucher_type": d.reference_type,
- "against_voucher": d.reference_name,
- "remarks": remarks,
- "voucher_detail_no": d.reference_detail_no,
- "cost_center": d.cost_center,
- "project": d.project,
- "finance_book": self.finance_book
- }, item=d)
+ self.get_gl_dict(
+ {
+ "account": d.account,
+ "party_type": d.party_type,
+ "due_date": self.due_date,
+ "party": d.party,
+ "against": d.against_account,
+ "debit": flt(d.debit, d.precision("debit")),
+ "credit": flt(d.credit, d.precision("credit")),
+ "account_currency": d.account_currency,
+ "debit_in_account_currency": flt(
+ d.debit_in_account_currency, d.precision("debit_in_account_currency")
+ ),
+ "credit_in_account_currency": flt(
+ d.credit_in_account_currency, d.precision("credit_in_account_currency")
+ ),
+ "against_voucher_type": d.reference_type,
+ "against_voucher": d.reference_name,
+ "remarks": remarks,
+ "voucher_detail_no": d.reference_detail_no,
+ "cost_center": d.cost_center,
+ "project": d.project,
+ "finance_book": self.finance_book,
+ },
+ item=d,
+ )
)
- if self.voucher_type in ('Deferred Revenue', 'Deferred Expense'):
- update_outstanding = 'No'
+ if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
+ update_outstanding = "No"
else:
- update_outstanding = 'Yes'
+ update_outstanding = "Yes"
if gl_map:
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
@frappe.whitelist()
def get_balance(self):
- if not self.get('accounts'):
+ if not self.get("accounts"):
msgprint(_("'Entries' cannot be empty"), raise_exception=True)
else:
self.total_debit, self.total_credit = 0, 0
@@ -675,18 +849,18 @@ class JournalEntry(AccountsController):
# If any row without amount, set the diff on that row
if diff:
blank_row = None
- for d in self.get('accounts'):
+ for d in self.get("accounts"):
if not d.credit_in_account_currency and not d.debit_in_account_currency and diff != 0:
blank_row = d
if not blank_row:
- blank_row = self.append('accounts', {})
+ blank_row = self.append("accounts", {})
blank_row.exchange_rate = 1
- if diff>0:
+ if diff > 0:
blank_row.credit_in_account_currency = diff
blank_row.credit = diff
- elif diff<0:
+ elif diff < 0:
blank_row.debit_in_account_currency = abs(diff)
blank_row.debit = abs(diff)
@@ -694,76 +868,100 @@ class JournalEntry(AccountsController):
@frappe.whitelist()
def get_outstanding_invoices(self):
- self.set('accounts', [])
+ self.set("accounts", [])
total = 0
for d in self.get_values():
total += flt(d.outstanding_amount, self.precision("credit", "accounts"))
- jd1 = self.append('accounts', {})
+ jd1 = self.append("accounts", {})
jd1.account = d.account
jd1.party = d.party
- if self.write_off_based_on == 'Accounts Receivable':
+ if self.write_off_based_on == "Accounts Receivable":
jd1.party_type = "Customer"
- jd1.credit_in_account_currency = flt(d.outstanding_amount, self.precision("credit", "accounts"))
+ jd1.credit_in_account_currency = flt(
+ d.outstanding_amount, self.precision("credit", "accounts")
+ )
jd1.reference_type = "Sales Invoice"
jd1.reference_name = cstr(d.name)
- elif self.write_off_based_on == 'Accounts Payable':
+ elif self.write_off_based_on == "Accounts Payable":
jd1.party_type = "Supplier"
jd1.debit_in_account_currency = flt(d.outstanding_amount, self.precision("debit", "accounts"))
jd1.reference_type = "Purchase Invoice"
jd1.reference_name = cstr(d.name)
- jd2 = self.append('accounts', {})
- if self.write_off_based_on == 'Accounts Receivable':
+ jd2 = self.append("accounts", {})
+ if self.write_off_based_on == "Accounts Receivable":
jd2.debit_in_account_currency = total
- elif self.write_off_based_on == 'Accounts Payable':
+ elif self.write_off_based_on == "Accounts Payable":
jd2.credit_in_account_currency = total
self.validate_total_debit_and_credit()
-
def get_values(self):
- cond = " and outstanding_amount <= {0}".format(self.write_off_amount) \
- if flt(self.write_off_amount) > 0 else ""
+ cond = (
+ " and outstanding_amount <= {0}".format(self.write_off_amount)
+ if flt(self.write_off_amount) > 0
+ else ""
+ )
- if self.write_off_based_on == 'Accounts Receivable':
- return frappe.db.sql("""select name, debit_to as account, customer as party, outstanding_amount
+ if self.write_off_based_on == "Accounts Receivable":
+ return frappe.db.sql(
+ """select name, debit_to as account, customer as party, outstanding_amount
from `tabSales Invoice` where docstatus = 1 and company = %s
- and outstanding_amount > 0 %s""" % ('%s', cond), self.company, as_dict=True)
- elif self.write_off_based_on == 'Accounts Payable':
- return frappe.db.sql("""select name, credit_to as account, supplier as party, outstanding_amount
+ and outstanding_amount > 0 %s"""
+ % ("%s", cond),
+ self.company,
+ as_dict=True,
+ )
+ elif self.write_off_based_on == "Accounts Payable":
+ return frappe.db.sql(
+ """select name, credit_to as account, supplier as party, outstanding_amount
from `tabPurchase Invoice` where docstatus = 1 and company = %s
- and outstanding_amount > 0 %s""" % ('%s', cond), self.company, as_dict=True)
+ and outstanding_amount > 0 %s"""
+ % ("%s", cond),
+ self.company,
+ as_dict=True,
+ )
def update_expense_claim(self):
for d in self.accounts:
- if d.reference_type=="Expense Claim" and d.reference_name:
+ if d.reference_type == "Expense Claim" and d.reference_name:
doc = frappe.get_doc("Expense Claim", d.reference_name)
if self.docstatus == 2:
update_reimbursed_amount(doc, -1 * d.debit)
else:
update_reimbursed_amount(doc, d.debit)
-
def validate_expense_claim(self):
for d in self.accounts:
- if d.reference_type=="Expense Claim":
- sanctioned_amount, reimbursed_amount = frappe.db.get_value("Expense Claim",
- d.reference_name, ("total_sanctioned_amount", "total_amount_reimbursed"))
+ if d.reference_type == "Expense Claim":
+ sanctioned_amount, reimbursed_amount = frappe.db.get_value(
+ "Expense Claim", d.reference_name, ("total_sanctioned_amount", "total_amount_reimbursed")
+ )
pending_amount = flt(sanctioned_amount) - flt(reimbursed_amount)
if d.debit > pending_amount:
- frappe.throw(_("Row No {0}: Amount cannot be greater than Pending Amount against Expense Claim {1}. Pending Amount is {2}").format(d.idx, d.reference_name, pending_amount))
+ frappe.throw(
+ _(
+ "Row No {0}: Amount cannot be greater than Pending Amount against Expense Claim {1}. Pending Amount is {2}"
+ ).format(d.idx, d.reference_name, pending_amount)
+ )
def validate_credit_debit_note(self):
if self.stock_entry:
if frappe.db.get_value("Stock Entry", self.stock_entry, "docstatus") != 1:
frappe.throw(_("Stock Entry {0} is not submitted").format(self.stock_entry))
- if frappe.db.exists({"doctype": "Journal Entry", "stock_entry": self.stock_entry, "docstatus":1}):
- frappe.msgprint(_("Warning: Another {0} # {1} exists against stock entry {2}").format(self.voucher_type, self.name, self.stock_entry))
+ if frappe.db.exists(
+ {"doctype": "Journal Entry", "stock_entry": self.stock_entry, "docstatus": 1}
+ ):
+ frappe.msgprint(
+ _("Warning: Another {0} # {1} exists against stock entry {2}").format(
+ self.voucher_type, self.name, self.stock_entry
+ )
+ )
def validate_empty_accounts_table(self):
- if not self.get('accounts'):
+ if not self.get("accounts"):
frappe.throw(_("Accounts table cannot be blank."))
def set_account_and_party_balance(self):
@@ -774,54 +972,66 @@ class JournalEntry(AccountsController):
account_balance[d.account] = get_balance_on(account=d.account, date=self.posting_date)
if (d.party_type, d.party) not in party_balance:
- party_balance[(d.party_type, d.party)] = get_balance_on(party_type=d.party_type,
- party=d.party, date=self.posting_date, company=self.company)
+ party_balance[(d.party_type, d.party)] = get_balance_on(
+ party_type=d.party_type, party=d.party, date=self.posting_date, company=self.company
+ )
d.account_balance = account_balance[d.account]
d.party_balance = party_balance[(d.party_type, d.party)]
+
@frappe.whitelist()
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
+
if mode_of_payment:
account = get_bank_cash_account(mode_of_payment, company).get("account")
if not account:
- '''
- Set the default account first. If the user hasn't set any default account then, he doesn't
- want us to set any random account. In this case set the account only if there is single
- account (of that type), otherwise return empty dict.
- '''
- if account_type=="Bank":
- account = frappe.get_cached_value('Company', company, "default_bank_account")
+ """
+ Set the default account first. If the user hasn't set any default account then, he doesn't
+ want us to set any random account. In this case set the account only if there is single
+ account (of that type), otherwise return empty dict.
+ """
+ if account_type == "Bank":
+ account = frappe.get_cached_value("Company", company, "default_bank_account")
if not account:
- account_list = frappe.get_all("Account", filters = {"company": company,
- "account_type": "Bank", "is_group": 0})
+ account_list = frappe.get_all(
+ "Account", filters={"company": company, "account_type": "Bank", "is_group": 0}
+ )
if len(account_list) == 1:
account = account_list[0].name
- elif account_type=="Cash":
- account = frappe.get_cached_value('Company', company, "default_cash_account")
+ elif account_type == "Cash":
+ account = frappe.get_cached_value("Company", company, "default_cash_account")
if not account:
- account_list = frappe.get_all("Account", filters = {"company": company,
- "account_type": "Cash", "is_group": 0})
+ account_list = frappe.get_all(
+ "Account", filters={"company": company, "account_type": "Cash", "is_group": 0}
+ )
if len(account_list) == 1:
account = account_list[0].name
if account:
- account_details = frappe.db.get_value("Account", account,
- ["account_currency", "account_type"], as_dict=1)
+ account_details = frappe.db.get_value(
+ "Account", account, ["account_currency", "account_type"], as_dict=1
+ )
+
+ return frappe._dict(
+ {
+ "account": account,
+ "balance": get_balance_on(account),
+ "account_currency": account_details.account_currency,
+ "account_type": account_details.account_type,
+ }
+ )
+ else:
+ return frappe._dict()
- return frappe._dict({
- "account": account,
- "balance": get_balance_on(account),
- "account_currency": account_details.account_currency,
- "account_type": account_details.account_type
- })
- else: return frappe._dict()
@frappe.whitelist()
-def get_payment_entry_against_order(dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None):
+def get_payment_entry_against_order(
+ dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None
+):
ref_doc = frappe.get_doc(dt, dn)
if flt(ref_doc.per_billed, 2) > 0:
@@ -845,22 +1055,28 @@ def get_payment_entry_against_order(dt, dn, amount=None, debit_in_account_curren
else:
amount = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
- return get_payment_entry(ref_doc, {
- "party_type": party_type,
- "party_account": party_account,
- "party_account_currency": party_account_currency,
- "amount_field_party": amount_field_party,
- "amount_field_bank": amount_field_bank,
- "amount": amount,
- "debit_in_account_currency": debit_in_account_currency,
- "remarks": 'Advance Payment received against {0} {1}'.format(dt, dn),
- "is_advance": "Yes",
- "bank_account": bank_account,
- "journal_entry": journal_entry
- })
+ return get_payment_entry(
+ ref_doc,
+ {
+ "party_type": party_type,
+ "party_account": party_account,
+ "party_account_currency": party_account_currency,
+ "amount_field_party": amount_field_party,
+ "amount_field_bank": amount_field_bank,
+ "amount": amount,
+ "debit_in_account_currency": debit_in_account_currency,
+ "remarks": "Advance Payment received against {0} {1}".format(dt, dn),
+ "is_advance": "Yes",
+ "bank_account": bank_account,
+ "journal_entry": journal_entry,
+ },
+ )
+
@frappe.whitelist()
-def get_payment_entry_against_invoice(dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None):
+def get_payment_entry_against_invoice(
+ dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None
+):
ref_doc = frappe.get_doc(dt, dn)
if dt == "Sales Invoice":
party_type = "Customer"
@@ -869,73 +1085,91 @@ def get_payment_entry_against_invoice(dt, dn, amount=None, debit_in_account_cur
party_type = "Supplier"
party_account = ref_doc.credit_to
- if (dt == "Sales Invoice" and ref_doc.outstanding_amount > 0) \
- or (dt == "Purchase Invoice" and ref_doc.outstanding_amount < 0):
- amount_field_party = "credit_in_account_currency"
- amount_field_bank = "debit_in_account_currency"
+ if (dt == "Sales Invoice" and ref_doc.outstanding_amount > 0) or (
+ dt == "Purchase Invoice" and ref_doc.outstanding_amount < 0
+ ):
+ amount_field_party = "credit_in_account_currency"
+ amount_field_bank = "debit_in_account_currency"
else:
amount_field_party = "debit_in_account_currency"
amount_field_bank = "credit_in_account_currency"
- return get_payment_entry(ref_doc, {
- "party_type": party_type,
- "party_account": party_account,
- "party_account_currency": ref_doc.party_account_currency,
- "amount_field_party": amount_field_party,
- "amount_field_bank": amount_field_bank,
- "amount": amount if amount else abs(ref_doc.outstanding_amount),
- "debit_in_account_currency": debit_in_account_currency,
- "remarks": 'Payment received against {0} {1}. {2}'.format(dt, dn, ref_doc.remarks),
- "is_advance": "No",
- "bank_account": bank_account,
- "journal_entry": journal_entry
- })
+ return get_payment_entry(
+ ref_doc,
+ {
+ "party_type": party_type,
+ "party_account": party_account,
+ "party_account_currency": ref_doc.party_account_currency,
+ "amount_field_party": amount_field_party,
+ "amount_field_bank": amount_field_bank,
+ "amount": amount if amount else abs(ref_doc.outstanding_amount),
+ "debit_in_account_currency": debit_in_account_currency,
+ "remarks": "Payment received against {0} {1}. {2}".format(dt, dn, ref_doc.remarks),
+ "is_advance": "No",
+ "bank_account": bank_account,
+ "journal_entry": journal_entry,
+ },
+ )
+
def get_payment_entry(ref_doc, args):
- cost_center = ref_doc.get("cost_center") or frappe.get_cached_value('Company', ref_doc.company, "cost_center")
+ cost_center = ref_doc.get("cost_center") or frappe.get_cached_value(
+ "Company", ref_doc.company, "cost_center"
+ )
exchange_rate = 1
if args.get("party_account"):
# Modified to include the posting date for which the exchange rate is required.
# Assumed to be the posting date in the reference document
- exchange_rate = get_exchange_rate(ref_doc.get("posting_date") or ref_doc.get("transaction_date"),
- args.get("party_account"), args.get("party_account_currency"),
- ref_doc.company, ref_doc.doctype, ref_doc.name)
+ exchange_rate = get_exchange_rate(
+ ref_doc.get("posting_date") or ref_doc.get("transaction_date"),
+ args.get("party_account"),
+ args.get("party_account_currency"),
+ ref_doc.company,
+ ref_doc.doctype,
+ ref_doc.name,
+ )
je = frappe.new_doc("Journal Entry")
- je.update({
- "voucher_type": "Bank Entry",
- "company": ref_doc.company,
- "remark": args.get("remarks")
- })
+ je.update(
+ {"voucher_type": "Bank Entry", "company": ref_doc.company, "remark": args.get("remarks")}
+ )
- party_row = je.append("accounts", {
- "account": args.get("party_account"),
- "party_type": args.get("party_type"),
- "party": ref_doc.get(args.get("party_type").lower()),
- "cost_center": cost_center,
- "account_type": frappe.db.get_value("Account", args.get("party_account"), "account_type"),
- "account_currency": args.get("party_account_currency") or \
- get_account_currency(args.get("party_account")),
- "balance": get_balance_on(args.get("party_account")),
- "party_balance": get_balance_on(party=args.get("party"), party_type=args.get("party_type")),
- "exchange_rate": exchange_rate,
- args.get("amount_field_party"): args.get("amount"),
- "is_advance": args.get("is_advance"),
- "reference_type": ref_doc.doctype,
- "reference_name": ref_doc.name
- })
+ party_row = je.append(
+ "accounts",
+ {
+ "account": args.get("party_account"),
+ "party_type": args.get("party_type"),
+ "party": ref_doc.get(args.get("party_type").lower()),
+ "cost_center": cost_center,
+ "account_type": frappe.db.get_value("Account", args.get("party_account"), "account_type"),
+ "account_currency": args.get("party_account_currency")
+ or get_account_currency(args.get("party_account")),
+ "balance": get_balance_on(args.get("party_account")),
+ "party_balance": get_balance_on(party=args.get("party"), party_type=args.get("party_type")),
+ "exchange_rate": exchange_rate,
+ args.get("amount_field_party"): args.get("amount"),
+ "is_advance": args.get("is_advance"),
+ "reference_type": ref_doc.doctype,
+ "reference_name": ref_doc.name,
+ },
+ )
bank_row = je.append("accounts")
# Make it bank_details
- bank_account = get_default_bank_cash_account(ref_doc.company, "Bank", account=args.get("bank_account"))
+ bank_account = get_default_bank_cash_account(
+ ref_doc.company, "Bank", account=args.get("bank_account")
+ )
if bank_account:
bank_row.update(bank_account)
# Modified to include the posting date for which the exchange rate is required.
# Assumed to be the posting date of the reference date
- bank_row.exchange_rate = get_exchange_rate(ref_doc.get("posting_date")
- or ref_doc.get("transaction_date"), bank_account["account"],
- bank_account["account_currency"], ref_doc.company)
+ bank_row.exchange_rate = get_exchange_rate(
+ ref_doc.get("posting_date") or ref_doc.get("transaction_date"),
+ bank_account["account"],
+ bank_account["account_currency"],
+ ref_doc.company,
+ )
bank_row.cost_center = cost_center
@@ -947,9 +1181,10 @@ def get_payment_entry(ref_doc, args):
bank_row.set(args.get("amount_field_bank"), amount * exchange_rate)
# Multi currency check again
- if party_row.account_currency != ref_doc.company_currency \
- or (bank_row.account_currency and bank_row.account_currency != ref_doc.company_currency):
- je.multi_currency = 1
+ if party_row.account_currency != ref_doc.company_currency or (
+ bank_row.account_currency and bank_row.account_currency != ref_doc.company_currency
+ ):
+ je.multi_currency = 1
je.set_amounts_in_company_currency()
je.set_total_debit_credit()
@@ -960,13 +1195,17 @@ def get_payment_entry(ref_doc, args):
@frappe.whitelist()
def get_opening_accounts(company):
"""get all balance sheet accounts for opening entry"""
- accounts = frappe.db.sql_list("""select
+ accounts = frappe.db.sql_list(
+ """select
name from tabAccount
where
is_group=0 and report_type='Balance Sheet' and company={0} and
name not in (select distinct account from tabWarehouse where
account is not null and account != '')
- order by name asc""".format(frappe.db.escape(company)))
+ order by name asc""".format(
+ frappe.db.escape(company)
+ )
+ )
return [{"account": a, "balance": get_balance_on(a)} for a in accounts]
@@ -974,10 +1213,11 @@ def get_opening_accounts(company):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
- if not frappe.db.has_column('Journal Entry', searchfield):
+ if not frappe.db.has_column("Journal Entry", searchfield):
return []
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
SELECT jv.name, jv.posting_date, jv.user_remark
FROM `tabJournal Entry` jv, `tabJournal Entry Account` jv_detail
WHERE jv_detail.parent = jv.name
@@ -991,14 +1231,17 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
AND jv.`{0}` LIKE %(txt)s
ORDER BY jv.name DESC
LIMIT %(offset)s, %(limit)s
- """.format(searchfield), dict(
- account=filters.get("account"),
- party=cstr(filters.get("party")),
- txt="%{0}%".format(txt),
- offset=start,
- limit=page_len
- )
- )
+ """.format(
+ searchfield
+ ),
+ dict(
+ account=filters.get("account"),
+ party=cstr(filters.get("party")),
+ txt="%{0}%".format(txt),
+ offset=start,
+ limit=page_len,
+ ),
+ )
@frappe.whitelist()
@@ -1014,37 +1257,55 @@ def get_outstanding(args):
if args.get("doctype") == "Journal Entry":
condition = " and party=%(party)s" if args.get("party") else ""
- against_jv_amount = frappe.db.sql("""
+ against_jv_amount = frappe.db.sql(
+ """
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabJournal Entry Account` where parent=%(docname)s and account=%(account)s {0}
- and (reference_type is null or reference_type = '')""".format(condition), args)
+ and (reference_type is null or reference_type = '')""".format(
+ condition
+ ),
+ args,
+ )
against_jv_amount = flt(against_jv_amount[0][0]) if against_jv_amount else 0
- amount_field = "credit_in_account_currency" if against_jv_amount > 0 else "debit_in_account_currency"
- return {
- amount_field: abs(against_jv_amount)
- }
+ amount_field = (
+ "credit_in_account_currency" if against_jv_amount > 0 else "debit_in_account_currency"
+ )
+ return {amount_field: abs(against_jv_amount)}
elif args.get("doctype") in ("Sales Invoice", "Purchase Invoice"):
party_type = "Customer" if args.get("doctype") == "Sales Invoice" else "Supplier"
- invoice = frappe.db.get_value(args["doctype"], args["docname"],
- ["outstanding_amount", "conversion_rate", scrub(party_type)], as_dict=1)
+ invoice = frappe.db.get_value(
+ args["doctype"],
+ args["docname"],
+ ["outstanding_amount", "conversion_rate", scrub(party_type)],
+ as_dict=1,
+ )
- exchange_rate = invoice.conversion_rate if (args.get("account_currency") != company_currency) else 1
+ exchange_rate = (
+ invoice.conversion_rate if (args.get("account_currency") != company_currency) else 1
+ )
if args["doctype"] == "Sales Invoice":
- amount_field = "credit_in_account_currency" \
- if flt(invoice.outstanding_amount) > 0 else "debit_in_account_currency"
+ amount_field = (
+ "credit_in_account_currency"
+ if flt(invoice.outstanding_amount) > 0
+ else "debit_in_account_currency"
+ )
else:
- amount_field = "debit_in_account_currency" \
- if flt(invoice.outstanding_amount) > 0 else "credit_in_account_currency"
+ amount_field = (
+ "debit_in_account_currency"
+ if flt(invoice.outstanding_amount) > 0
+ else "credit_in_account_currency"
+ )
return {
amount_field: abs(flt(invoice.outstanding_amount)),
"exchange_rate": exchange_rate,
"party_type": party_type,
- "party": invoice.get(scrub(party_type))
+ "party": invoice.get(scrub(party_type)),
}
+
@frappe.whitelist()
def get_party_account_and_balance(company, party_type, party, cost_center=None):
if not frappe.has_permission("Account"):
@@ -1053,24 +1314,30 @@ def get_party_account_and_balance(company, party_type, party, cost_center=None):
account = get_party_account(party_type, party, company)
account_balance = get_balance_on(account=account, cost_center=cost_center)
- party_balance = get_balance_on(party_type=party_type, party=party, company=company, cost_center=cost_center)
+ party_balance = get_balance_on(
+ party_type=party_type, party=party, company=company, cost_center=cost_center
+ )
return {
"account": account,
"balance": account_balance,
"party_balance": party_balance,
- "account_currency": frappe.db.get_value("Account", account, "account_currency")
+ "account_currency": frappe.db.get_value("Account", account, "account_currency"),
}
@frappe.whitelist()
-def get_account_balance_and_party_type(account, date, company, debit=None, credit=None, exchange_rate=None, cost_center=None):
+def get_account_balance_and_party_type(
+ account, date, company, debit=None, credit=None, exchange_rate=None, cost_center=None
+):
"""Returns dict of account balance and party type to be set in Journal Entry on selection of account."""
if not frappe.has_permission("Account"):
frappe.msgprint(_("No Permission"), raise_exception=1)
company_currency = erpnext.get_company_currency(company)
- account_details = frappe.db.get_value("Account", account, ["account_type", "account_currency"], as_dict=1)
+ account_details = frappe.db.get_value(
+ "Account", account, ["account_type", "account_currency"], as_dict=1
+ )
if not account_details:
return
@@ -1087,11 +1354,17 @@ def get_account_balance_and_party_type(account, date, company, debit=None, credi
"party_type": party_type,
"account_type": account_details.account_type,
"account_currency": account_details.account_currency or company_currency,
-
# The date used to retreive the exchange rate here is the date passed in
# as an argument to this function. It is assumed to be the date on which the balance is sought
- "exchange_rate": get_exchange_rate(date, account, account_details.account_currency,
- company, debit=debit, credit=credit, exchange_rate=exchange_rate)
+ "exchange_rate": get_exchange_rate(
+ date,
+ account,
+ account_details.account_currency,
+ company,
+ debit=debit,
+ credit=credit,
+ exchange_rate=exchange_rate,
+ ),
}
# un-set party if not party type
@@ -1102,11 +1375,22 @@ def get_account_balance_and_party_type(account, date, company, debit=None, credi
@frappe.whitelist()
-def get_exchange_rate(posting_date, account=None, account_currency=None, company=None,
- reference_type=None, reference_name=None, debit=None, credit=None, exchange_rate=None):
+def get_exchange_rate(
+ posting_date,
+ account=None,
+ account_currency=None,
+ company=None,
+ reference_type=None,
+ reference_name=None,
+ debit=None,
+ credit=None,
+ exchange_rate=None,
+):
from erpnext.setup.utils import get_exchange_rate
- account_details = frappe.db.get_value("Account", account,
- ["account_type", "root_type", "account_currency", "company"], as_dict=1)
+
+ account_details = frappe.db.get_value(
+ "Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
+ )
if not account_details:
frappe.throw(_("Please select correct account"))
@@ -1125,7 +1409,7 @@ def get_exchange_rate(posting_date, account=None, account_currency=None, company
# The date used to retreive the exchange rate here is the date passed
# in as an argument to this function.
- elif (not exchange_rate or flt(exchange_rate)==1) and account_currency and posting_date:
+ elif (not exchange_rate or flt(exchange_rate) == 1) and account_currency and posting_date:
exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
else:
exchange_rate = 1
@@ -1144,15 +1428,17 @@ def get_average_exchange_rate(account):
return exchange_rate
+
@frappe.whitelist()
def make_inter_company_journal_entry(name, voucher_type, company):
- journal_entry = frappe.new_doc('Journal Entry')
+ journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = voucher_type
journal_entry.company = company
journal_entry.posting_date = nowdate()
journal_entry.inter_company_journal_entry_reference = name
return journal_entry.as_dict()
+
@frappe.whitelist()
def make_reverse_journal_entry(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
@@ -1160,24 +1446,25 @@ def make_reverse_journal_entry(source_name, target_doc=None):
def post_process(source, target):
target.reversal_of = source.name
- doclist = get_mapped_doc("Journal Entry", source_name, {
- "Journal Entry": {
- "doctype": "Journal Entry",
- "validation": {
- "docstatus": ["=", 1]
- }
+ doclist = get_mapped_doc(
+ "Journal Entry",
+ source_name,
+ {
+ "Journal Entry": {"doctype": "Journal Entry", "validation": {"docstatus": ["=", 1]}},
+ "Journal Entry Account": {
+ "doctype": "Journal Entry Account",
+ "field_map": {
+ "account_currency": "account_currency",
+ "exchange_rate": "exchange_rate",
+ "debit_in_account_currency": "credit_in_account_currency",
+ "debit": "credit",
+ "credit_in_account_currency": "debit_in_account_currency",
+ "credit": "debit",
+ },
+ },
},
- "Journal Entry Account": {
- "doctype": "Journal Entry Account",
- "field_map": {
- "account_currency": "account_currency",
- "exchange_rate": "exchange_rate",
- "debit_in_account_currency": "credit_in_account_currency",
- "debit": "credit",
- "credit_in_account_currency": "debit_in_account_currency",
- "credit": "debit",
- }
- },
- }, target_doc, post_process)
+ target_doc,
+ post_process,
+ )
return doclist
diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
index 481462b2aba..2cc5378e927 100644
--- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
@@ -39,14 +39,25 @@ class TestJournalEntry(unittest.TestCase):
test_voucher.submit()
if test_voucher.doctype == "Journal Entry":
- self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account`
+ self.assertTrue(
+ frappe.db.sql(
+ """select name from `tabJournal Entry Account`
where account = %s and docstatus = 1 and parent = %s""",
- ("_Test Receivable - _TC", test_voucher.name)))
+ ("_Test Receivable - _TC", test_voucher.name),
+ )
+ )
- self.assertFalse(frappe.db.sql("""select name from `tabJournal Entry Account`
- where reference_type = %s and reference_name = %s""", (test_voucher.doctype, test_voucher.name)))
+ self.assertFalse(
+ frappe.db.sql(
+ """select name from `tabJournal Entry Account`
+ where reference_type = %s and reference_name = %s""",
+ (test_voucher.doctype, test_voucher.name),
+ )
+ )
- base_jv.get("accounts")[0].is_advance = "Yes" if (test_voucher.doctype in ["Sales Order", "Purchase Order"]) else "No"
+ base_jv.get("accounts")[0].is_advance = (
+ "Yes" if (test_voucher.doctype in ["Sales Order", "Purchase Order"]) else "No"
+ )
base_jv.get("accounts")[0].set("reference_type", test_voucher.doctype)
base_jv.get("accounts")[0].set("reference_name", test_voucher.name)
base_jv.insert()
@@ -54,18 +65,28 @@ class TestJournalEntry(unittest.TestCase):
submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name)
- self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account`
- where reference_type = %s and reference_name = %s and {0}=400""".format(dr_or_cr),
- (submitted_voucher.doctype, submitted_voucher.name)))
+ self.assertTrue(
+ frappe.db.sql(
+ """select name from `tabJournal Entry Account`
+ where reference_type = %s and reference_name = %s and {0}=400""".format(
+ dr_or_cr
+ ),
+ (submitted_voucher.doctype, submitted_voucher.name),
+ )
+ )
if base_jv.get("accounts")[0].is_advance == "Yes":
self.advance_paid_testcase(base_jv, submitted_voucher, dr_or_cr)
self.cancel_against_voucher_testcase(submitted_voucher)
def advance_paid_testcase(self, base_jv, test_voucher, dr_or_cr):
- #Test advance paid field
- advance_paid = frappe.db.sql("""select advance_paid from `tab%s`
- where name=%s""" % (test_voucher.doctype, '%s'), (test_voucher.name))
+ # Test advance paid field
+ advance_paid = frappe.db.sql(
+ """select advance_paid from `tab%s`
+ where name=%s"""
+ % (test_voucher.doctype, "%s"),
+ (test_voucher.name),
+ )
payment_against_order = base_jv.get("accounts")[0].get(dr_or_cr)
self.assertTrue(flt(advance_paid[0][0]) == flt(payment_against_order))
@@ -74,13 +95,19 @@ class TestJournalEntry(unittest.TestCase):
if test_voucher.doctype == "Journal Entry":
# if test_voucher is a Journal Entry, test cancellation of test_voucher
test_voucher.cancel()
- self.assertFalse(frappe.db.sql("""select name from `tabJournal Entry Account`
- where reference_type='Journal Entry' and reference_name=%s""", test_voucher.name))
+ self.assertFalse(
+ frappe.db.sql(
+ """select name from `tabJournal Entry Account`
+ where reference_type='Journal Entry' and reference_name=%s""",
+ test_voucher.name,
+ )
+ )
elif test_voucher.doctype in ["Sales Order", "Purchase Order"]:
# if test_voucher is a Sales Order/Purchase Order, test error on cancellation of test_voucher
- frappe.db.set_value("Accounts Settings", "Accounts Settings",
- "unlink_advance_payment_on_cancelation_of_order", 0)
+ frappe.db.set_value(
+ "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", 0
+ )
submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name)
self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel)
@@ -89,7 +116,10 @@ class TestJournalEntry(unittest.TestCase):
stock_account = get_inventory_account(company)
from erpnext.accounts.utils import get_stock_and_account_balance
- account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company)
+
+ account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
+ stock_account, nowdate(), company
+ )
diff = flt(account_bal) - flt(stock_bal)
if not diff:
@@ -98,19 +128,25 @@ class TestJournalEntry(unittest.TestCase):
jv = frappe.new_doc("Journal Entry")
jv.company = company
jv.posting_date = nowdate()
- jv.append("accounts", {
- "account": stock_account,
- "cost_center": "Main - TCP1",
- "debit_in_account_currency": 0 if diff > 0 else abs(diff),
- "credit_in_account_currency": diff if diff > 0 else 0
- })
+ jv.append(
+ "accounts",
+ {
+ "account": stock_account,
+ "cost_center": "Main - TCP1",
+ "debit_in_account_currency": 0 if diff > 0 else abs(diff),
+ "credit_in_account_currency": diff if diff > 0 else 0,
+ },
+ )
- jv.append("accounts", {
- "account": "Stock Adjustment - TCP1",
- "cost_center": "Main - TCP1",
- "debit_in_account_currency": diff if diff > 0 else 0,
- "credit_in_account_currency": 0 if diff > 0 else abs(diff)
- })
+ jv.append(
+ "accounts",
+ {
+ "account": "Stock Adjustment - TCP1",
+ "cost_center": "Main - TCP1",
+ "debit_in_account_currency": diff if diff > 0 else 0,
+ "credit_in_account_currency": 0 if diff > 0 else abs(diff),
+ },
+ )
jv.insert()
if account_bal == stock_bal:
@@ -121,16 +157,21 @@ class TestJournalEntry(unittest.TestCase):
jv.cancel()
def test_multi_currency(self):
- jv = make_journal_entry("_Test Bank USD - _TC",
- "_Test Bank - _TC", 100, exchange_rate=50, save=False)
+ jv = make_journal_entry(
+ "_Test Bank USD - _TC", "_Test Bank - _TC", 100, exchange_rate=50, save=False
+ )
jv.get("accounts")[1].credit_in_account_currency = 5000
jv.submit()
- gl_entries = frappe.db.sql("""select account, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
- order by account asc""", jv.name, as_dict=1)
+ order by account asc""",
+ jv.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -140,33 +181,42 @@ class TestJournalEntry(unittest.TestCase):
"debit": 5000,
"debit_in_account_currency": 100,
"credit": 0,
- "credit_in_account_currency": 0
+ "credit_in_account_currency": 0,
},
"_Test Bank - _TC": {
"account_currency": "INR",
"debit": 0,
"debit_in_account_currency": 0,
"credit": 5000,
- "credit_in_account_currency": 5000
- }
+ "credit_in_account_currency": 5000,
+ },
}
- for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"):
+ for field in (
+ "account_currency",
+ "debit",
+ "debit_in_account_currency",
+ "credit",
+ "credit_in_account_currency",
+ ):
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][field], gle[field])
# cancel
jv.cancel()
- gle = frappe.db.sql("""select name from `tabGL Entry`
- where voucher_type='Sales Invoice' and voucher_no=%s""", jv.name)
+ gle = frappe.db.sql(
+ """select name from `tabGL Entry`
+ where voucher_type='Sales Invoice' and voucher_no=%s""",
+ jv.name,
+ )
self.assertFalse(gle)
def test_reverse_journal_entry(self):
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
- jv = make_journal_entry("_Test Bank USD - _TC",
- "Sales - _TC", 100, exchange_rate=50, save=False)
+
+ jv = make_journal_entry("_Test Bank USD - _TC", "Sales - _TC", 100, exchange_rate=50, save=False)
jv.get("accounts")[1].credit_in_account_currency = 5000
jv.get("accounts")[1].exchange_rate = 1
@@ -176,15 +226,17 @@ class TestJournalEntry(unittest.TestCase):
rjv.posting_date = nowdate()
rjv.submit()
-
- gl_entries = frappe.db.sql("""select account, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
- order by account asc""", rjv.name, as_dict=1)
+ order by account asc""",
+ rjv.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
-
expected_values = {
"_Test Bank USD - _TC": {
"account_currency": "USD",
@@ -199,44 +251,38 @@ class TestJournalEntry(unittest.TestCase):
"debit_in_account_currency": 5000,
"credit": 0,
"credit_in_account_currency": 0,
- }
+ },
}
- for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"):
+ for field in (
+ "account_currency",
+ "debit",
+ "debit_in_account_currency",
+ "credit",
+ "credit_in_account_currency",
+ ):
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][field], gle[field])
def test_disallow_change_in_account_currency_for_a_party(self):
# create jv in USD
- jv = make_journal_entry("_Test Bank USD - _TC",
- "_Test Receivable USD - _TC", 100, save=False)
+ jv = make_journal_entry("_Test Bank USD - _TC", "_Test Receivable USD - _TC", 100, save=False)
- jv.accounts[1].update({
- "party_type": "Customer",
- "party": "_Test Customer USD"
- })
+ jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD"})
jv.submit()
# create jv in USD, but account currency in INR
- jv = make_journal_entry("_Test Bank - _TC",
- "_Test Receivable - _TC", 100, save=False)
+ jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable - _TC", 100, save=False)
- jv.accounts[1].update({
- "party_type": "Customer",
- "party": "_Test Customer USD"
- })
+ jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD"})
self.assertRaises(InvalidAccountCurrency, jv.submit)
# back in USD
- jv = make_journal_entry("_Test Bank USD - _TC",
- "_Test Receivable USD - _TC", 100, save=False)
+ jv = make_journal_entry("_Test Bank USD - _TC", "_Test Receivable USD - _TC", 100, save=False)
- jv.accounts[1].update({
- "party_type": "Customer",
- "party": "_Test Customer USD"
- })
+ jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD"})
jv.submit()
@@ -245,13 +291,27 @@ class TestJournalEntry(unittest.TestCase):
frappe.db.set_value("Account", "Buildings - _TC", "inter_company_account", 1)
frappe.db.set_value("Account", "Sales Expenses - _TC1", "inter_company_account", 1)
frappe.db.set_value("Account", "Buildings - _TC1", "inter_company_account", 1)
- jv = make_journal_entry("Sales Expenses - _TC", "Buildings - _TC", 100, posting_date=nowdate(), cost_center = "Main - _TC", save=False)
+ jv = make_journal_entry(
+ "Sales Expenses - _TC",
+ "Buildings - _TC",
+ 100,
+ posting_date=nowdate(),
+ cost_center="Main - _TC",
+ save=False,
+ )
jv.voucher_type = "Inter Company Journal Entry"
jv.multi_currency = 0
jv.insert()
jv.submit()
- jv1 = make_journal_entry("Sales Expenses - _TC1", "Buildings - _TC1", 100, posting_date=nowdate(), cost_center = "Main - _TC1", save=False)
+ jv1 = make_journal_entry(
+ "Sales Expenses - _TC1",
+ "Buildings - _TC1",
+ 100,
+ posting_date=nowdate(),
+ cost_center="Main - _TC1",
+ save=False,
+ )
jv1.inter_company_journal_entry_reference = jv.name
jv1.company = "_Test Company 1"
jv1.voucher_type = "Inter Company Journal Entry"
@@ -273,9 +333,12 @@ class TestJournalEntry(unittest.TestCase):
def test_jv_with_cost_centre(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+
cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
- jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, cost_center = cost_center, save=False)
+ jv = make_journal_entry(
+ "_Test Cash - _TC", "_Test Bank - _TC", 100, cost_center=cost_center, save=False
+ )
jv.voucher_type = "Bank Entry"
jv.multi_currency = 0
jv.cheque_no = "112233"
@@ -284,17 +347,17 @@ class TestJournalEntry(unittest.TestCase):
jv.submit()
expected_values = {
- "_Test Cash - _TC": {
- "cost_center": cost_center
- },
- "_Test Bank - _TC": {
- "cost_center": cost_center
- }
+ "_Test Cash - _TC": {"cost_center": cost_center},
+ "_Test Bank - _TC": {"cost_center": cost_center},
}
- gl_entries = frappe.db.sql("""select account, cost_center, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, cost_center, debit, credit
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
- order by account asc""", jv.name, as_dict=1)
+ order by account asc""",
+ jv.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -305,11 +368,13 @@ class TestJournalEntry(unittest.TestCase):
from erpnext.projects.doctype.project.test_project import make_project
if not frappe.db.exists("Project", {"project_name": "Journal Entry Project"}):
- project = make_project({
- 'project_name': 'Journal Entry Project',
- 'project_template_name': 'Test Project Template',
- 'start_date': '2020-01-01'
- })
+ project = make_project(
+ {
+ "project_name": "Journal Entry Project",
+ "project_template_name": "Test Project Template",
+ "start_date": "2020-01-01",
+ }
+ )
project_name = project.name
else:
project_name = frappe.get_value("Project", {"project_name": "_Test Project"})
@@ -325,17 +390,17 @@ class TestJournalEntry(unittest.TestCase):
jv.submit()
expected_values = {
- "_Test Cash - _TC": {
- "project": project_name
- },
- "_Test Bank - _TC": {
- "project": project_name
- }
+ "_Test Cash - _TC": {"project": project_name},
+ "_Test Bank - _TC": {"project": project_name},
}
- gl_entries = frappe.db.sql("""select account, project, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, project, debit, credit
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
- order by account asc""", jv.name, as_dict=1)
+ order by account asc""",
+ jv.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -345,9 +410,12 @@ class TestJournalEntry(unittest.TestCase):
def test_jv_account_and_party_balance_with_cost_centre(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.utils import get_balance_on
+
cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
- jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, cost_center = cost_center, save=False)
+ jv = make_journal_entry(
+ "_Test Cash - _TC", "_Test Bank - _TC", 100, cost_center=cost_center, save=False
+ )
account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=cost_center)
jv.voucher_type = "Bank Entry"
jv.multi_currency = 0
@@ -360,7 +428,18 @@ class TestJournalEntry(unittest.TestCase):
account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=cost_center)
self.assertEqual(expected_account_balance, account_balance)
-def make_journal_entry(account1, account2, amount, cost_center=None, posting_date=None, exchange_rate=1, save=True, submit=False, project=None):
+
+def make_journal_entry(
+ account1,
+ account2,
+ amount,
+ cost_center=None,
+ posting_date=None,
+ exchange_rate=1,
+ save=True,
+ submit=False,
+ project=None,
+):
if not cost_center:
cost_center = "_Test Cost Center - _TC"
@@ -369,23 +448,27 @@ def make_journal_entry(account1, account2, amount, cost_center=None, posting_dat
jv.company = "_Test Company"
jv.user_remark = "test"
jv.multi_currency = 1
- jv.set("accounts", [
- {
- "account": account1,
- "cost_center": cost_center,
- "project": project,
- "debit_in_account_currency": amount if amount > 0 else 0,
- "credit_in_account_currency": abs(amount) if amount < 0 else 0,
- "exchange_rate": exchange_rate
- }, {
- "account": account2,
- "cost_center": cost_center,
- "project": project,
- "credit_in_account_currency": amount if amount > 0 else 0,
- "debit_in_account_currency": abs(amount) if amount < 0 else 0,
- "exchange_rate": exchange_rate
- }
- ])
+ jv.set(
+ "accounts",
+ [
+ {
+ "account": account1,
+ "cost_center": cost_center,
+ "project": project,
+ "debit_in_account_currency": amount if amount > 0 else 0,
+ "credit_in_account_currency": abs(amount) if amount < 0 else 0,
+ "exchange_rate": exchange_rate,
+ },
+ {
+ "account": account2,
+ "cost_center": cost_center,
+ "project": project,
+ "credit_in_account_currency": amount if amount > 0 else 0,
+ "debit_in_account_currency": abs(amount) if amount < 0 else 0,
+ "exchange_rate": exchange_rate,
+ },
+ ],
+ )
if save or submit:
jv.insert()
@@ -394,4 +477,5 @@ def make_journal_entry(account1, account2, amount, cost_center=None, posting_dat
return jv
-test_records = frappe.get_test_records('Journal Entry')
+
+test_records = frappe.get_test_records("Journal Entry")
diff --git a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.py b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.py
index 2da72c20ad8..b8ef3545d33 100644
--- a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.py
+++ b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.py
@@ -9,6 +9,7 @@ from frappe.model.document import Document
class JournalEntryTemplate(Document):
pass
+
@frappe.whitelist()
def get_naming_series():
return frappe.get_meta("Journal Entry").get_field("naming_series").options
diff --git a/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py
index f460b9f7953..dcb43fb2cb3 100644
--- a/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py
+++ b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py
@@ -8,6 +8,7 @@ from frappe.utils import today
exclude_from_linked_with = True
+
class LoyaltyPointEntry(Document):
pass
@@ -16,18 +17,28 @@ def get_loyalty_point_entries(customer, loyalty_program, company, expiry_date=No
if not expiry_date:
expiry_date = today()
- return frappe.db.sql('''
+ return frappe.db.sql(
+ """
select name, loyalty_points, expiry_date, loyalty_program_tier, invoice_type, invoice
from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s
and expiry_date>=%s and loyalty_points>0 and company=%s
order by expiry_date
- ''', (customer, loyalty_program, expiry_date, company), as_dict=1)
+ """,
+ (customer, loyalty_program, expiry_date, company),
+ as_dict=1,
+ )
+
def get_redemption_details(customer, loyalty_program, company):
- return frappe._dict(frappe.db.sql('''
+ return frappe._dict(
+ frappe.db.sql(
+ """
select redeem_against, sum(loyalty_points)
from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s and loyalty_points<0 and company=%s
group by redeem_against
- ''', (customer, loyalty_program, company)))
+ """,
+ (customer, loyalty_program, company),
+ )
+ )
diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
index 70da03b27f3..48a25ad6b81 100644
--- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
+++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
@@ -12,39 +12,61 @@ class LoyaltyProgram(Document):
pass
-def get_loyalty_details(customer, loyalty_program, expiry_date=None, company=None, include_expired_entry=False):
+def get_loyalty_details(
+ customer, loyalty_program, expiry_date=None, company=None, include_expired_entry=False
+):
if not expiry_date:
expiry_date = today()
- condition = ''
+ condition = ""
if company:
condition = " and company=%s " % frappe.db.escape(company)
if not include_expired_entry:
condition += " and expiry_date>='%s' " % expiry_date
- loyalty_point_details = frappe.db.sql('''select sum(loyalty_points) as loyalty_points,
+ loyalty_point_details = frappe.db.sql(
+ """select sum(loyalty_points) as loyalty_points,
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s and posting_date <= %s
{condition}
- group by customer'''.format(condition=condition),
- (customer, loyalty_program, expiry_date), as_dict=1)
+ group by customer""".format(
+ condition=condition
+ ),
+ (customer, loyalty_program, expiry_date),
+ as_dict=1,
+ )
if loyalty_point_details:
return loyalty_point_details[0]
else:
return {"loyalty_points": 0, "total_spent": 0}
-@frappe.whitelist()
-def get_loyalty_program_details_with_points(customer, loyalty_program=None, expiry_date=None, company=None, \
- silent=False, include_expired_entry=False, current_transaction_amount=0):
- lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent)
- loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program)
- lp_details.update(get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry))
- tier_spent_level = sorted([d.as_dict() for d in loyalty_program.collection_rules],
- key=lambda rule:rule.min_spent, reverse=True)
+@frappe.whitelist()
+def get_loyalty_program_details_with_points(
+ customer,
+ loyalty_program=None,
+ expiry_date=None,
+ company=None,
+ silent=False,
+ include_expired_entry=False,
+ current_transaction_amount=0,
+):
+ lp_details = get_loyalty_program_details(
+ customer, loyalty_program, company=company, silent=silent
+ )
+ loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program)
+ lp_details.update(
+ get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry)
+ )
+
+ tier_spent_level = sorted(
+ [d.as_dict() for d in loyalty_program.collection_rules],
+ key=lambda rule: rule.min_spent,
+ reverse=True,
+ )
for i, d in enumerate(tier_spent_level):
- if i==0 or (lp_details.total_spent+current_transaction_amount) <= d.min_spent:
+ if i == 0 or (lp_details.total_spent + current_transaction_amount) <= d.min_spent:
lp_details.tier_name = d.tier_name
lp_details.collection_factor = d.collection_factor
else:
@@ -52,8 +74,16 @@ def get_loyalty_program_details_with_points(customer, loyalty_program=None, expi
return lp_details
+
@frappe.whitelist()
-def get_loyalty_program_details(customer, loyalty_program=None, expiry_date=None, company=None, silent=False, include_expired_entry=False):
+def get_loyalty_program_details(
+ customer,
+ loyalty_program=None,
+ expiry_date=None,
+ company=None,
+ silent=False,
+ include_expired_entry=False,
+):
lp_details = frappe._dict()
if not loyalty_program:
@@ -72,6 +102,7 @@ def get_loyalty_program_details(customer, loyalty_program=None, expiry_date=None
lp_details.update(loyalty_program.as_dict())
return lp_details
+
@frappe.whitelist()
def get_redeemption_factor(loyalty_program=None, customer=None):
customer_loyalty_program = None
@@ -98,13 +129,16 @@ def validate_loyalty_points(ref_doc, points_to_redeem):
else:
loyalty_program = frappe.db.get_value("Customer", ref_doc.customer, ["loyalty_program"])
- if loyalty_program and frappe.db.get_value("Loyalty Program", loyalty_program, ["company"]) !=\
- ref_doc.company:
+ if (
+ loyalty_program
+ and frappe.db.get_value("Loyalty Program", loyalty_program, ["company"]) != ref_doc.company
+ ):
frappe.throw(_("The Loyalty Program isn't valid for the selected company"))
if loyalty_program and points_to_redeem:
- loyalty_program_details = get_loyalty_program_details_with_points(ref_doc.customer, loyalty_program,
- posting_date, ref_doc.company)
+ loyalty_program_details = get_loyalty_program_details_with_points(
+ ref_doc.customer, loyalty_program, posting_date, ref_doc.company
+ )
if points_to_redeem > loyalty_program_details.loyalty_points:
frappe.throw(_("You don't have enought Loyalty Points to redeem"))
diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program_dashboard.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program_dashboard.py
index 7652e9691b4..3a4f908a1e9 100644
--- a/erpnext/accounts/doctype/loyalty_program/loyalty_program_dashboard.py
+++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program_dashboard.py
@@ -1,11 +1,5 @@
-
-
def get_data():
return {
- 'fieldname': 'loyalty_program',
- 'transactions': [
- {
- 'items': ['Sales Invoice', 'Customer']
- }
- ]
+ "fieldname": "loyalty_program",
+ "transactions": [{"items": ["Sales Invoice", "Customer"]}],
}
diff --git a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
index 82c14324f5c..3641ac4428f 100644
--- a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
+++ b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
@@ -19,19 +19,28 @@ class TestLoyaltyProgram(unittest.TestCase):
create_records()
def test_loyalty_points_earned_single_tier(self):
- frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
+ frappe.db.set_value(
+ "Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
+ )
# create a new sales invoice
si_original = create_sales_invoice_record()
si_original.insert()
si_original.submit()
- customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"})
+ customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"})
earned_points = get_points_earned(si_original)
- lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer})
+ lpe = frappe.get_doc(
+ "Loyalty Point Entry",
+ {
+ "invoice_type": "Sales Invoice",
+ "invoice": si_original.name,
+ "customer": si_original.customer,
+ },
+ )
- self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program)
- self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier)
+ self.assertEqual(si_original.get("loyalty_program"), customer.loyalty_program)
+ self.assertEqual(lpe.get("loyalty_program_tier"), customer.loyalty_program_tier)
self.assertEqual(lpe.loyalty_points, earned_points)
# add redemption point
@@ -43,21 +52,31 @@ class TestLoyaltyProgram(unittest.TestCase):
earned_after_redemption = get_points_earned(si_redeem)
- lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'redeem_against': lpe.name})
- lpe_earn = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]})
+ lpe_redeem = frappe.get_doc(
+ "Loyalty Point Entry",
+ {"invoice_type": "Sales Invoice", "invoice": si_redeem.name, "redeem_against": lpe.name},
+ )
+ lpe_earn = frappe.get_doc(
+ "Loyalty Point Entry",
+ {"invoice_type": "Sales Invoice", "invoice": si_redeem.name, "name": ["!=", lpe_redeem.name]},
+ )
self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption)
- self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points))
+ self.assertEqual(lpe_redeem.loyalty_points, (-1 * earned_points))
# cancel and delete
for d in [si_redeem, si_original]:
d.cancel()
def test_loyalty_points_earned_multiple_tier(self):
- frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Multiple Loyalty")
+ frappe.db.set_value(
+ "Customer", "Test Loyalty Customer", "loyalty_program", "Test Multiple Loyalty"
+ )
# assign multiple tier program to the customer
- customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"})
- customer.loyalty_program = frappe.get_doc('Loyalty Program', {'loyalty_program_name': 'Test Multiple Loyalty'}).name
+ customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"})
+ customer.loyalty_program = frappe.get_doc(
+ "Loyalty Program", {"loyalty_program_name": "Test Multiple Loyalty"}
+ ).name
customer.save()
# create a new sales invoice
@@ -67,10 +86,17 @@ class TestLoyaltyProgram(unittest.TestCase):
earned_points = get_points_earned(si_original)
- lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer})
+ lpe = frappe.get_doc(
+ "Loyalty Point Entry",
+ {
+ "invoice_type": "Sales Invoice",
+ "invoice": si_original.name,
+ "customer": si_original.customer,
+ },
+ )
- self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program)
- self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier)
+ self.assertEqual(si_original.get("loyalty_program"), customer.loyalty_program)
+ self.assertEqual(lpe.get("loyalty_program_tier"), customer.loyalty_program_tier)
self.assertEqual(lpe.loyalty_points, earned_points)
# add redemption point
@@ -80,14 +106,20 @@ class TestLoyaltyProgram(unittest.TestCase):
si_redeem.insert()
si_redeem.submit()
- customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"})
+ customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"})
earned_after_redemption = get_points_earned(si_redeem)
- lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'redeem_against': lpe.name})
- lpe_earn = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]})
+ lpe_redeem = frappe.get_doc(
+ "Loyalty Point Entry",
+ {"invoice_type": "Sales Invoice", "invoice": si_redeem.name, "redeem_against": lpe.name},
+ )
+ lpe_earn = frappe.get_doc(
+ "Loyalty Point Entry",
+ {"invoice_type": "Sales Invoice", "invoice": si_redeem.name, "name": ["!=", lpe_redeem.name]},
+ )
self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption)
- self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points))
+ self.assertEqual(lpe_redeem.loyalty_points, (-1 * earned_points))
self.assertEqual(lpe_earn.loyalty_program_tier, customer.loyalty_program_tier)
# cancel and delete
@@ -95,23 +127,30 @@ class TestLoyaltyProgram(unittest.TestCase):
d.cancel()
def test_cancel_sales_invoice(self):
- ''' cancelling the sales invoice should cancel the earned points'''
- frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
+ """cancelling the sales invoice should cancel the earned points"""
+ frappe.db.set_value(
+ "Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
+ )
# create a new sales invoice
si = create_sales_invoice_record()
si.insert()
si.submit()
- lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si.name, 'customer': si.customer})
+ lpe = frappe.get_doc(
+ "Loyalty Point Entry",
+ {"invoice_type": "Sales Invoice", "invoice": si.name, "customer": si.customer},
+ )
self.assertEqual(True, not (lpe is None))
# cancelling sales invoice
si.cancel()
- lpe = frappe.db.exists('Loyalty Point Entry', lpe.name)
+ lpe = frappe.db.exists("Loyalty Point Entry", lpe.name)
self.assertEqual(True, (lpe is None))
def test_sales_invoice_return(self):
- frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
+ frappe.db.set_value(
+ "Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
+ )
# create a new sales invoice
si_original = create_sales_invoice_record(2)
si_original.conversion_rate = flt(1)
@@ -119,7 +158,14 @@ class TestLoyaltyProgram(unittest.TestCase):
si_original.submit()
earned_points = get_points_earned(si_original)
- lpe_original = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer})
+ lpe_original = frappe.get_doc(
+ "Loyalty Point Entry",
+ {
+ "invoice_type": "Sales Invoice",
+ "invoice": si_original.name,
+ "customer": si_original.customer,
+ },
+ )
self.assertEqual(lpe_original.loyalty_points, earned_points)
# create sales invoice return
@@ -131,10 +177,17 @@ class TestLoyaltyProgram(unittest.TestCase):
si_return.submit()
# fetch original invoice again as its status would have been updated
- si_original = frappe.get_doc('Sales Invoice', lpe_original.invoice)
+ si_original = frappe.get_doc("Sales Invoice", lpe_original.invoice)
earned_points = get_points_earned(si_original)
- lpe_after_return = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer})
+ lpe_after_return = frappe.get_doc(
+ "Loyalty Point Entry",
+ {
+ "invoice_type": "Sales Invoice",
+ "invoice": si_original.name,
+ "customer": si_original.customer,
+ },
+ )
self.assertEqual(lpe_after_return.loyalty_points, earned_points)
self.assertEqual(True, (lpe_original.loyalty_points > lpe_after_return.loyalty_points))
@@ -143,144 +196,164 @@ class TestLoyaltyProgram(unittest.TestCase):
try:
d.cancel()
except frappe.TimestampMismatchError:
- frappe.get_doc('Sales Invoice', d.name).cancel()
+ frappe.get_doc("Sales Invoice", d.name).cancel()
def test_loyalty_points_for_dashboard(self):
- doc = frappe.get_doc('Customer', 'Test Loyalty Customer')
+ doc = frappe.get_doc("Customer", "Test Loyalty Customer")
company_wise_info = get_dashboard_info("Customer", doc.name, doc.loyalty_program)
for d in company_wise_info:
self.assertTrue(d.get("loyalty_points"))
+
def get_points_earned(self):
def get_returned_amount():
- returned_amount = frappe.db.sql("""
+ returned_amount = frappe.db.sql(
+ """
select sum(grand_total)
from `tabSales Invoice`
where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s
- """, self.name)
+ """,
+ self.name,
+ )
return abs(flt(returned_amount[0][0])) if returned_amount else 0
- lp_details = get_loyalty_program_details_with_points(self.customer, company=self.company,
- loyalty_program=self.loyalty_program, expiry_date=self.posting_date, include_expired_entry=True)
- if lp_details and getdate(lp_details.from_date) <= getdate(self.posting_date) and \
- (not lp_details.to_date or getdate(lp_details.to_date) >= getdate(self.posting_date)):
+ lp_details = get_loyalty_program_details_with_points(
+ self.customer,
+ company=self.company,
+ loyalty_program=self.loyalty_program,
+ expiry_date=self.posting_date,
+ include_expired_entry=True,
+ )
+ if (
+ lp_details
+ and getdate(lp_details.from_date) <= getdate(self.posting_date)
+ and (not lp_details.to_date or getdate(lp_details.to_date) >= getdate(self.posting_date))
+ ):
returned_amount = get_returned_amount()
eligible_amount = flt(self.grand_total) - cint(self.loyalty_amount) - returned_amount
- points_earned = cint(eligible_amount/lp_details.collection_factor)
+ points_earned = cint(eligible_amount / lp_details.collection_factor)
return points_earned or 0
+
def create_sales_invoice_record(qty=1):
# return sales invoice doc object
- return frappe.get_doc({
- "doctype": "Sales Invoice",
- "customer": frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"}).name,
- "company": '_Test Company',
- "due_date": today(),
- "posting_date": today(),
- "currency": "INR",
- "taxes_and_charges": "",
- "debit_to": "Debtors - _TC",
- "taxes": [],
- "items": [{
- 'doctype': 'Sales Invoice Item',
- 'item_code': frappe.get_doc('Item', {'item_name': 'Loyal Item'}).name,
- 'qty': qty,
- "rate": 10000,
- 'income_account': 'Sales - _TC',
- 'cost_center': 'Main - _TC',
- 'expense_account': 'Cost of Goods Sold - _TC'
- }]
- })
+ return frappe.get_doc(
+ {
+ "doctype": "Sales Invoice",
+ "customer": frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"}).name,
+ "company": "_Test Company",
+ "due_date": today(),
+ "posting_date": today(),
+ "currency": "INR",
+ "taxes_and_charges": "",
+ "debit_to": "Debtors - _TC",
+ "taxes": [],
+ "items": [
+ {
+ "doctype": "Sales Invoice Item",
+ "item_code": frappe.get_doc("Item", {"item_name": "Loyal Item"}).name,
+ "qty": qty,
+ "rate": 10000,
+ "income_account": "Sales - _TC",
+ "cost_center": "Main - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ }
+ ],
+ }
+ )
+
def create_records():
# create a new loyalty Account
if not frappe.db.exists("Account", "Loyalty - _TC"):
- frappe.get_doc({
- "doctype": "Account",
- "account_name": "Loyalty",
- "parent_account": "Direct Expenses - _TC",
- "company": "_Test Company",
- "is_group": 0,
- "account_type": "Expense Account",
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "Loyalty",
+ "parent_account": "Direct Expenses - _TC",
+ "company": "_Test Company",
+ "is_group": 0,
+ "account_type": "Expense Account",
+ }
+ ).insert()
# create a new loyalty program Single tier
- if not frappe.db.exists("Loyalty Program","Test Single Loyalty"):
- frappe.get_doc({
- "doctype": "Loyalty Program",
- "loyalty_program_name": "Test Single Loyalty",
- "auto_opt_in": 1,
- "from_date": today(),
- "loyalty_program_type": "Single Tier Program",
- "conversion_factor": 1,
- "expiry_duration": 10,
- "company": "_Test Company",
- "cost_center": "Main - _TC",
- "expense_account": "Loyalty - _TC",
- "collection_rules": [{
- 'tier_name': 'Silver',
- 'collection_factor': 1000,
- 'min_spent': 1000
- }]
- }).insert()
+ if not frappe.db.exists("Loyalty Program", "Test Single Loyalty"):
+ frappe.get_doc(
+ {
+ "doctype": "Loyalty Program",
+ "loyalty_program_name": "Test Single Loyalty",
+ "auto_opt_in": 1,
+ "from_date": today(),
+ "loyalty_program_type": "Single Tier Program",
+ "conversion_factor": 1,
+ "expiry_duration": 10,
+ "company": "_Test Company",
+ "cost_center": "Main - _TC",
+ "expense_account": "Loyalty - _TC",
+ "collection_rules": [{"tier_name": "Silver", "collection_factor": 1000, "min_spent": 1000}],
+ }
+ ).insert()
# create a new customer
- if not frappe.db.exists("Customer","Test Loyalty Customer"):
- frappe.get_doc({
- "customer_group": "_Test Customer Group",
- "customer_name": "Test Loyalty Customer",
- "customer_type": "Individual",
- "doctype": "Customer",
- "territory": "_Test Territory"
- }).insert()
+ if not frappe.db.exists("Customer", "Test Loyalty Customer"):
+ frappe.get_doc(
+ {
+ "customer_group": "_Test Customer Group",
+ "customer_name": "Test Loyalty Customer",
+ "customer_type": "Individual",
+ "doctype": "Customer",
+ "territory": "_Test Territory",
+ }
+ ).insert()
# create a new loyalty program Multiple tier
- if not frappe.db.exists("Loyalty Program","Test Multiple Loyalty"):
- frappe.get_doc({
- "doctype": "Loyalty Program",
- "loyalty_program_name": "Test Multiple Loyalty",
- "auto_opt_in": 1,
- "from_date": today(),
- "loyalty_program_type": "Multiple Tier Program",
- "conversion_factor": 1,
- "expiry_duration": 10,
- "company": "_Test Company",
- "cost_center": "Main - _TC",
- "expense_account": "Loyalty - _TC",
- "collection_rules": [
- {
- 'tier_name': 'Silver',
- 'collection_factor': 1000,
- 'min_spent': 10000
- },
- {
- 'tier_name': 'Gold',
- 'collection_factor': 1000,
- 'min_spent': 19000
- }
- ]
- }).insert()
+ if not frappe.db.exists("Loyalty Program", "Test Multiple Loyalty"):
+ frappe.get_doc(
+ {
+ "doctype": "Loyalty Program",
+ "loyalty_program_name": "Test Multiple Loyalty",
+ "auto_opt_in": 1,
+ "from_date": today(),
+ "loyalty_program_type": "Multiple Tier Program",
+ "conversion_factor": 1,
+ "expiry_duration": 10,
+ "company": "_Test Company",
+ "cost_center": "Main - _TC",
+ "expense_account": "Loyalty - _TC",
+ "collection_rules": [
+ {"tier_name": "Silver", "collection_factor": 1000, "min_spent": 10000},
+ {"tier_name": "Gold", "collection_factor": 1000, "min_spent": 19000},
+ ],
+ }
+ ).insert()
# create an item
if not frappe.db.exists("Item", "Loyal Item"):
- frappe.get_doc({
- "doctype": "Item",
- "item_code": "Loyal Item",
- "item_name": "Loyal Item",
- "item_group": "All Item Groups",
- "company": "_Test Company",
- "is_stock_item": 1,
- "opening_stock": 100,
- "valuation_rate": 10000,
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Item",
+ "item_code": "Loyal Item",
+ "item_name": "Loyal Item",
+ "item_group": "All Item Groups",
+ "company": "_Test Company",
+ "is_stock_item": 1,
+ "opening_stock": 100,
+ "valuation_rate": 10000,
+ }
+ ).insert()
# create item price
- if not frappe.db.exists("Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}):
- frappe.get_doc({
- "doctype": "Item Price",
- "price_list": "Standard Selling",
- "item_code": "Loyal Item",
- "price_list_rate": 10000
- }).insert()
+ if not frappe.db.exists(
+ "Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}
+ ):
+ frappe.get_doc(
+ {
+ "doctype": "Item Price",
+ "price_list": "Standard Selling",
+ "item_code": "Loyal Item",
+ "price_list_rate": 10000,
+ }
+ ).insert()
diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py
index f21d1b9baa9..d0373021a69 100644
--- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py
+++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py
@@ -19,23 +19,35 @@ class ModeofPayment(Document):
for entry in self.accounts:
accounts_list.append(entry.company)
- if len(accounts_list)!= len(set(accounts_list)):
+ if len(accounts_list) != len(set(accounts_list)):
frappe.throw(_("Same Company is entered more than once"))
def validate_accounts(self):
for entry in self.accounts:
"""Error when Company of Ledger account doesn't match with Company Selected"""
if frappe.db.get_value("Account", entry.default_account, "company") != entry.company:
- frappe.throw(_("Account {0} does not match with Company {1} in Mode of Account: {2}")
- .format(entry.default_account, entry.company, self.name))
+ frappe.throw(
+ _("Account {0} does not match with Company {1} in Mode of Account: {2}").format(
+ entry.default_account, entry.company, self.name
+ )
+ )
def validate_pos_mode_of_payment(self):
if not self.enabled:
- pos_profiles = frappe.db.sql("""SELECT sip.parent FROM `tabSales Invoice Payment` sip
- WHERE sip.parenttype = 'POS Profile' and sip.mode_of_payment = %s""", (self.name))
+ pos_profiles = frappe.db.sql(
+ """SELECT sip.parent FROM `tabSales Invoice Payment` sip
+ WHERE sip.parenttype = 'POS Profile' and sip.mode_of_payment = %s""",
+ (self.name),
+ )
pos_profiles = list(map(lambda x: x[0], pos_profiles))
if pos_profiles:
- message = "POS Profile " + frappe.bold(", ".join(pos_profiles)) + " contains \
- Mode of Payment " + frappe.bold(str(self.name)) + ". Please remove them to disable this mode."
+ message = (
+ "POS Profile "
+ + frappe.bold(", ".join(pos_profiles))
+ + " contains \
+ Mode of Payment "
+ + frappe.bold(str(self.name))
+ + ". Please remove them to disable this mode."
+ )
frappe.throw(_(message), title="Not Allowed")
diff --git a/erpnext/accounts/doctype/mode_of_payment/test_mode_of_payment.py b/erpnext/accounts/doctype/mode_of_payment/test_mode_of_payment.py
index 2ff02a7c4dc..9733fb89e2f 100644
--- a/erpnext/accounts/doctype/mode_of_payment/test_mode_of_payment.py
+++ b/erpnext/accounts/doctype/mode_of_payment/test_mode_of_payment.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Mode of Payment')
+
class TestModeofPayment(unittest.TestCase):
pass
diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py
index a8c5f68c110..1d19708eddf 100644
--- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py
+++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py
@@ -11,13 +11,25 @@ from frappe.utils import add_months, flt
class MonthlyDistribution(Document):
@frappe.whitelist()
def get_months(self):
- month_list = ['January','February','March','April','May','June','July','August','September',
- 'October','November','December']
- idx =1
+ month_list = [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+ ]
+ idx = 1
for m in month_list:
- mnth = self.append('percentages')
+ mnth = self.append("percentages")
mnth.month = m
- mnth.percentage_allocation = 100.0/12
+ mnth.percentage_allocation = 100.0 / 12
mnth.idx = idx
idx += 1
@@ -25,18 +37,15 @@ class MonthlyDistribution(Document):
total = sum(flt(d.percentage_allocation) for d in self.get("percentages"))
if flt(total, 2) != 100.0:
- frappe.throw(_("Percentage Allocation should be equal to 100%") + \
- " ({0}%)".format(str(flt(total, 2))))
+ frappe.throw(
+ _("Percentage Allocation should be equal to 100%") + " ({0}%)".format(str(flt(total, 2)))
+ )
+
def get_periodwise_distribution_data(distribution_id, period_list, periodicity):
- doc = frappe.get_doc('Monthly Distribution', distribution_id)
+ doc = frappe.get_doc("Monthly Distribution", distribution_id)
- months_to_add = {
- "Yearly": 12,
- "Half-Yearly": 6,
- "Quarterly": 3,
- "Monthly": 1
- }[periodicity]
+ months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity]
period_dict = {}
@@ -45,6 +54,7 @@ def get_periodwise_distribution_data(distribution_id, period_list, periodicity):
return period_dict
+
def get_percentage(doc, start_date, period):
percentage = 0
months = [start_date.strftime("%B").title()]
diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py
index 3e6575f2540..ba2cb671d40 100644
--- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py
+++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py
@@ -1,22 +1,16 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'monthly_distribution',
- 'non_standard_fieldnames': {
- 'Sales Person': 'distribution_id',
- 'Territory': 'distribution_id',
- 'Sales Partner': 'distribution_id',
+ "fieldname": "monthly_distribution",
+ "non_standard_fieldnames": {
+ "Sales Person": "distribution_id",
+ "Territory": "distribution_id",
+ "Sales Partner": "distribution_id",
},
- 'transactions': [
- {
- 'label': _('Target Details'),
- 'items': ['Sales Person', 'Territory', 'Sales Partner']
- },
- {
- 'items': ['Budget']
- }
- ]
+ "transactions": [
+ {"label": _("Target Details"), "items": ["Sales Person", "Territory", "Sales Partner"]},
+ {"items": ["Budget"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/monthly_distribution/test_monthly_distribution.py b/erpnext/accounts/doctype/monthly_distribution/test_monthly_distribution.py
index 4a878b2aaf7..848b1f9de76 100644
--- a/erpnext/accounts/doctype/monthly_distribution/test_monthly_distribution.py
+++ b/erpnext/accounts/doctype/monthly_distribution/test_monthly_distribution.py
@@ -6,7 +6,8 @@ import unittest
import frappe
-test_records = frappe.get_test_records('Monthly Distribution')
+test_records = frappe.get_test_records("Monthly Distribution")
+
class TestMonthlyDistribution(unittest.TestCase):
pass
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json
index daee8f8c1ab..c367b360e1f 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json
@@ -75,7 +75,7 @@
],
"hide_toolbar": 1,
"issingle": 1,
- "modified": "2022-01-04 15:25:06.053187",
+ "modified": "2022-01-04 16:25:06.053187",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool",
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
index a33892e044d..0f0ab68dcb8 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
@@ -20,9 +20,9 @@ class OpeningInvoiceCreationTool(Document):
def onload(self):
"""Load the Opening Invoice summary"""
summary, max_count = self.get_opening_invoice_summary()
- self.set_onload('opening_invoices_summary', summary)
- self.set_onload('max_count', max_count)
- self.set_onload('temporary_opening_account', get_temporary_opening_account(self.company))
+ self.set_onload("opening_invoices_summary", summary)
+ self.set_onload("max_count", max_count)
+ self.set_onload("temporary_opening_account", get_temporary_opening_account(self.company))
def get_opening_invoice_summary(self):
def prepare_invoice_summary(doctype, invoices):
@@ -32,10 +32,7 @@ class OpeningInvoiceCreationTool(Document):
for invoice in invoices:
company = invoice.pop("company")
_summary = invoices_summary.get(company, {})
- _summary.update({
- "currency": company_wise_currency.get(company),
- doctype: invoice
- })
+ _summary.update({"currency": company_wise_currency.get(company), doctype: invoice})
invoices_summary.update({company: _summary})
if invoice.paid_amount:
@@ -44,17 +41,21 @@ class OpeningInvoiceCreationTool(Document):
outstanding_amount.append(invoice.outstanding_amount)
if paid_amount or outstanding_amount:
- max_count.update({
- doctype: {
- "max_paid": max(paid_amount) if paid_amount else 0.0,
- "max_due": max(outstanding_amount) if outstanding_amount else 0.0
+ max_count.update(
+ {
+ doctype: {
+ "max_paid": max(paid_amount) if paid_amount else 0.0,
+ "max_due": max(outstanding_amount) if outstanding_amount else 0.0,
+ }
}
- })
+ )
invoices_summary = {}
max_count = {}
fields = [
- "company", "count(name) as total_invoices", "sum(outstanding_amount) as outstanding_amount"
+ "company",
+ "count(name) as total_invoices",
+ "sum(outstanding_amount) as outstanding_amount",
]
companies = frappe.get_all("Company", fields=["name as company", "default_currency as currency"])
if not companies:
@@ -62,8 +63,9 @@ class OpeningInvoiceCreationTool(Document):
company_wise_currency = {row.company: row.currency for row in companies}
for doctype in ["Sales Invoice", "Purchase Invoice"]:
- invoices = frappe.get_all(doctype, filters=dict(is_opening="Yes", docstatus=1),
- fields=fields, group_by="company")
+ invoices = frappe.get_all(
+ doctype, filters=dict(is_opening="Yes", docstatus=1), fields=fields, group_by="company"
+ )
prepare_invoice_summary(doctype, invoices)
return invoices_summary, max_count
@@ -74,7 +76,9 @@ class OpeningInvoiceCreationTool(Document):
def set_missing_values(self, row):
row.qty = row.qty or 1.0
- row.temporary_opening_account = row.temporary_opening_account or get_temporary_opening_account(self.company)
+ row.temporary_opening_account = row.temporary_opening_account or get_temporary_opening_account(
+ self.company
+ )
row.party_type = "Customer" if self.invoice_type == "Sales" else "Supplier"
row.item_name = row.item_name or _("Opening Invoice Item")
row.posting_date = row.posting_date or nowdate()
@@ -85,7 +89,11 @@ class OpeningInvoiceCreationTool(Document):
if self.create_missing_party:
self.add_party(row.party_type, row.party)
else:
- frappe.throw(_("Row #{}: {} {} does not exist.").format(row.idx, frappe.bold(row.party_type), frappe.bold(row.party)))
+ frappe.throw(
+ _("Row #{}: {} {} does not exist.").format(
+ row.idx, frappe.bold(row.party_type), frappe.bold(row.party)
+ )
+ )
mandatory_error_msg = _("Row #{0}: {1} is required to create the Opening {2} Invoices")
for d in ("Party", "Outstanding Amount", "Temporary Opening Account"):
@@ -100,12 +108,22 @@ class OpeningInvoiceCreationTool(Document):
self.set_missing_values(row)
self.validate_mandatory_invoice_fields(row)
invoice = self.get_invoice_dict(row)
- company_details = frappe.get_cached_value('Company', self.company, ["default_currency", "default_letter_head"], as_dict=1) or {}
+ company_details = (
+ frappe.get_cached_value(
+ "Company", self.company, ["default_currency", "default_letter_head"], as_dict=1
+ )
+ or {}
+ )
+
+ default_currency = frappe.db.get_value(row.party_type, row.party, "default_currency")
+
if company_details:
- invoice.update({
- "currency": company_details.get("default_currency"),
- "letter_head": company_details.get("default_letter_head")
- })
+ invoice.update(
+ {
+ "currency": default_currency or company_details.get("default_currency"),
+ "letter_head": company_details.get("default_letter_head"),
+ }
+ )
invoices.append(invoice)
return invoices
@@ -127,55 +145,61 @@ class OpeningInvoiceCreationTool(Document):
def get_invoice_dict(self, row=None):
def get_item_dict():
- cost_center = row.get('cost_center') or frappe.get_cached_value('Company', self.company, "cost_center")
+ cost_center = row.get("cost_center") or frappe.get_cached_value(
+ "Company", self.company, "cost_center"
+ )
if not cost_center:
- frappe.throw(_("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company)))
+ frappe.throw(
+ _("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company))
+ )
- income_expense_account_field = "income_account" if row.party_type == "Customer" else "expense_account"
+ income_expense_account_field = (
+ "income_account" if row.party_type == "Customer" else "expense_account"
+ )
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos")
rate = flt(row.outstanding_amount) / flt(row.qty)
- item_dict = frappe._dict({
- "uom": default_uom,
- "rate": rate or 0.0,
- "qty": row.qty,
- "conversion_factor": 1.0,
- "item_name": row.item_name or "Opening Invoice Item",
- "description": row.item_name or "Opening Invoice Item",
- income_expense_account_field: row.temporary_opening_account,
- "cost_center": cost_center
- })
+ item_dict = frappe._dict(
+ {
+ "uom": default_uom,
+ "rate": rate or 0.0,
+ "qty": row.qty,
+ "conversion_factor": 1.0,
+ "item_name": row.item_name or "Opening Invoice Item",
+ "description": row.item_name or "Opening Invoice Item",
+ income_expense_account_field: row.temporary_opening_account,
+ "cost_center": cost_center,
+ }
+ )
for dimension in get_accounting_dimensions():
- item_dict.update({
- dimension: row.get(dimension)
- })
+ item_dict.update({dimension: row.get(dimension)})
return item_dict
item = get_item_dict()
- invoice = frappe._dict({
- "items": [item],
- "is_opening": "Yes",
- "set_posting_time": 1,
- "company": self.company,
- "cost_center": self.cost_center,
- "due_date": row.due_date,
- "posting_date": row.posting_date,
- frappe.scrub(row.party_type): row.party,
- "is_pos": 0,
- "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
- "update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559
- "invoice_number": row.invoice_number,
- "disable_rounded_total": 1
- })
+ invoice = frappe._dict(
+ {
+ "items": [item],
+ "is_opening": "Yes",
+ "set_posting_time": 1,
+ "company": self.company,
+ "cost_center": self.cost_center,
+ "due_date": row.due_date,
+ "posting_date": row.posting_date,
+ frappe.scrub(row.party_type): row.party,
+ "is_pos": 0,
+ "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
+ "update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559
+ "invoice_number": row.invoice_number,
+ "disable_rounded_total": 1,
+ }
+ )
accounting_dimension = get_accounting_dimensions()
for dimension in accounting_dimension:
- invoice.update({
- dimension: self.get(dimension) or item.get(dimension)
- })
+ invoice.update({dimension: self.get(dimension) or item.get(dimension)})
return invoice
@@ -201,9 +225,10 @@ class OpeningInvoiceCreationTool(Document):
event="opening_invoice_creation",
job_name=self.name,
invoices=invoices,
- now=frappe.conf.developer_mode or frappe.flags.in_test
+ now=frappe.conf.developer_mode or frappe.flags.in_test,
)
+
def start_import(invoices):
errors = 0
names = []
@@ -222,35 +247,43 @@ def start_import(invoices):
except Exception:
errors += 1
frappe.db.rollback()
- message = "\n".join(["Data:", dumps(d, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()])
+ message = "\n".join(
+ ["Data:", dumps(d, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()]
+ )
frappe.log_error(title="Error while creating Opening Invoice", message=message)
frappe.db.commit()
if errors:
- frappe.msgprint(_("You had {} errors while creating opening invoices. Check {} for more details")
- .format(errors, "Error Log"), indicator="red", title=_("Error Occured"))
+ frappe.msgprint(
+ _("You had {} errors while creating opening invoices. Check {} for more details").format(
+ errors, "Error Log"
+ ),
+ indicator="red",
+ title=_("Error Occured"),
+ )
return names
+
def publish(index, total, doctype):
- if total < 5: return
+ if total < 5:
+ return
frappe.publish_realtime(
"opening_invoice_creation_progress",
dict(
title=_("Opening Invoice Creation In Progress"),
- message=_('Creating {} out of {} {}').format(index + 1, total, doctype),
+ message=_("Creating {} out of {} {}").format(index + 1, total, doctype),
user=frappe.session.user,
- count=index+1,
- total=total
- ))
+ count=index + 1,
+ total=total,
+ ),
+ )
+
@frappe.whitelist()
def get_temporary_opening_account(company=None):
if not company:
return
- accounts = frappe.get_all("Account", filters={
- 'company': company,
- 'account_type': 'Temporary'
- })
+ accounts = frappe.get_all("Account", filters={"company": company, "account_type": "Temporary"})
if not accounts:
frappe.throw(_("Please add a Temporary Opening account in Chart of Accounts"))
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
index 3eaf6a28f37..1e22c64c8f2 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
@@ -2,6 +2,7 @@
# See license.txt
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
@@ -10,11 +11,11 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account,
)
-from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
-class TestOpeningInvoiceCreationTool(ERPNextTestCase):
+
+class TestOpeningInvoiceCreationTool(FrappeTestCase):
@classmethod
def setUpClass(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
@@ -22,10 +23,24 @@ class TestOpeningInvoiceCreationTool(ERPNextTestCase):
create_dimension()
return super().setUpClass()
- def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None):
+ def make_invoices(
+ self,
+ invoice_type="Sales",
+ company=None,
+ party_1=None,
+ party_2=None,
+ invoice_number=None,
+ department=None,
+ ):
doc = frappe.get_single("Opening Invoice Creation Tool")
- args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
- party_1=party_1, party_2=party_2, invoice_number=invoice_number, department=department)
+ args = get_opening_invoice_creation_dict(
+ invoice_type=invoice_type,
+ company=company,
+ party_1=party_1,
+ party_2=party_2,
+ invoice_number=invoice_number,
+ department=department,
+ )
doc.update(args)
return doc.make_invoices()
@@ -68,15 +83,30 @@ class TestOpeningInvoiceCreationTool(ERPNextTestCase):
company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
- old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account")
+ old_default_receivable_account = frappe.db.get_value(
+ "Company", company, "default_receivable_account"
+ )
frappe.db.set_value("Company", company, "default_receivable_account", "")
if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
- cc = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "_Test Opening Invoice Company",
- "is_group": 1, "company": "_Test Opening Invoice Company"})
+ cc = frappe.get_doc(
+ {
+ "doctype": "Cost Center",
+ "cost_center_name": "_Test Opening Invoice Company",
+ "is_group": 1,
+ "company": "_Test Opening Invoice Company",
+ }
+ )
cc.insert(ignore_mandatory=True)
- cc2 = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "Main", "is_group": 0,
- "company": "_Test Opening Invoice Company", "parent_cost_center": cc.name})
+ cc2 = frappe.get_doc(
+ {
+ "doctype": "Cost Center",
+ "cost_center_name": "Main",
+ "is_group": 0,
+ "company": "_Test Opening Invoice Company",
+ "parent_cost_center": cc.name,
+ }
+ )
cc2.insert()
frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC")
@@ -84,28 +114,37 @@ class TestOpeningInvoiceCreationTool(ERPNextTestCase):
self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2)
# Check if missing debit account error raised
- error_log = frappe.db.exists("Error Log", {"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]})
+ error_log = frappe.db.exists(
+ "Error Log",
+ {"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]},
+ )
self.assertTrue(error_log)
# teardown
- frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
+ frappe.db.set_value(
+ "Company", company, "default_receivable_account", old_default_receivable_account
+ )
def test_renaming_of_invoice_using_invoice_number_field(self):
company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
- self.make_invoices(company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11")
+ self.make_invoices(
+ company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11"
+ )
- sales_inv1 = frappe.get_all('Sales Invoice', filters={'customer':'Customer A'})[0].get("name")
- sales_inv2 = frappe.get_all('Sales Invoice', filters={'customer':'Customer B'})[0].get("name")
+ sales_inv1 = frappe.get_all("Sales Invoice", filters={"customer": "Customer A"})[0].get("name")
+ sales_inv2 = frappe.get_all("Sales Invoice", filters={"customer": "Customer B"})[0].get("name")
self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
- #teardown
+ # teardown
for inv in [sales_inv1, sales_inv2]:
- doc = frappe.get_doc('Sales Invoice', inv)
+ doc = frappe.get_doc("Sales Invoice", inv)
doc.cancel()
def test_opening_invoice_with_accounting_dimension(self):
- invoices = self.make_invoices(invoice_type="Sales", company="_Test Opening Invoice Company", department='Sales - _TOIC')
+ invoices = self.make_invoices(
+ invoice_type="Sales", company="_Test Opening Invoice Company", department="Sales - _TOIC"
+ )
expected_value = {
"keys": ["customer", "outstanding_amount", "status", "department"],
@@ -117,40 +156,44 @@ class TestOpeningInvoiceCreationTool(ERPNextTestCase):
def tearDown(self):
disable_dimension()
+
def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company")
- invoice_dict = frappe._dict({
- "company": company,
- "invoice_type": args.get("invoice_type", "Sales"),
- "invoices": [
- {
- "qty": 1.0,
- "outstanding_amount": 300,
- "party": args.get("party_1") or "_Test {0}".format(party),
- "item_name": "Opening Item",
- "due_date": "2016-09-10",
- "posting_date": "2016-09-05",
- "temporary_opening_account": get_temporary_opening_account(company),
- "invoice_number": args.get("invoice_number")
- },
- {
- "qty": 2.0,
- "outstanding_amount": 250,
- "party": args.get("party_2") or "_Test {0} 1".format(party),
- "item_name": "Opening Item",
- "due_date": "2016-09-10",
- "posting_date": "2016-09-05",
- "temporary_opening_account": get_temporary_opening_account(company),
- "invoice_number": None
- }
- ]
- })
+ invoice_dict = frappe._dict(
+ {
+ "company": company,
+ "invoice_type": args.get("invoice_type", "Sales"),
+ "invoices": [
+ {
+ "qty": 1.0,
+ "outstanding_amount": 300,
+ "party": args.get("party_1") or "_Test {0}".format(party),
+ "item_name": "Opening Item",
+ "due_date": "2016-09-10",
+ "posting_date": "2016-09-05",
+ "temporary_opening_account": get_temporary_opening_account(company),
+ "invoice_number": args.get("invoice_number"),
+ },
+ {
+ "qty": 2.0,
+ "outstanding_amount": 250,
+ "party": args.get("party_2") or "_Test {0} 1".format(party),
+ "item_name": "Opening Item",
+ "due_date": "2016-09-10",
+ "posting_date": "2016-09-05",
+ "temporary_opening_account": get_temporary_opening_account(company),
+ "invoice_number": None,
+ },
+ ],
+ }
+ )
invoice_dict.update(args)
return invoice_dict
+
def make_company():
if frappe.db.exists("Company", "_Test Opening Invoice Company"):
return frappe.get_doc("Company", "_Test Opening Invoice Company")
@@ -163,15 +206,18 @@ def make_company():
company.insert()
return company
+
def make_customer(customer=None):
customer_name = customer or "Opening Customer"
- customer = frappe.get_doc({
- "doctype": "Customer",
- "customer_name": customer_name,
- "customer_group": "All Customer Groups",
- "customer_type": "Company",
- "territory": "All Territories"
- })
+ customer = frappe.get_doc(
+ {
+ "doctype": "Customer",
+ "customer_name": customer_name,
+ "customer_group": "All Customer Groups",
+ "customer_type": "Company",
+ "territory": "All Territories",
+ }
+ )
if not frappe.db.exists("Customer", customer_name):
customer.insert(ignore_permissions=True)
return customer.name
diff --git a/erpnext/accounts/doctype/party_account/party_account.json b/erpnext/accounts/doctype/party_account/party_account.json
index c9f15a6a470..69330577ab3 100644
--- a/erpnext/accounts/doctype/party_account/party_account.json
+++ b/erpnext/accounts/doctype/party_account/party_account.json
@@ -3,6 +3,7 @@
"creation": "2014-08-29 16:02:39.740505",
"doctype": "DocType",
"editable_grid": 1,
+ "engine": "InnoDB",
"field_order": [
"company",
"account"
@@ -11,6 +12,7 @@
{
"fieldname": "company",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Company",
"options": "Company",
@@ -27,7 +29,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-04-07 18:13:08.833822",
+ "modified": "2022-04-04 12:31:02.994197",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Party Account",
@@ -35,5 +37,6 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py
index e9f813c17c6..d766aad9176 100644
--- a/erpnext/accounts/doctype/party_link/party_link.py
+++ b/erpnext/accounts/doctype/party_link/party_link.py
@@ -8,34 +8,43 @@ from frappe.model.document import Document
class PartyLink(Document):
def validate(self):
- if self.primary_role not in ['Customer', 'Supplier']:
- frappe.throw(_("Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."),
- title=_("Invalid Primary Role"))
+ if self.primary_role not in ["Customer", "Supplier"]:
+ frappe.throw(
+ _(
+ "Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."
+ ),
+ title=_("Invalid Primary Role"),
+ )
- existing_party_link = frappe.get_all('Party Link', {
- 'primary_party': self.secondary_party
- }, pluck="primary_role")
+ existing_party_link = frappe.get_all(
+ "Party Link", {"primary_party": self.secondary_party}, pluck="primary_role"
+ )
if existing_party_link:
- frappe.throw(_('{} {} is already linked with another {}')
- .format(self.secondary_role, self.secondary_party, existing_party_link[0]))
+ frappe.throw(
+ _("{} {} is already linked with another {}").format(
+ self.secondary_role, self.secondary_party, existing_party_link[0]
+ )
+ )
- existing_party_link = frappe.get_all('Party Link', {
- 'secondary_party': self.primary_party
- }, pluck="primary_role")
+ existing_party_link = frappe.get_all(
+ "Party Link", {"secondary_party": self.primary_party}, pluck="primary_role"
+ )
if existing_party_link:
- frappe.throw(_('{} {} is already linked with another {}')
- .format(self.primary_role, self.primary_party, existing_party_link[0]))
+ frappe.throw(
+ _("{} {} is already linked with another {}").format(
+ self.primary_role, self.primary_party, existing_party_link[0]
+ )
+ )
@frappe.whitelist()
def create_party_link(primary_role, primary_party, secondary_party):
- party_link = frappe.new_doc('Party Link')
+ party_link = frappe.new_doc("Party Link")
party_link.primary_role = primary_role
party_link.primary_party = primary_party
- party_link.secondary_role = 'Customer' if primary_role == 'Supplier' else 'Supplier'
+ party_link.secondary_role = "Customer" if primary_role == "Supplier" else "Supplier"
party_link.secondary_party = secondary_party
party_link.save(ignore_permissions=True)
return party_link
-
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 345764fb418..3a89ce8cd12 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -226,10 +226,7 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.total_allocated_amount > party_amount)));
frm.toggle_display("set_exchange_gain_loss",
- (frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount &&
- ((frm.doc.paid_from_account_currency != company_currency ||
- frm.doc.paid_to_account_currency != company_currency) &&
- frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency)));
+ frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount);
frm.refresh_fields();
},
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 7c3574266ea..7b1a52986d7 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -97,7 +97,7 @@ class PaymentEntry(AccountsController):
self.set_status()
def on_cancel(self):
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.make_gl_entries(cancel=1)
self.update_expense_claim()
self.update_outstanding_amounts()
@@ -110,6 +110,7 @@ class PaymentEntry(AccountsController):
def set_payment_req_status(self):
from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status
+
update_payment_req_status(self, None)
def update_outstanding_amounts(self):
@@ -119,8 +120,11 @@ class PaymentEntry(AccountsController):
reference_names = []
for d in self.get("references"):
if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names:
- frappe.throw(_("Row #{0}: Duplicate entry in References {1} {2}")
- .format(d.idx, d.reference_doctype, d.reference_name))
+ frappe.throw(
+ _("Row #{0}: Duplicate entry in References {1} {2}").format(
+ d.idx, d.reference_doctype, d.reference_name
+ )
+ )
reference_names.append((d.reference_doctype, d.reference_name, d.payment_term))
def set_bank_account_data(self):
@@ -136,20 +140,27 @@ class PaymentEntry(AccountsController):
self.set(field, bank_data.account)
def validate_payment_type_with_outstanding(self):
- total_outstanding = sum(d.allocated_amount for d in self.get('references'))
- if total_outstanding < 0 and self.party_type == 'Customer' and self.payment_type == 'Receive':
- frappe.throw(_("Cannot receive from customer against negative outstanding"), title=_("Incorrect Payment Type"))
+ total_outstanding = sum(d.allocated_amount for d in self.get("references"))
+ if total_outstanding < 0 and self.party_type == "Customer" and self.payment_type == "Receive":
+ frappe.throw(
+ _("Cannot receive from customer against negative outstanding"),
+ title=_("Incorrect Payment Type"),
+ )
def validate_allocated_amount(self):
for d in self.get("references"):
- if (flt(d.allocated_amount))> 0:
+ if (flt(d.allocated_amount)) > 0:
if flt(d.allocated_amount) > flt(d.outstanding_amount):
- frappe.throw(_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx))
+ frappe.throw(
+ _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
+ )
# Check for negative outstanding invoices as well
if flt(d.allocated_amount) < 0:
if flt(d.allocated_amount) < flt(d.outstanding_amount):
- frappe.throw(_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx))
+ frappe.throw(
+ _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
+ )
def delink_advance_entry_references(self):
for reference in self.references:
@@ -159,9 +170,14 @@ class PaymentEntry(AccountsController):
def set_missing_values(self):
if self.payment_type == "Internal Transfer":
- for field in ("party", "party_balance", "total_allocated_amount",
- "base_total_allocated_amount", "unallocated_amount"):
- self.set(field, None)
+ for field in (
+ "party",
+ "party_balance",
+ "total_allocated_amount",
+ "base_total_allocated_amount",
+ "unallocated_amount",
+ ):
+ self.set(field, None)
self.references = []
else:
if not self.party_type:
@@ -170,13 +186,16 @@ class PaymentEntry(AccountsController):
if not self.party:
frappe.throw(_("Party is mandatory"))
- _party_name = "title" if self.party_type in ("Student", "Shareholder") else self.party_type.lower() + "_name"
+ _party_name = (
+ "title" if self.party_type in ("Student", "Shareholder") else self.party_type.lower() + "_name"
+ )
self.party_name = frappe.db.get_value(self.party_type, self.party, _party_name)
if self.party:
if not self.party_balance:
- self.party_balance = get_balance_on(party_type=self.party_type,
- party=self.party, date=self.posting_date, company=self.company)
+ self.party_balance = get_balance_on(
+ party_type=self.party_type, party=self.party, date=self.posting_date, company=self.company
+ )
if not self.party_account:
party_account = get_party_account(self.party_type, self.party, self.company)
@@ -193,16 +212,20 @@ class PaymentEntry(AccountsController):
self.paid_to_account_currency = acc.account_currency
self.paid_to_account_balance = acc.account_balance
- self.party_account_currency = self.paid_from_account_currency \
- if self.payment_type=="Receive" else self.paid_to_account_currency
+ self.party_account_currency = (
+ self.paid_from_account_currency
+ if self.payment_type == "Receive"
+ else self.paid_to_account_currency
+ )
self.set_missing_ref_details()
def set_missing_ref_details(self, force=False):
for d in self.get("references"):
if d.allocated_amount:
- ref_details = get_reference_details(d.reference_doctype,
- d.reference_name, self.party_account_currency)
+ ref_details = get_reference_details(
+ d.reference_doctype, d.reference_name, self.party_account_currency
+ )
for field, value in iteritems(ref_details):
if d.exchange_gain_loss:
@@ -212,7 +235,7 @@ class PaymentEntry(AccountsController):
# refer -> `update_reference_in_payment_entry()` in utils.py
continue
- if field == 'exchange_rate' or not d.get(field) or force:
+ if field == "exchange_rate" or not d.get(field) or force:
d.db_set(field, value)
def validate_payment_type(self):
@@ -225,8 +248,9 @@ class PaymentEntry(AccountsController):
frappe.throw(_("Invalid {0}: {1}").format(self.party_type, self.party))
if self.party_account and self.party_type in ("Customer", "Supplier"):
- self.validate_account_type(self.party_account,
- [erpnext.get_party_account_type(self.party_type)])
+ self.validate_account_type(
+ self.party_account, [erpnext.get_party_account_type(self.party_type)]
+ )
def validate_bank_accounts(self):
if self.payment_type in ("Pay", "Internal Transfer"):
@@ -254,8 +278,9 @@ class PaymentEntry(AccountsController):
self.source_exchange_rate = ref_doc.get("exchange_rate")
if not self.source_exchange_rate:
- self.source_exchange_rate = get_exchange_rate(self.paid_from_account_currency,
- self.company_currency, self.posting_date)
+ self.source_exchange_rate = get_exchange_rate(
+ self.paid_from_account_currency, self.company_currency, self.posting_date
+ )
def set_target_exchange_rate(self, ref_doc=None):
if self.paid_from_account_currency == self.paid_to_account_currency:
@@ -266,8 +291,9 @@ class PaymentEntry(AccountsController):
self.target_exchange_rate = ref_doc.get("exchange_rate")
if not self.target_exchange_rate:
- self.target_exchange_rate = get_exchange_rate(self.paid_to_account_currency,
- self.company_currency, self.posting_date)
+ self.target_exchange_rate = get_exchange_rate(
+ self.paid_to_account_currency, self.company_currency, self.posting_date
+ )
def validate_mandatory(self):
for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"):
@@ -276,7 +302,7 @@ class PaymentEntry(AccountsController):
def validate_reference_documents(self):
if self.party_type == "Student":
- valid_reference_doctypes = ("Fees")
+ valid_reference_doctypes = "Fees"
elif self.party_type == "Customer":
valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
elif self.party_type == "Supplier":
@@ -284,16 +310,17 @@ class PaymentEntry(AccountsController):
elif self.party_type == "Employee":
valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance", "Gratuity")
elif self.party_type == "Shareholder":
- valid_reference_doctypes = ("Journal Entry")
+ valid_reference_doctypes = "Journal Entry"
elif self.party_type == "Donor":
- valid_reference_doctypes = ("Donation")
+ valid_reference_doctypes = "Donation"
for d in self.get("references"):
if not d.allocated_amount:
continue
if d.reference_doctype not in valid_reference_doctypes:
- frappe.throw(_("Reference Doctype must be one of {0}")
- .format(comma_or(valid_reference_doctypes)))
+ frappe.throw(
+ _("Reference Doctype must be one of {0}").format(comma_or(valid_reference_doctypes))
+ )
elif d.reference_name:
if not frappe.db.exists(d.reference_doctype, d.reference_name):
@@ -303,28 +330,35 @@ class PaymentEntry(AccountsController):
if d.reference_doctype != "Journal Entry":
if self.party != ref_doc.get(scrub(self.party_type)):
- frappe.throw(_("{0} {1} is not associated with {2} {3}")
- .format(d.reference_doctype, d.reference_name, self.party_type, self.party))
+ frappe.throw(
+ _("{0} {1} is not associated with {2} {3}").format(
+ d.reference_doctype, d.reference_name, self.party_type, self.party
+ )
+ )
else:
self.validate_journal_entry()
if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Expense Claim", "Fees"):
if self.party_type == "Customer":
- ref_party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or ref_doc.debit_to
+ ref_party_account = (
+ get_party_account_based_on_invoice_discounting(d.reference_name) or ref_doc.debit_to
+ )
elif self.party_type == "Student":
ref_party_account = ref_doc.receivable_account
- elif self.party_type=="Supplier":
+ elif self.party_type == "Supplier":
ref_party_account = ref_doc.credit_to
- elif self.party_type=="Employee":
+ elif self.party_type == "Employee":
ref_party_account = ref_doc.payable_account
if ref_party_account != self.party_account:
- frappe.throw(_("{0} {1} is associated with {2}, but Party Account is {3}")
- .format(d.reference_doctype, d.reference_name, ref_party_account, self.party_account))
+ frappe.throw(
+ _("{0} {1} is associated with {2}, but Party Account is {3}").format(
+ d.reference_doctype, d.reference_name, ref_party_account, self.party_account
+ )
+ )
if ref_doc.docstatus != 1:
- frappe.throw(_("{0} {1} must be submitted")
- .format(d.reference_doctype, d.reference_name))
+ frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name))
def validate_paid_invoices(self):
no_oustanding_refs = {}
@@ -334,28 +368,45 @@ class PaymentEntry(AccountsController):
continue
if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Fees"):
- outstanding_amount, is_return = frappe.get_cached_value(d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"])
+ outstanding_amount, is_return = frappe.get_cached_value(
+ d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"]
+ )
if outstanding_amount <= 0 and not is_return:
no_oustanding_refs.setdefault(d.reference_doctype, []).append(d)
for k, v in no_oustanding_refs.items():
frappe.msgprint(
- _("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.")
- .format(_(k), frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold(_("negative outstanding amount")))
- + "
" + _("If this is undesirable please cancel the corresponding Payment Entry."),
- title=_("Warning"), indicator="orange")
+ _(
+ "{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry."
+ ).format(
+ _(k),
+ frappe.bold(", ".join(d.reference_name for d in v)),
+ frappe.bold(_("negative outstanding amount")),
+ )
+ + "
"
+ + _("If this is undesirable please cancel the corresponding Payment Entry."),
+ title=_("Warning"),
+ indicator="orange",
+ )
def validate_journal_entry(self):
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype == "Journal Entry":
- je_accounts = frappe.db.sql("""select debit, credit from `tabJournal Entry Account`
+ je_accounts = frappe.db.sql(
+ """select debit, credit from `tabJournal Entry Account`
where account = %s and party=%s and docstatus = 1 and parent = %s
and (reference_type is null or reference_type in ("", "Sales Order", "Purchase Order"))
- """, (self.party_account, self.party, d.reference_name), as_dict=True)
+ """,
+ (self.party_account, self.party, d.reference_name),
+ as_dict=True,
+ )
if not je_accounts:
- frappe.throw(_("Row #{0}: Journal Entry {1} does not have account {2} or already matched against another voucher")
- .format(d.idx, d.reference_name, self.party_account))
+ frappe.throw(
+ _(
+ "Row #{0}: Journal Entry {1} does not have account {2} or already matched against another voucher"
+ ).format(d.idx, d.reference_name, self.party_account)
+ )
else:
dr_or_cr = "debit" if self.payment_type == "Receive" else "credit"
valid = False
@@ -363,14 +414,17 @@ class PaymentEntry(AccountsController):
if flt(jvd[dr_or_cr]) > 0:
valid = True
if not valid:
- frappe.throw(_("Against Journal Entry {0} does not have any unmatched {1} entry")
- .format(d.reference_name, dr_or_cr))
+ frappe.throw(
+ _("Against Journal Entry {0} does not have any unmatched {1} entry").format(
+ d.reference_name, dr_or_cr
+ )
+ )
def update_payment_schedule(self, cancel=0):
invoice_payment_amount_map = {}
invoice_paid_amount_map = {}
- for ref in self.get('references'):
+ for ref in self.get("references"):
if ref.payment_term and ref.reference_name:
key = (ref.payment_term, ref.reference_name)
invoice_payment_amount_map.setdefault(key, 0.0)
@@ -378,58 +432,68 @@ class PaymentEntry(AccountsController):
if not invoice_paid_amount_map.get(key):
payment_schedule = frappe.get_all(
- 'Payment Schedule',
- filters={'parent': ref.reference_name},
- fields=['paid_amount', 'payment_amount', 'payment_term', 'discount', 'outstanding']
+ "Payment Schedule",
+ filters={"parent": ref.reference_name},
+ fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"],
)
for term in payment_schedule:
invoice_key = (term.payment_term, ref.reference_name)
invoice_paid_amount_map.setdefault(invoice_key, {})
- invoice_paid_amount_map[invoice_key]['outstanding'] = term.outstanding
- invoice_paid_amount_map[invoice_key]['discounted_amt'] = ref.total_amount * (term.discount / 100)
+ invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
+ invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
+ term.discount / 100
+ )
for idx, (key, allocated_amount) in enumerate(iteritems(invoice_payment_amount_map), 1):
if not invoice_paid_amount_map.get(key):
- frappe.throw(_('Payment term {0} not used in {1}').format(key[0], key[1]))
+ frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1]))
- outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding'))
- discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get('discounted_amt'))
+ outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
+ discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
if cancel:
- frappe.db.sql("""
+ frappe.db.sql(
+ """
UPDATE `tabPayment Schedule`
SET
paid_amount = `paid_amount` - %s,
discounted_amount = `discounted_amount` - %s,
outstanding = `outstanding` + %s
WHERE parent = %s and payment_term = %s""",
- (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]))
+ (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
+ )
else:
if allocated_amount > outstanding:
- frappe.throw(_('Row #{0}: Cannot allocate more than {1} against payment term {2}').format(idx, outstanding, key[0]))
+ frappe.throw(
+ _("Row #{0}: Cannot allocate more than {1} against payment term {2}").format(
+ idx, outstanding, key[0]
+ )
+ )
if allocated_amount and outstanding:
- frappe.db.sql("""
+ frappe.db.sql(
+ """
UPDATE `tabPayment Schedule`
SET
paid_amount = `paid_amount` + %s,
discounted_amount = `discounted_amount` + %s,
outstanding = `outstanding` - %s
WHERE parent = %s and payment_term = %s""",
- (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]))
+ (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
+ )
def set_status(self):
if self.docstatus == 2:
- self.status = 'Cancelled'
+ self.status = "Cancelled"
elif self.docstatus == 1:
- self.status = 'Submitted'
+ self.status = "Submitted"
else:
- self.status = 'Draft'
+ self.status = "Draft"
- self.db_set('status', self.status, update_modified = True)
+ self.db_set("status", self.status, update_modified=True)
def set_tax_withholding(self):
- if not self.party_type == 'Supplier':
+ if not self.party_type == "Supplier":
return
if not self.apply_tax_withholding_amount:
@@ -438,22 +502,24 @@ class PaymentEntry(AccountsController):
net_total = self.paid_amount
# Adding args as purchase invoice to get TDS amount
- args = frappe._dict({
- 'company': self.company,
- 'doctype': 'Payment Entry',
- 'supplier': self.party,
- 'posting_date': self.posting_date,
- 'net_total': net_total
- })
+ args = frappe._dict(
+ {
+ "company": self.company,
+ "doctype": "Payment Entry",
+ "supplier": self.party,
+ "posting_date": self.posting_date,
+ "net_total": net_total,
+ }
+ )
tax_withholding_details = get_party_tax_withholding_details(args, self.tax_withholding_category)
if not tax_withholding_details:
return
- tax_withholding_details.update({
- 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company)
- })
+ tax_withholding_details.update(
+ {"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company)}
+ )
accounts = []
for d in self.taxes:
@@ -461,7 +527,7 @@ class PaymentEntry(AccountsController):
# Preserve user updated included in paid amount
if d.included_in_paid_amount:
- tax_withholding_details.update({'included_in_paid_amount': d.included_in_paid_amount})
+ tax_withholding_details.update({"included_in_paid_amount": d.included_in_paid_amount})
d.update(tax_withholding_details)
accounts.append(d.account_head)
@@ -469,8 +535,11 @@ class PaymentEntry(AccountsController):
if not accounts or tax_withholding_details.get("account_head") not in accounts:
self.append("taxes", tax_withholding_details)
- to_remove = [d for d in self.taxes
- if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")]
+ to_remove = [
+ d
+ for d in self.taxes
+ if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")
+ ]
for d in to_remove:
self.remove(d)
@@ -497,40 +566,53 @@ class PaymentEntry(AccountsController):
def set_received_amount(self):
self.base_received_amount = self.base_paid_amount
- if self.paid_from_account_currency == self.paid_to_account_currency \
- and not self.payment_type == 'Internal Transfer':
+ if (
+ self.paid_from_account_currency == self.paid_to_account_currency
+ and not self.payment_type == "Internal Transfer"
+ ):
self.received_amount = self.paid_amount
def set_amounts_after_tax(self):
applicable_tax = 0
base_applicable_tax = 0
- for tax in self.get('taxes'):
+ for tax in self.get("taxes"):
if not tax.included_in_paid_amount:
- amount = -1 * tax.tax_amount if tax.add_deduct_tax == 'Deduct' else tax.tax_amount
- base_amount = -1 * tax.base_tax_amount if tax.add_deduct_tax == 'Deduct' else tax.base_tax_amount
+ amount = -1 * tax.tax_amount if tax.add_deduct_tax == "Deduct" else tax.tax_amount
+ base_amount = (
+ -1 * tax.base_tax_amount if tax.add_deduct_tax == "Deduct" else tax.base_tax_amount
+ )
applicable_tax += amount
base_applicable_tax += base_amount
- self.paid_amount_after_tax = flt(flt(self.paid_amount) + flt(applicable_tax),
- self.precision("paid_amount_after_tax"))
- self.base_paid_amount_after_tax = flt(flt(self.paid_amount_after_tax) * flt(self.source_exchange_rate),
- self.precision("base_paid_amount_after_tax"))
+ self.paid_amount_after_tax = flt(
+ flt(self.paid_amount) + flt(applicable_tax), self.precision("paid_amount_after_tax")
+ )
+ self.base_paid_amount_after_tax = flt(
+ flt(self.paid_amount_after_tax) * flt(self.source_exchange_rate),
+ self.precision("base_paid_amount_after_tax"),
+ )
- self.received_amount_after_tax = flt(flt(self.received_amount) + flt(applicable_tax),
- self.precision("paid_amount_after_tax"))
- self.base_received_amount_after_tax = flt(flt(self.received_amount_after_tax) * flt(self.target_exchange_rate),
- self.precision("base_paid_amount_after_tax"))
+ self.received_amount_after_tax = flt(
+ flt(self.received_amount) + flt(applicable_tax), self.precision("paid_amount_after_tax")
+ )
+ self.base_received_amount_after_tax = flt(
+ flt(self.received_amount_after_tax) * flt(self.target_exchange_rate),
+ self.precision("base_paid_amount_after_tax"),
+ )
def set_amounts_in_company_currency(self):
self.base_paid_amount, self.base_received_amount, self.difference_amount = 0, 0, 0
if self.paid_amount:
- self.base_paid_amount = flt(flt(self.paid_amount) * flt(self.source_exchange_rate),
- self.precision("base_paid_amount"))
+ self.base_paid_amount = flt(
+ flt(self.paid_amount) * flt(self.source_exchange_rate), self.precision("base_paid_amount")
+ )
if self.received_amount:
- self.base_received_amount = flt(flt(self.received_amount) * flt(self.target_exchange_rate),
- self.precision("base_received_amount"))
+ self.base_received_amount = flt(
+ flt(self.received_amount) * flt(self.target_exchange_rate),
+ self.precision("base_received_amount"),
+ )
def set_total_allocated_amount(self):
if self.payment_type == "Internal Transfer":
@@ -540,8 +622,9 @@ class PaymentEntry(AccountsController):
for d in self.get("references"):
if d.allocated_amount:
total_allocated_amount += flt(d.allocated_amount)
- base_total_allocated_amount += flt(flt(d.allocated_amount) * flt(d.exchange_rate),
- self.precision("base_paid_amount"))
+ base_total_allocated_amount += flt(
+ flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
+ )
self.total_allocated_amount = abs(total_allocated_amount)
self.base_total_allocated_amount = abs(base_total_allocated_amount)
@@ -551,22 +634,33 @@ class PaymentEntry(AccountsController):
if self.party:
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
included_taxes = self.get_included_taxes()
- if self.payment_type == "Receive" \
- and self.base_total_allocated_amount < self.base_received_amount + total_deductions \
- and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate):
- self.unallocated_amount = (self.base_received_amount + total_deductions -
- self.base_total_allocated_amount) / self.source_exchange_rate
+ if (
+ self.payment_type == "Receive"
+ and self.base_total_allocated_amount < self.base_received_amount + total_deductions
+ and self.total_allocated_amount
+ < self.paid_amount + (total_deductions / self.source_exchange_rate)
+ ):
+ self.unallocated_amount = (
+ self.base_received_amount + total_deductions - self.base_total_allocated_amount
+ ) / self.source_exchange_rate
self.unallocated_amount -= included_taxes
- elif self.payment_type == "Pay" \
- and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \
- and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate):
- self.unallocated_amount = (self.base_paid_amount - (total_deductions +
- self.base_total_allocated_amount)) / self.target_exchange_rate
+ elif (
+ self.payment_type == "Pay"
+ and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions)
+ and self.total_allocated_amount
+ < self.received_amount + (total_deductions / self.target_exchange_rate)
+ ):
+ self.unallocated_amount = (
+ self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)
+ ) / self.target_exchange_rate
self.unallocated_amount -= included_taxes
def set_difference_amount(self):
- base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate)
- if self.payment_type == "Receive" else flt(self.target_exchange_rate))
+ base_unallocated_amount = flt(self.unallocated_amount) * (
+ flt(self.source_exchange_rate)
+ if self.payment_type == "Receive"
+ else flt(self.target_exchange_rate)
+ )
base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount)
@@ -580,14 +674,15 @@ class PaymentEntry(AccountsController):
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
included_taxes = self.get_included_taxes()
- self.difference_amount = flt(self.difference_amount - total_deductions - included_taxes,
- self.precision("difference_amount"))
+ self.difference_amount = flt(
+ self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount")
+ )
def get_included_taxes(self):
included_taxes = 0
- for tax in self.get('taxes'):
+ for tax in self.get("taxes"):
if tax.included_in_paid_amount:
- if tax.add_deduct_tax == 'Add':
+ if tax.add_deduct_tax == "Add":
included_taxes += tax.base_tax_amount
else:
included_taxes -= tax.base_tax_amount
@@ -598,27 +693,41 @@ class PaymentEntry(AccountsController):
# Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast
def clear_unallocated_reference_document_rows(self):
self.set("references", self.get("references", {"allocated_amount": ["not in", [0, None, ""]]}))
- frappe.db.sql("""delete from `tabPayment Entry Reference`
- where parent = %s and allocated_amount = 0""", self.name)
+ frappe.db.sql(
+ """delete from `tabPayment Entry Reference`
+ where parent = %s and allocated_amount = 0""",
+ self.name,
+ )
def validate_payment_against_negative_invoice(self):
- if ((self.payment_type=="Pay" and self.party_type=="Customer")
- or (self.payment_type=="Receive" and self.party_type=="Supplier")):
+ if (self.payment_type == "Pay" and self.party_type == "Customer") or (
+ self.payment_type == "Receive" and self.party_type == "Supplier"
+ ):
- total_negative_outstanding = sum(abs(flt(d.outstanding_amount))
- for d in self.get("references") if flt(d.outstanding_amount) < 0)
+ total_negative_outstanding = sum(
+ abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
+ )
- paid_amount = self.paid_amount if self.payment_type=="Receive" else self.received_amount
+ paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
additional_charges = sum([flt(d.amount) for d in self.deductions])
if not total_negative_outstanding:
- frappe.throw(_("Cannot {0} {1} {2} without any negative outstanding invoice")
- .format(_(self.payment_type), (_("to") if self.party_type=="Customer" else _("from")),
- self.party_type), InvalidPaymentEntry)
+ frappe.throw(
+ _("Cannot {0} {1} {2} without any negative outstanding invoice").format(
+ _(self.payment_type),
+ (_("to") if self.party_type == "Customer" else _("from")),
+ self.party_type,
+ ),
+ InvalidPaymentEntry,
+ )
elif paid_amount - additional_charges > total_negative_outstanding:
- frappe.throw(_("Paid Amount cannot be greater than total negative outstanding amount {0}")
- .format(total_negative_outstanding), InvalidPaymentEntry)
+ frappe.throw(
+ _("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
+ total_negative_outstanding
+ ),
+ InvalidPaymentEntry,
+ )
def set_title(self):
if frappe.flags.in_import and self.title:
@@ -639,33 +748,45 @@ class PaymentEntry(AccountsController):
frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction"))
def set_remarks(self):
- if self.custom_remarks: return
+ if self.custom_remarks:
+ return
- if self.payment_type=="Internal Transfer":
- remarks = [_("Amount {0} {1} transferred from {2} to {3}")
- .format(self.paid_from_account_currency, self.paid_amount, self.paid_from, self.paid_to)]
+ if self.payment_type == "Internal Transfer":
+ remarks = [
+ _("Amount {0} {1} transferred from {2} to {3}").format(
+ self.paid_from_account_currency, self.paid_amount, self.paid_from, self.paid_to
+ )
+ ]
else:
- remarks = [_("Amount {0} {1} {2} {3}").format(
- self.party_account_currency,
- self.paid_amount if self.payment_type=="Receive" else self.received_amount,
- _("received from") if self.payment_type=="Receive" else _("to"), self.party
- )]
+ remarks = [
+ _("Amount {0} {1} {2} {3}").format(
+ self.party_account_currency,
+ self.paid_amount if self.payment_type == "Receive" else self.received_amount,
+ _("received from") if self.payment_type == "Receive" else _("to"),
+ self.party,
+ )
+ ]
if self.reference_no:
- remarks.append(_("Transaction reference no {0} dated {1}")
- .format(self.reference_no, self.reference_date))
+ remarks.append(
+ _("Transaction reference no {0} dated {1}").format(self.reference_no, self.reference_date)
+ )
if self.payment_type in ["Receive", "Pay"]:
for d in self.get("references"):
if d.allocated_amount:
- remarks.append(_("Amount {0} {1} against {2} {3}").format(self.party_account_currency,
- d.allocated_amount, d.reference_doctype, d.reference_name))
+ remarks.append(
+ _("Amount {0} {1} against {2} {3}").format(
+ self.party_account_currency, d.allocated_amount, d.reference_doctype, d.reference_name
+ )
+ )
for d in self.get("deductions"):
if d.amount:
- remarks.append(_("Amount {0} {1} deducted against {2}")
- .format(self.company_currency, d.amount, d.account))
+ remarks.append(
+ _("Amount {0} {1} deducted against {2}").format(self.company_currency, d.amount, d.account)
+ )
self.set("remarks", "\n".join(remarks))
@@ -684,92 +805,110 @@ class PaymentEntry(AccountsController):
def add_party_gl_entries(self, gl_entries):
if self.party_account:
- if self.payment_type=="Receive":
+ if self.payment_type == "Receive":
against_account = self.paid_to
else:
against_account = self.paid_from
- party_gl_dict = self.get_gl_dict({
- "account": self.party_account,
- "party_type": self.party_type,
- "party": self.party,
- "against": against_account,
- "account_currency": self.party_account_currency,
- "cost_center": self.cost_center
- }, item=self)
+ party_gl_dict = self.get_gl_dict(
+ {
+ "account": self.party_account,
+ "party_type": self.party_type,
+ "party": self.party,
+ "against": against_account,
+ "account_currency": self.party_account_currency,
+ "cost_center": self.cost_center,
+ },
+ item=self,
+ )
- dr_or_cr = "credit" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit"
+ dr_or_cr = (
+ "credit" if erpnext.get_party_account_type(self.party_type) == "Receivable" else "debit"
+ )
for d in self.get("references"):
cost_center = self.cost_center
if d.reference_doctype == "Sales Invoice" and not cost_center:
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
gle = party_gl_dict.copy()
- gle.update({
- "against_voucher_type": d.reference_doctype,
- "against_voucher": d.reference_name,
- "cost_center": cost_center
- })
+ gle.update(
+ {
+ "against_voucher_type": d.reference_doctype,
+ "against_voucher": d.reference_name,
+ "cost_center": cost_center,
+ }
+ )
- allocated_amount_in_company_currency = flt(flt(d.allocated_amount) * flt(d.exchange_rate),
- self.precision("paid_amount"))
+ allocated_amount_in_company_currency = flt(
+ flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("paid_amount")
+ )
- gle.update({
- dr_or_cr + "_in_account_currency": d.allocated_amount,
- dr_or_cr: allocated_amount_in_company_currency
- })
+ gle.update(
+ {
+ dr_or_cr + "_in_account_currency": d.allocated_amount,
+ dr_or_cr: allocated_amount_in_company_currency,
+ }
+ )
gl_entries.append(gle)
if self.unallocated_amount:
exchange_rate = self.get_exchange_rate()
- base_unallocated_amount = (self.unallocated_amount * exchange_rate)
+ base_unallocated_amount = self.unallocated_amount * exchange_rate
gle = party_gl_dict.copy()
- gle.update({
- dr_or_cr + "_in_account_currency": self.unallocated_amount,
- dr_or_cr: base_unallocated_amount
- })
+ gle.update(
+ {
+ dr_or_cr + "_in_account_currency": self.unallocated_amount,
+ dr_or_cr: base_unallocated_amount,
+ }
+ )
gl_entries.append(gle)
def add_bank_gl_entries(self, gl_entries):
if self.payment_type in ("Pay", "Internal Transfer"):
gl_entries.append(
- self.get_gl_dict({
- "account": self.paid_from,
- "account_currency": self.paid_from_account_currency,
- "against": self.party if self.payment_type=="Pay" else self.paid_to,
- "credit_in_account_currency": self.paid_amount,
- "credit": self.base_paid_amount,
- "cost_center": self.cost_center,
- "post_net_value": True
- }, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.paid_from,
+ "account_currency": self.paid_from_account_currency,
+ "against": self.party if self.payment_type == "Pay" else self.paid_to,
+ "credit_in_account_currency": self.paid_amount,
+ "credit": self.base_paid_amount,
+ "cost_center": self.cost_center,
+ "post_net_value": True,
+ },
+ item=self,
+ )
)
if self.payment_type in ("Receive", "Internal Transfer"):
gl_entries.append(
- self.get_gl_dict({
- "account": self.paid_to,
- "account_currency": self.paid_to_account_currency,
- "against": self.party if self.payment_type=="Receive" else self.paid_from,
- "debit_in_account_currency": self.received_amount,
- "debit": self.base_received_amount,
- "cost_center": self.cost_center
- }, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.paid_to,
+ "account_currency": self.paid_to_account_currency,
+ "against": self.party if self.payment_type == "Receive" else self.paid_from,
+ "debit_in_account_currency": self.received_amount,
+ "debit": self.base_received_amount,
+ "cost_center": self.cost_center,
+ },
+ item=self,
+ )
)
def add_tax_gl_entries(self, gl_entries):
- for d in self.get('taxes'):
+ for d in self.get("taxes"):
account_currency = get_account_currency(d.account_head)
if account_currency != self.company_currency:
frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency))
- if self.payment_type in ('Pay', 'Internal Transfer'):
+ if self.payment_type in ("Pay", "Internal Transfer"):
dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit"
rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit"
against = self.party or self.paid_from
- elif self.payment_type == 'Receive':
+ elif self.payment_type == "Receive":
dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit"
rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit"
against = self.party or self.paid_to
@@ -779,29 +918,39 @@ class PaymentEntry(AccountsController):
base_tax_amount = d.base_tax_amount
gl_entries.append(
- self.get_gl_dict({
- "account": d.account_head,
- "against": against,
- dr_or_cr: tax_amount,
- dr_or_cr + "_in_account_currency": base_tax_amount
- if account_currency==self.company_currency
- else d.tax_amount,
- "cost_center": d.cost_center,
- "post_net_value": True,
- }, account_currency, item=d))
+ self.get_gl_dict(
+ {
+ "account": d.account_head,
+ "against": against,
+ dr_or_cr: tax_amount,
+ dr_or_cr + "_in_account_currency": base_tax_amount
+ if account_currency == self.company_currency
+ else d.tax_amount,
+ "cost_center": d.cost_center,
+ "post_net_value": True,
+ },
+ account_currency,
+ item=d,
+ )
+ )
if not d.included_in_paid_amount:
gl_entries.append(
- self.get_gl_dict({
- "account": payment_account,
- "against": against,
- rev_dr_or_cr: tax_amount,
- rev_dr_or_cr + "_in_account_currency": base_tax_amount
- if account_currency==self.company_currency
- else d.tax_amount,
- "cost_center": self.cost_center,
- "post_net_value": True,
- }, account_currency, item=d))
+ self.get_gl_dict(
+ {
+ "account": payment_account,
+ "against": against,
+ rev_dr_or_cr: tax_amount,
+ rev_dr_or_cr + "_in_account_currency": base_tax_amount
+ if account_currency == self.company_currency
+ else d.tax_amount,
+ "cost_center": self.cost_center,
+ "post_net_value": True,
+ },
+ account_currency,
+ item=d,
+ )
+ )
def add_deductions_gl_entries(self, gl_entries):
for d in self.get("deductions"):
@@ -811,33 +960,40 @@ class PaymentEntry(AccountsController):
frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency))
gl_entries.append(
- self.get_gl_dict({
- "account": d.account,
- "account_currency": account_currency,
- "against": self.party or self.paid_from,
- "debit_in_account_currency": d.amount,
- "debit": d.amount,
- "cost_center": d.cost_center
- }, item=d)
+ self.get_gl_dict(
+ {
+ "account": d.account,
+ "account_currency": account_currency,
+ "against": self.party or self.paid_from,
+ "debit_in_account_currency": d.amount,
+ "debit": d.amount,
+ "cost_center": d.cost_center,
+ },
+ item=d,
+ )
)
def get_party_account_for_taxes(self):
- if self.payment_type == 'Receive':
+ if self.payment_type == "Receive":
return self.paid_to
- elif self.payment_type in ('Pay', 'Internal Transfer'):
+ elif self.payment_type in ("Pay", "Internal Transfer"):
return self.paid_from
def update_advance_paid(self):
if self.payment_type in ("Receive", "Pay") and self.party:
for d in self.get("references"):
- if d.allocated_amount \
- and d.reference_doctype in ("Sales Order", "Purchase Order", "Employee Advance", "Gratuity"):
- frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid()
+ if d.allocated_amount and d.reference_doctype in (
+ "Sales Order",
+ "Purchase Order",
+ "Employee Advance",
+ "Gratuity",
+ ):
+ frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid()
def update_expense_claim(self):
if self.payment_type in ("Pay") and self.party:
for d in self.get("references"):
- if d.reference_doctype=="Expense Claim" and d.reference_name:
+ if d.reference_doctype == "Expense Claim" and d.reference_name:
doc = frappe.get_doc("Expense Claim", d.reference_name)
if self.docstatus == 2:
update_reimbursed_amount(doc, -1 * d.allocated_amount)
@@ -847,7 +1003,7 @@ class PaymentEntry(AccountsController):
def update_donation(self, cancel=0):
if self.payment_type == "Receive" and self.party_type == "Donor" and self.party:
for d in self.get("references"):
- if d.reference_doctype=="Donation" and d.reference_name:
+ if d.reference_doctype == "Donation" and d.reference_name:
is_paid = 0 if cancel else 1
frappe.db.set_value("Donation", d.reference_name, "paid", is_paid)
@@ -857,31 +1013,29 @@ class PaymentEntry(AccountsController):
def calculate_deductions(self, tax_details):
return {
- "account": tax_details['tax']['account_head'],
- "cost_center": frappe.get_cached_value('Company', self.company, "cost_center"),
- "amount": self.total_allocated_amount * (tax_details['tax']['rate'] / 100)
+ "account": tax_details["tax"]["account_head"],
+ "cost_center": frappe.get_cached_value("Company", self.company, "cost_center"),
+ "amount": self.total_allocated_amount * (tax_details["tax"]["rate"] / 100),
}
def set_gain_or_loss(self, account_details=None):
if not self.difference_amount:
self.set_difference_amount()
- row = {
- 'amount': self.difference_amount
- }
+ row = {"amount": self.difference_amount}
if account_details:
row.update(account_details)
- if not row.get('amount'):
+ if not row.get("amount"):
# if no difference amount
return
- self.append('deductions', row)
+ self.append("deductions", row)
self.set_unallocated_amount()
def get_exchange_rate(self):
- return self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate
+ return self.source_exchange_rate if self.payment_type == "Receive" else self.target_exchange_rate
def initialize_taxes(self):
for tax in self.get("taxes"):
@@ -905,25 +1059,31 @@ class PaymentEntry(AccountsController):
cumulated_tax_fraction = 0
for i, tax in enumerate(self.get("taxes")):
tax.tax_fraction_for_current_item = self.get_current_tax_fraction(tax)
- if i==0:
+ if i == 0:
tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item
else:
- tax.grand_total_fraction_for_current_item = \
- self.get("taxes")[i-1].grand_total_fraction_for_current_item \
+ tax.grand_total_fraction_for_current_item = (
+ self.get("taxes")[i - 1].grand_total_fraction_for_current_item
+ tax.tax_fraction_for_current_item
+ )
cumulated_tax_fraction += tax.tax_fraction_for_current_item
- self.paid_amount_after_tax = flt(self.paid_amount/(1+cumulated_tax_fraction))
+ self.paid_amount_after_tax = flt(self.paid_amount / (1 + cumulated_tax_fraction))
def calculate_taxes(self):
self.total_taxes_and_charges = 0.0
self.base_total_taxes_and_charges = 0.0
- actual_tax_dict = dict([[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))]
- for tax in self.get("taxes") if tax.charge_type == "Actual"])
+ actual_tax_dict = dict(
+ [
+ [tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))]
+ for tax in self.get("taxes")
+ if tax.charge_type == "Actual"
+ ]
+ )
- for i, tax in enumerate(self.get('taxes')):
+ for i, tax in enumerate(self.get("taxes")):
current_tax_amount = self.get_current_tax_amount(tax)
if tax.charge_type == "Actual":
@@ -942,19 +1102,21 @@ class PaymentEntry(AccountsController):
if i == 0:
tax.total = flt(self.paid_amount_after_tax + current_tax_amount, self.precision("total", tax))
else:
- tax.total = flt(self.get('taxes')[i-1].total + current_tax_amount, self.precision("total", tax))
+ tax.total = flt(
+ self.get("taxes")[i - 1].total + current_tax_amount, self.precision("total", tax)
+ )
tax.base_total = tax.total * self.source_exchange_rate
- if self.payment_type == 'Pay':
+ if self.payment_type == "Pay":
self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
else:
self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
- if self.get('taxes'):
- self.paid_amount_after_tax = self.get('taxes')[-1].base_total
+ if self.get("taxes"):
+ self.paid_amount_after_tax = self.get("taxes")[-1].base_total
def get_current_tax_amount(self, tax):
tax_rate = tax.rate
@@ -962,7 +1124,11 @@ class PaymentEntry(AccountsController):
# To set row_id by default as previous row.
if tax.charge_type in ["On Previous Row Amount", "On Previous Row Total"]:
if tax.idx == 1:
- frappe.throw(_("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"))
+ frappe.throw(
+ _(
+ "Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"
+ )
+ )
if not tax.row_id:
tax.row_id = tax.idx - 1
@@ -972,12 +1138,10 @@ class PaymentEntry(AccountsController):
elif tax.charge_type == "On Paid Amount":
current_tax_amount = (tax_rate / 100.0) * self.paid_amount_after_tax
elif tax.charge_type == "On Previous Row Amount":
- current_tax_amount = (tax_rate / 100.0) * \
- self.get('taxes')[cint(tax.row_id) - 1].tax_amount
+ current_tax_amount = (tax_rate / 100.0) * self.get("taxes")[cint(tax.row_id) - 1].tax_amount
elif tax.charge_type == "On Previous Row Total":
- current_tax_amount = (tax_rate / 100.0) * \
- self.get('taxes')[cint(tax.row_id) - 1].total
+ current_tax_amount = (tax_rate / 100.0) * self.get("taxes")[cint(tax.row_id) - 1].total
return current_tax_amount
@@ -990,83 +1154,106 @@ class PaymentEntry(AccountsController):
if tax.charge_type == "On Paid Amount":
current_tax_fraction = tax_rate / 100.0
elif tax.charge_type == "On Previous Row Amount":
- current_tax_fraction = (tax_rate / 100.0) * \
- self.get("taxes")[cint(tax.row_id) - 1].tax_fraction_for_current_item
+ current_tax_fraction = (tax_rate / 100.0) * self.get("taxes")[
+ cint(tax.row_id) - 1
+ ].tax_fraction_for_current_item
elif tax.charge_type == "On Previous Row Total":
- current_tax_fraction = (tax_rate / 100.0) * \
- self.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item
+ current_tax_fraction = (tax_rate / 100.0) * self.get("taxes")[
+ cint(tax.row_id) - 1
+ ].grand_total_fraction_for_current_item
if getattr(tax, "add_deduct_tax", None) and tax.add_deduct_tax == "Deduct":
current_tax_fraction *= -1.0
return current_tax_fraction
+
def validate_inclusive_tax(tax, doc):
def _on_previous_row_error(row_range):
- throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range))
+ throw(
+ _("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(
+ tax.idx, row_range
+ )
+ )
if cint(getattr(tax, "included_in_paid_amount", None)):
if tax.charge_type == "Actual":
# inclusive tax cannot be of type Actual
- throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx))
- elif tax.charge_type == "On Previous Row Amount" and \
- not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_paid_amount):
+ throw(
+ _("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(
+ tax.idx
+ )
+ )
+ elif tax.charge_type == "On Previous Row Amount" and not cint(
+ doc.get("taxes")[cint(tax.row_id) - 1].included_in_paid_amount
+ ):
# referred row should also be inclusive
_on_previous_row_error(tax.row_id)
- elif tax.charge_type == "On Previous Row Total" and \
- not all([cint(t.included_in_paid_amount for t in doc.get("taxes")[:cint(tax.row_id) - 1])]):
+ elif tax.charge_type == "On Previous Row Total" and not all(
+ [cint(t.included_in_paid_amount for t in doc.get("taxes")[: cint(tax.row_id) - 1])]
+ ):
# all rows about the referred tax should be inclusive
_on_previous_row_error("1 - %d" % (cint(tax.row_id),))
elif tax.get("category") == "Valuation":
frappe.throw(_("Valuation type charges can not be marked as Inclusive"))
+
@frappe.whitelist()
def get_outstanding_reference_documents(args):
if isinstance(args, string_types):
args = json.loads(args)
- if args.get('party_type') == 'Member':
+ if args.get("party_type") == "Member":
return
# confirm that Supplier is not blocked
- if args.get('party_type') == 'Supplier':
- supplier_status = get_supplier_block_status(args['party'])
- if supplier_status['on_hold']:
- if supplier_status['hold_type'] == 'All':
+ if args.get("party_type") == "Supplier":
+ supplier_status = get_supplier_block_status(args["party"])
+ if supplier_status["on_hold"]:
+ if supplier_status["hold_type"] == "All":
return []
- elif supplier_status['hold_type'] == 'Payments':
- if not supplier_status['release_date'] or getdate(nowdate()) <= supplier_status['release_date']:
+ elif supplier_status["hold_type"] == "Payments":
+ if (
+ not supplier_status["release_date"] or getdate(nowdate()) <= supplier_status["release_date"]
+ ):
return []
party_account_currency = get_account_currency(args.get("party_account"))
- company_currency = frappe.get_cached_value('Company', args.get("company"), "default_currency")
+ company_currency = frappe.get_cached_value("Company", args.get("company"), "default_currency")
# Get positive outstanding sales /purchase invoices/ Fees
condition = ""
if args.get("voucher_type") and args.get("voucher_no"):
- condition = " and voucher_type={0} and voucher_no={1}"\
- .format(frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"]))
+ condition = " and voucher_type={0} and voucher_no={1}".format(
+ frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
+ )
# Add cost center condition
if args.get("cost_center"):
condition += " and cost_center='%s'" % args.get("cost_center")
date_fields_dict = {
- 'posting_date': ['from_posting_date', 'to_posting_date'],
- 'due_date': ['from_due_date', 'to_due_date']
+ "posting_date": ["from_posting_date", "to_posting_date"],
+ "due_date": ["from_due_date", "to_due_date"],
}
for fieldname, date_fields in date_fields_dict.items():
if args.get(date_fields[0]) and args.get(date_fields[1]):
- condition += " and {0} between '{1}' and '{2}'".format(fieldname,
- args.get(date_fields[0]), args.get(date_fields[1]))
+ condition += " and {0} between '{1}' and '{2}'".format(
+ fieldname, args.get(date_fields[0]), args.get(date_fields[1])
+ )
if args.get("company"):
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
- outstanding_invoices = get_outstanding_invoices(args.get("party_type"), args.get("party"),
- args.get("party_account"), filters=args, condition=condition)
+ outstanding_invoices = get_outstanding_invoices(
+ args.get("party_type"),
+ args.get("party"),
+ args.get("party_account"),
+ filters=args,
+ condition=condition,
+ )
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
@@ -1077,28 +1264,44 @@ def get_outstanding_reference_documents(args):
d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate")
elif d.voucher_type == "Journal Entry":
d["exchange_rate"] = get_exchange_rate(
- party_account_currency, company_currency, d.posting_date
+ party_account_currency, company_currency, d.posting_date
)
if d.voucher_type in ("Purchase Invoice"):
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
# Get all SO / PO which are not fully billed or against which full advance not paid
orders_to_be_billed = []
- if (args.get("party_type") != "Student"):
- orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),
- args.get("party"), args.get("company"), party_account_currency, company_currency, filters=args)
+ if args.get("party_type") != "Student":
+ orders_to_be_billed = get_orders_to_be_billed(
+ args.get("posting_date"),
+ args.get("party_type"),
+ args.get("party"),
+ args.get("company"),
+ party_account_currency,
+ company_currency,
+ filters=args,
+ )
# Get negative outstanding sales /purchase invoices
negative_outstanding_invoices = []
if args.get("party_type") not in ["Student", "Employee"] and not args.get("voucher_no"):
- negative_outstanding_invoices = get_negative_outstanding_invoices(args.get("party_type"), args.get("party"),
- args.get("party_account"), party_account_currency, company_currency, condition=condition)
+ negative_outstanding_invoices = get_negative_outstanding_invoices(
+ args.get("party_type"),
+ args.get("party"),
+ args.get("party_account"),
+ party_account_currency,
+ company_currency,
+ condition=condition,
+ )
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
if not data:
- frappe.msgprint(_("No outstanding invoices found for the {0} {1} which qualify the filters you have specified.")
- .format(_(args.get("party_type")).lower(), frappe.bold(args.get("party"))))
+ frappe.msgprint(
+ _(
+ "No outstanding invoices found for the {0} {1} which qualify the filters you have specified."
+ ).format(_(args.get("party_type")).lower(), frappe.bold(args.get("party")))
+ )
return data
@@ -1106,53 +1309,75 @@ def get_outstanding_reference_documents(args):
def split_invoices_based_on_payment_terms(outstanding_invoices):
invoice_ref_based_on_payment_terms = {}
for idx, d in enumerate(outstanding_invoices):
- if d.voucher_type in ['Sales Invoice', 'Purchase Invoice']:
- payment_term_template = frappe.db.get_value(d.voucher_type, d.voucher_no, 'payment_terms_template')
+ if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
+ payment_term_template = frappe.db.get_value(
+ d.voucher_type, d.voucher_no, "payment_terms_template"
+ )
if payment_term_template:
allocate_payment_based_on_payment_terms = frappe.db.get_value(
- 'Payment Terms Template', payment_term_template, 'allocate_payment_based_on_payment_terms')
+ "Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
+ )
if allocate_payment_based_on_payment_terms:
- payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': d.voucher_no}, fields=["*"])
+ payment_schedule = frappe.get_all(
+ "Payment Schedule", filters={"parent": d.voucher_no}, fields=["*"]
+ )
for payment_term in payment_schedule:
if payment_term.outstanding > 0.1:
invoice_ref_based_on_payment_terms.setdefault(idx, [])
- invoice_ref_based_on_payment_terms[idx].append(frappe._dict({
- 'due_date': d.due_date,
- 'currency': d.currency,
- 'voucher_no': d.voucher_no,
- 'voucher_type': d.voucher_type,
- 'posting_date': d.posting_date,
- 'invoice_amount': flt(d.invoice_amount),
- 'outstanding_amount': flt(d.outstanding_amount),
- 'payment_amount': payment_term.payment_amount,
- 'payment_term': payment_term.payment_term
- }))
+ invoice_ref_based_on_payment_terms[idx].append(
+ frappe._dict(
+ {
+ "due_date": d.due_date,
+ "currency": d.currency,
+ "voucher_no": d.voucher_no,
+ "voucher_type": d.voucher_type,
+ "posting_date": d.posting_date,
+ "invoice_amount": flt(d.invoice_amount),
+ "outstanding_amount": flt(d.outstanding_amount),
+ "payment_amount": payment_term.payment_amount,
+ "payment_term": payment_term.payment_term,
+ }
+ )
+ )
outstanding_invoices_after_split = []
if invoice_ref_based_on_payment_terms:
for idx, ref in invoice_ref_based_on_payment_terms.items():
- voucher_no = ref[0]['voucher_no']
- voucher_type = ref[0]['voucher_type']
+ voucher_no = ref[0]["voucher_no"]
+ voucher_type = ref[0]["voucher_type"]
- frappe.msgprint(_("Spliting {} {} into {} row(s) as per Payment Terms").format(
- voucher_type, voucher_no, len(ref)), alert=True)
+ frappe.msgprint(
+ _("Spliting {} {} into {} row(s) as per Payment Terms").format(
+ voucher_type, voucher_no, len(ref)
+ ),
+ alert=True,
+ )
outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx]
- existing_row = list(filter(lambda x: x.get('voucher_no') == voucher_no, outstanding_invoices))
+ existing_row = list(filter(lambda x: x.get("voucher_no") == voucher_no, outstanding_invoices))
index = outstanding_invoices.index(existing_row[0])
outstanding_invoices.pop(index)
outstanding_invoices_after_split += outstanding_invoices
return outstanding_invoices_after_split
-def get_orders_to_be_billed(posting_date, party_type, party,
- company, party_account_currency, company_currency, cost_center=None, filters=None):
+
+def get_orders_to_be_billed(
+ posting_date,
+ party_type,
+ party,
+ company,
+ party_account_currency,
+ company_currency,
+ cost_center=None,
+ filters=None,
+):
if party_type == "Customer":
- voucher_type = 'Sales Order'
+ voucher_type = "Sales Order"
elif party_type == "Supplier":
- voucher_type = 'Purchase Order'
+ voucher_type = "Purchase Order"
elif party_type == "Employee":
voucher_type = None
@@ -1160,7 +1385,7 @@ def get_orders_to_be_billed(posting_date, party_type, party,
if voucher_type:
doc = frappe.get_doc({"doctype": voucher_type})
condition = ""
- if doc and hasattr(doc, 'cost_center'):
+ if doc and hasattr(doc, "cost_center"):
condition = " and cost_center='%s'" % cost_center
orders = []
@@ -1172,7 +1397,8 @@ def get_orders_to_be_billed(posting_date, party_type, party,
grand_total_field = "grand_total"
rounded_total_field = "rounded_total"
- orders = frappe.db.sql("""
+ orders = frappe.db.sql(
+ """
select
name as voucher_no,
if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount,
@@ -1190,18 +1416,25 @@ def get_orders_to_be_billed(posting_date, party_type, party,
{condition}
order by
transaction_date, name
- """.format(**{
- "rounded_total_field": rounded_total_field,
- "grand_total_field": grand_total_field,
- "voucher_type": voucher_type,
- "party_type": scrub(party_type),
- "condition": condition
- }), (party, company), as_dict=True)
+ """.format(
+ **{
+ "rounded_total_field": rounded_total_field,
+ "grand_total_field": grand_total_field,
+ "voucher_type": voucher_type,
+ "party_type": scrub(party_type),
+ "condition": condition,
+ }
+ ),
+ (party, company),
+ as_dict=True,
+ )
order_list = []
for d in orders:
- if not (flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than"))
- and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than"))):
+ if not (
+ flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than"))
+ and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than"))
+ ):
continue
d["voucher_type"] = voucher_type
@@ -1211,8 +1444,16 @@ def get_orders_to_be_billed(posting_date, party_type, party,
return order_list
-def get_negative_outstanding_invoices(party_type, party, party_account,
- party_account_currency, company_currency, cost_center=None, condition=None):
+
+def get_negative_outstanding_invoices(
+ party_type,
+ party,
+ party_account,
+ party_account_currency,
+ company_currency,
+ cost_center=None,
+ condition=None,
+):
voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
supplier_condition = ""
if voucher_type == "Purchase Invoice":
@@ -1224,7 +1465,8 @@ def get_negative_outstanding_invoices(party_type, party, party_account,
grand_total_field = "grand_total"
rounded_total_field = "rounded_total"
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
select
"{voucher_type}" as voucher_type, name as voucher_no,
if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount,
@@ -1239,21 +1481,26 @@ def get_negative_outstanding_invoices(party_type, party, party_account,
{condition}
order by
posting_date, name
- """.format(**{
- "supplier_condition": supplier_condition,
- "condition": condition,
- "rounded_total_field": rounded_total_field,
- "grand_total_field": grand_total_field,
- "voucher_type": voucher_type,
- "party_type": scrub(party_type),
- "party_account": "debit_to" if party_type == "Customer" else "credit_to",
- "cost_center": cost_center
- }), (party, party_account), as_dict=True)
+ """.format(
+ **{
+ "supplier_condition": supplier_condition,
+ "condition": condition,
+ "rounded_total_field": rounded_total_field,
+ "grand_total_field": grand_total_field,
+ "voucher_type": voucher_type,
+ "party_type": scrub(party_type),
+ "party_account": "debit_to" if party_type == "Customer" else "credit_to",
+ "cost_center": cost_center,
+ }
+ ),
+ (party, party_account),
+ as_dict=True,
+ )
@frappe.whitelist()
def get_party_details(company, party_type, party, date, cost_center=None):
- bank_account = ''
+ bank_account = ""
if not frappe.db.exists(party_type, party):
frappe.throw(_("Invalid {0}: {1}").format(party_type, party))
@@ -1261,7 +1508,9 @@ def get_party_details(company, party_type, party, date, cost_center=None):
account_currency = get_account_currency(party_account)
account_balance = get_balance_on(party_account, date, cost_center=cost_center)
- _party_name = "title" if party_type in ("Student", "Shareholder") else party_type.lower() + "_name"
+ _party_name = (
+ "title" if party_type in ("Student", "Shareholder") else party_type.lower() + "_name"
+ )
party_name = frappe.db.get_value(party_type, party, _party_name)
party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center)
if party_type in ["Customer", "Supplier"]:
@@ -1273,61 +1522,68 @@ def get_party_details(company, party_type, party, date, cost_center=None):
"party_account_currency": account_currency,
"party_balance": party_balance,
"account_balance": account_balance,
- "bank_account": bank_account
+ "bank_account": bank_account,
}
@frappe.whitelist()
def get_account_details(account, date, cost_center=None):
- frappe.has_permission('Payment Entry', throw=True)
+ frappe.has_permission("Payment Entry", throw=True)
# to check if the passed account is accessible under reference doctype Payment Entry
- account_list = frappe.get_list('Account', {
- 'name': account
- }, reference_doctype='Payment Entry', limit=1)
+ account_list = frappe.get_list(
+ "Account", {"name": account}, reference_doctype="Payment Entry", limit=1
+ )
# There might be some user permissions which will allow account under certain doctypes
# except for Payment Entry, only in such case we should throw permission error
if not account_list:
- frappe.throw(_('Account: {0} is not permitted under Payment Entry').format(account))
+ frappe.throw(_("Account: {0} is not permitted under Payment Entry").format(account))
- account_balance = get_balance_on(account, date, cost_center=cost_center,
- ignore_account_permission=True)
+ account_balance = get_balance_on(
+ account, date, cost_center=cost_center, ignore_account_permission=True
+ )
- return frappe._dict({
- "account_currency": get_account_currency(account),
- "account_balance": account_balance,
- "account_type": frappe.db.get_value("Account", account, "account_type")
- })
+ return frappe._dict(
+ {
+ "account_currency": get_account_currency(account),
+ "account_balance": account_balance,
+ "account_type": frappe.db.get_value("Account", account, "account_type"),
+ }
+ )
@frappe.whitelist()
def get_company_defaults(company):
fields = ["write_off_account", "exchange_gain_loss_account", "cost_center"]
- ret = frappe.get_cached_value('Company', company, fields, as_dict=1)
+ ret = frappe.get_cached_value("Company", company, fields, as_dict=1)
for fieldname in fields:
if not ret[fieldname]:
- frappe.throw(_("Please set default {0} in Company {1}")
- .format(frappe.get_meta("Company").get_label(fieldname), company))
+ frappe.throw(
+ _("Please set default {0} in Company {1}").format(
+ frappe.get_meta("Company").get_label(fieldname), company
+ )
+ )
return ret
def get_outstanding_on_journal_entry(name):
res = frappe.db.sql(
- 'SELECT '
- 'CASE WHEN party_type IN ("Customer", "Student") '
- 'THEN ifnull(sum(debit_in_account_currency - credit_in_account_currency), 0) '
- 'ELSE ifnull(sum(credit_in_account_currency - debit_in_account_currency), 0) '
- 'END as outstanding_amount '
- 'FROM `tabGL Entry` WHERE (voucher_no=%s OR against_voucher=%s) '
- 'AND party_type IS NOT NULL '
- 'AND party_type != ""',
- (name, name), as_dict=1
- )
+ "SELECT "
+ 'CASE WHEN party_type IN ("Customer", "Student") '
+ "THEN ifnull(sum(debit_in_account_currency - credit_in_account_currency), 0) "
+ "ELSE ifnull(sum(credit_in_account_currency - debit_in_account_currency), 0) "
+ "END as outstanding_amount "
+ "FROM `tabGL Entry` WHERE (voucher_no=%s OR against_voucher=%s) "
+ "AND party_type IS NOT NULL "
+ 'AND party_type != ""',
+ (name, name),
+ as_dict=1,
+ )
- outstanding_amount = res[0].get('outstanding_amount', 0) if res else 0
+ outstanding_amount = res[0].get("outstanding_amount", 0) if res else 0
return outstanding_amount
@@ -1336,7 +1592,9 @@ def get_outstanding_on_journal_entry(name):
def get_reference_details(reference_doctype, reference_name, party_account_currency):
total_amount = outstanding_amount = exchange_rate = bill_no = None
ref_doc = frappe.get_doc(reference_doctype, reference_name)
- company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company)
+ company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(
+ ref_doc.company
+ )
if reference_doctype == "Fees":
total_amount = ref_doc.get("grand_total")
@@ -1353,20 +1611,22 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
total_amount = ref_doc.get("total_amount")
if ref_doc.multi_currency:
- exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
+ exchange_rate = get_exchange_rate(
+ party_account_currency, company_currency, ref_doc.posting_date
+ )
else:
exchange_rate = 1
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
elif reference_doctype != "Journal Entry":
if ref_doc.doctype == "Expense Claim":
- total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
+ total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
elif ref_doc.doctype == "Employee Advance":
total_amount = ref_doc.advance_amount
exchange_rate = ref_doc.get("exchange_rate")
if party_account_currency != ref_doc.currency:
total_amount = flt(total_amount) * flt(exchange_rate)
elif ref_doc.doctype == "Gratuity":
- total_amount = ref_doc.amount
+ total_amount = ref_doc.amount
if not total_amount:
if party_account_currency == company_currency:
total_amount = ref_doc.base_grand_total
@@ -1376,16 +1636,21 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
if not exchange_rate:
# Get the exchange rate from the original ref doc
# or get it based on the posting date of the ref doc.
- exchange_rate = ref_doc.get("conversion_rate") or \
- get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
+ exchange_rate = ref_doc.get("conversion_rate") or get_exchange_rate(
+ party_account_currency, company_currency, ref_doc.posting_date
+ )
if reference_doctype in ("Sales Invoice", "Purchase Invoice"):
outstanding_amount = ref_doc.get("outstanding_amount")
bill_no = ref_doc.get("bill_no")
elif reference_doctype == "Expense Claim":
- outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\
- - flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount"))
+ outstanding_amount = (
+ flt(ref_doc.get("total_sanctioned_amount"))
+ + flt(ref_doc.get("total_taxes_and_charges"))
+ - flt(ref_doc.get("total_amount_reimbursed"))
+ - flt(ref_doc.get("total_advance_amount"))
+ )
elif reference_doctype == "Employee Advance":
- outstanding_amount = (flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount))
+ outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)
if party_account_currency != ref_doc.currency:
outstanding_amount = flt(outstanding_amount) * flt(exchange_rate)
if party_account_currency == company_currency:
@@ -1396,18 +1661,22 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid)
else:
# Get the exchange rate based on the posting date of the ref doc.
- exchange_rate = get_exchange_rate(party_account_currency,
- company_currency, ref_doc.posting_date)
+ exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
- return frappe._dict({
- "due_date": ref_doc.get("due_date"),
- "total_amount": flt(total_amount),
- "outstanding_amount": flt(outstanding_amount),
- "exchange_rate": flt(exchange_rate),
- "bill_no": bill_no
- })
+ return frappe._dict(
+ {
+ "due_date": ref_doc.get("due_date"),
+ "total_amount": flt(total_amount),
+ "outstanding_amount": flt(outstanding_amount),
+ "exchange_rate": flt(exchange_rate),
+ "bill_no": bill_no,
+ }
+ )
-def get_amounts_based_on_reference_doctype(reference_doctype, ref_doc, party_account_currency, company_currency, reference_name):
+
+def get_amounts_based_on_reference_doctype(
+ reference_doctype, ref_doc, party_account_currency, company_currency, reference_name
+):
total_amount = outstanding_amount = exchange_rate = None
if reference_doctype == "Fees":
total_amount = ref_doc.get("grand_total")
@@ -1420,35 +1689,46 @@ def get_amounts_based_on_reference_doctype(reference_doctype, ref_doc, party_acc
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
total_amount = ref_doc.get("total_amount")
if ref_doc.multi_currency:
- exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
+ exchange_rate = get_exchange_rate(
+ party_account_currency, company_currency, ref_doc.posting_date
+ )
else:
exchange_rate = 1
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
return total_amount, outstanding_amount, exchange_rate
-def get_amounts_based_on_ref_doc(reference_doctype, ref_doc, party_account_currency, company_currency):
+
+def get_amounts_based_on_ref_doc(
+ reference_doctype, ref_doc, party_account_currency, company_currency
+):
total_amount = outstanding_amount = exchange_rate = None
if ref_doc.doctype == "Expense Claim":
- total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
+ total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
elif ref_doc.doctype == "Employee Advance":
- total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc)
+ total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance(
+ party_account_currency, ref_doc
+ )
if not total_amount:
total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency(
- party_account_currency, company_currency, ref_doc)
+ party_account_currency, company_currency, ref_doc
+ )
if not exchange_rate:
# Get the exchange rate from the original ref doc
# or get it based on the posting date of the ref doc
- exchange_rate = ref_doc.get("conversion_rate") or \
- get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
+ exchange_rate = ref_doc.get("conversion_rate") or get_exchange_rate(
+ party_account_currency, company_currency, ref_doc.posting_date
+ )
outstanding_amount, exchange_rate, bill_no = get_bill_no_and_update_amounts(
- reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency)
+ reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency
+ )
return total_amount, outstanding_amount, exchange_rate, bill_no
+
def get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc):
total_amount = ref_doc.advance_amount
exchange_rate = ref_doc.get("exchange_rate")
@@ -1457,7 +1737,10 @@ def get_total_amount_exchange_rate_for_employee_advance(party_account_currency,
return total_amount, exchange_rate
-def get_total_amount_exchange_rate_base_on_currency(party_account_currency, company_currency, ref_doc):
+
+def get_total_amount_exchange_rate_base_on_currency(
+ party_account_currency, company_currency, ref_doc
+):
exchange_rate = None
if party_account_currency == company_currency:
total_amount = ref_doc.base_grand_total
@@ -1467,16 +1750,23 @@ def get_total_amount_exchange_rate_base_on_currency(party_account_currency, comp
return total_amount, exchange_rate
-def get_bill_no_and_update_amounts(reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency):
+
+def get_bill_no_and_update_amounts(
+ reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency
+):
outstanding_amount = bill_no = None
if reference_doctype in ("Sales Invoice", "Purchase Invoice"):
outstanding_amount = ref_doc.get("outstanding_amount")
bill_no = ref_doc.get("bill_no")
elif reference_doctype == "Expense Claim":
- outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\
- - flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount"))
+ outstanding_amount = (
+ flt(ref_doc.get("total_sanctioned_amount"))
+ + flt(ref_doc.get("total_taxes_and_charges"))
+ - flt(ref_doc.get("total_amount_reimbursed"))
+ - flt(ref_doc.get("total_advance_amount"))
+ )
elif reference_doctype == "Employee Advance":
- outstanding_amount = (flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount))
+ outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)
if party_account_currency != ref_doc.currency:
outstanding_amount = flt(outstanding_amount) * flt(exchange_rate)
if party_account_currency == company_currency:
@@ -1498,15 +1788,20 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
party_account = set_party_account(dt, dn, doc, party_type)
party_account_currency = set_party_account_currency(dt, party_account, doc)
payment_type = set_payment_type(dt, doc)
- grand_total, outstanding_amount = set_grand_total_and_outstanding_amount(party_amount, dt, party_account_currency, doc)
+ grand_total, outstanding_amount = set_grand_total_and_outstanding_amount(
+ party_amount, dt, party_account_currency, doc
+ )
# bank or cash
bank = get_bank_cash_account(doc, bank_account)
paid_amount, received_amount = set_paid_amount_and_received_amount(
- dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc)
+ dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
+ )
- paid_amount, received_amount, discount_amount = apply_early_payment_discount(paid_amount, received_amount, doc)
+ paid_amount, received_amount, discount_amount = apply_early_payment_discount(
+ paid_amount, received_amount, doc
+ )
pe = frappe.new_doc("Payment Entry")
pe.payment_type = payment_type
@@ -1520,18 +1815,22 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.contact_email = doc.get("contact_email")
pe.ensure_supplier_is_not_blocked()
- pe.paid_from = party_account if payment_type=="Receive" else bank.account
- pe.paid_to = party_account if payment_type=="Pay" else bank.account
- pe.paid_from_account_currency = party_account_currency \
- if payment_type=="Receive" else bank.account_currency
- pe.paid_to_account_currency = party_account_currency if payment_type=="Pay" else bank.account_currency
+ pe.paid_from = party_account if payment_type == "Receive" else bank.account
+ pe.paid_to = party_account if payment_type == "Pay" else bank.account
+ pe.paid_from_account_currency = (
+ party_account_currency if payment_type == "Receive" else bank.account_currency
+ )
+ pe.paid_to_account_currency = (
+ party_account_currency if payment_type == "Pay" else bank.account_currency
+ )
pe.paid_amount = paid_amount
pe.received_amount = received_amount
pe.letter_head = doc.get("letter_head")
- if dt in ['Purchase Order', 'Sales Order', 'Sales Invoice', 'Purchase Invoice']:
- pe.project = (doc.get('project') or
- reduce(lambda prev,cur: prev or cur, [x.get('project') for x in doc.get('items')], None)) # get first non-empty project from items
+ if dt in ["Purchase Order", "Sales Order", "Sales Invoice", "Purchase Invoice"]:
+ pe.project = doc.get("project") or reduce(
+ lambda prev, cur: prev or cur, [x.get("project") for x in doc.get("items")], None
+ ) # get first non-empty project from items
if pe.party_type in ["Customer", "Supplier"]:
bank_account = get_party_bank_account(pe.party_type, pe.party)
@@ -1540,44 +1839,57 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
# only Purchase Invoice can be blocked individually
if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked():
- frappe.msgprint(_('{0} is on hold till {1}').format(doc.name, doc.release_date))
+ frappe.msgprint(_("{0} is on hold till {1}").format(doc.name, doc.release_date))
else:
- if (doc.doctype in ('Sales Invoice', 'Purchase Invoice')
- and frappe.get_value('Payment Terms Template',
- {'name': doc.payment_terms_template}, 'allocate_payment_based_on_payment_terms')):
+ if doc.doctype in ("Sales Invoice", "Purchase Invoice") and frappe.get_value(
+ "Payment Terms Template",
+ {"name": doc.payment_terms_template},
+ "allocate_payment_based_on_payment_terms",
+ ):
- for reference in get_reference_as_per_payment_terms(doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount):
- pe.append('references', reference)
+ for reference in get_reference_as_per_payment_terms(
+ doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount
+ ):
+ pe.append("references", reference)
else:
if dt == "Dunning":
- pe.append("references", {
- 'reference_doctype': 'Sales Invoice',
- 'reference_name': doc.get('sales_invoice'),
- "bill_no": doc.get("bill_no"),
- "due_date": doc.get("due_date"),
- 'total_amount': doc.get('outstanding_amount'),
- 'outstanding_amount': doc.get('outstanding_amount'),
- 'allocated_amount': doc.get('outstanding_amount')
- })
- pe.append("references", {
- 'reference_doctype': dt,
- 'reference_name': dn,
- "bill_no": doc.get("bill_no"),
- "due_date": doc.get("due_date"),
- 'total_amount': doc.get('dunning_amount'),
- 'outstanding_amount': doc.get('dunning_amount'),
- 'allocated_amount': doc.get('dunning_amount')
- })
+ pe.append(
+ "references",
+ {
+ "reference_doctype": "Sales Invoice",
+ "reference_name": doc.get("sales_invoice"),
+ "bill_no": doc.get("bill_no"),
+ "due_date": doc.get("due_date"),
+ "total_amount": doc.get("outstanding_amount"),
+ "outstanding_amount": doc.get("outstanding_amount"),
+ "allocated_amount": doc.get("outstanding_amount"),
+ },
+ )
+ pe.append(
+ "references",
+ {
+ "reference_doctype": dt,
+ "reference_name": dn,
+ "bill_no": doc.get("bill_no"),
+ "due_date": doc.get("due_date"),
+ "total_amount": doc.get("dunning_amount"),
+ "outstanding_amount": doc.get("dunning_amount"),
+ "allocated_amount": doc.get("dunning_amount"),
+ },
+ )
else:
- pe.append("references", {
- 'reference_doctype': dt,
- 'reference_name': dn,
- "bill_no": doc.get("bill_no"),
- "due_date": doc.get("due_date"),
- 'total_amount': grand_total,
- 'outstanding_amount': outstanding_amount,
- 'allocated_amount': outstanding_amount
- })
+ pe.append(
+ "references",
+ {
+ "reference_doctype": dt,
+ "reference_name": dn,
+ "bill_no": doc.get("bill_no"),
+ "due_date": doc.get("due_date"),
+ "total_amount": grand_total,
+ "outstanding_amount": outstanding_amount,
+ "allocated_amount": outstanding_amount,
+ },
+ )
pe.setup_party_account_field()
pe.set_missing_values()
@@ -1588,25 +1900,32 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts()
if discount_amount:
- pe.set_gain_or_loss(account_details={
- 'account': frappe.get_cached_value('Company', pe.company, "default_discount_account"),
- 'cost_center': pe.cost_center or frappe.get_cached_value('Company', pe.company, "cost_center"),
- 'amount': discount_amount * (-1 if payment_type == "Pay" else 1)
- })
+ pe.set_gain_or_loss(
+ account_details={
+ "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
+ "cost_center": pe.cost_center
+ or frappe.get_cached_value("Company", pe.company, "cost_center"),
+ "amount": discount_amount * (-1 if payment_type == "Pay" else 1),
+ }
+ )
pe.set_difference_amount()
return pe
+
def get_bank_cash_account(doc, bank_account):
- bank = get_default_bank_cash_account(doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"),
- account=bank_account)
+ bank = get_default_bank_cash_account(
+ doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account
+ )
if not bank:
- bank = get_default_bank_cash_account(doc.company, "Cash", mode_of_payment=doc.get("mode_of_payment"),
- account=bank_account)
+ bank = get_default_bank_cash_account(
+ doc.company, "Cash", mode_of_payment=doc.get("mode_of_payment"), account=bank_account
+ )
return bank
+
def set_party_type(dt):
if dt in ("Sales Invoice", "Sales Order", "Dunning"):
party_type = "Customer"
@@ -1620,6 +1939,7 @@ def set_party_type(dt):
party_type = "Donor"
return party_type
+
def set_party_account(dt, dn, doc, party_type):
if dt == "Sales Invoice":
party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to
@@ -1637,6 +1957,7 @@ def set_party_account(dt, dn, doc, party_type):
party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company)
return party_account
+
def set_party_account_currency(dt, party_account, doc):
if dt not in ("Sales Invoice", "Purchase Invoice"):
party_account_currency = get_account_currency(party_account)
@@ -1644,14 +1965,18 @@ def set_party_account_currency(dt, party_account, doc):
party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account)
return party_account_currency
+
def set_payment_type(dt, doc):
- if (dt in ("Sales Order", "Donation") or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
- or (dt=="Purchase Invoice" and doc.outstanding_amount < 0):
- payment_type = "Receive"
+ if (
+ dt in ("Sales Order", "Donation")
+ or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)
+ ) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0):
+ payment_type = "Receive"
else:
payment_type = "Pay"
return payment_type
+
def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_currency, doc):
grand_total = outstanding_amount = 0
if party_amount:
@@ -1664,8 +1989,7 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre
outstanding_amount = doc.outstanding_amount
elif dt in ("Expense Claim"):
grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges
- outstanding_amount = doc.grand_total \
- - doc.total_amount_reimbursed
+ outstanding_amount = doc.grand_total - doc.total_amount_reimbursed
elif dt == "Employee Advance":
grand_total = flt(doc.advance_amount)
outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount)
@@ -1692,7 +2016,10 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre
outstanding_amount = grand_total - flt(doc.advance_paid)
return grand_total, outstanding_amount
-def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc):
+
+def set_paid_amount_and_received_amount(
+ dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
+):
paid_amount = received_amount = 0
if party_account_currency == bank.account_currency:
paid_amount = received_amount = abs(outstanding_amount)
@@ -1701,37 +2028,38 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
if bank_amount:
received_amount = bank_amount
else:
- received_amount = paid_amount * doc.get('conversion_rate', 1)
+ received_amount = paid_amount * doc.get("conversion_rate", 1)
if dt == "Employee Advance":
- received_amount = paid_amount * doc.get('exchange_rate', 1)
+ received_amount = paid_amount * doc.get("exchange_rate", 1)
else:
received_amount = abs(outstanding_amount)
if bank_amount:
paid_amount = bank_amount
else:
# if party account currency and bank currency is different then populate paid amount as well
- paid_amount = received_amount * doc.get('conversion_rate', 1)
+ paid_amount = received_amount * doc.get("conversion_rate", 1)
if dt == "Employee Advance":
- paid_amount = received_amount * doc.get('exchange_rate', 1)
+ paid_amount = received_amount * doc.get("exchange_rate", 1)
return paid_amount, received_amount
+
def apply_early_payment_discount(paid_amount, received_amount, doc):
total_discount = 0
- eligible_for_payments = ['Sales Order', 'Sales Invoice', 'Purchase Order', 'Purchase Invoice']
- has_payment_schedule = hasattr(doc, 'payment_schedule') and doc.payment_schedule
+ eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
+ has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule
if doc.doctype in eligible_for_payments and has_payment_schedule:
for term in doc.payment_schedule:
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
- if term.discount_type == 'Percentage':
- discount_amount = flt(doc.get('grand_total')) * (term.discount / 100)
+ if term.discount_type == "Percentage":
+ discount_amount = flt(doc.get("grand_total")) * (term.discount / 100)
else:
discount_amount = term.discount
- discount_amount_in_foreign_currency = discount_amount * doc.get('conversion_rate', 1)
+ discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1)
- if doc.doctype == 'Sales Invoice':
+ if doc.doctype == "Sales Invoice":
paid_amount -= discount_amount
received_amount -= discount_amount_in_foreign_currency
else:
@@ -1741,38 +2069,46 @@ def apply_early_payment_discount(paid_amount, received_amount, doc):
total_discount += discount_amount
if total_discount:
- money = frappe.utils.fmt_money(total_discount, currency=doc.get('currency'))
+ money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency"))
frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1)
return paid_amount, received_amount, total_discount
-def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount):
+
+def get_reference_as_per_payment_terms(
+ payment_schedule, dt, dn, doc, grand_total, outstanding_amount
+):
references = []
for payment_term in payment_schedule:
- payment_term_outstanding = flt(payment_term.payment_amount - payment_term.paid_amount,
- payment_term.precision('payment_amount'))
+ payment_term_outstanding = flt(
+ payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount")
+ )
if payment_term_outstanding:
- references.append({
- 'reference_doctype': dt,
- 'reference_name': dn,
- 'bill_no': doc.get('bill_no'),
- 'due_date': doc.get('due_date'),
- 'total_amount': grand_total,
- 'outstanding_amount': outstanding_amount,
- 'payment_term': payment_term.payment_term,
- 'allocated_amount': payment_term_outstanding
- })
+ references.append(
+ {
+ "reference_doctype": dt,
+ "reference_name": dn,
+ "bill_no": doc.get("bill_no"),
+ "due_date": doc.get("due_date"),
+ "total_amount": grand_total,
+ "outstanding_amount": outstanding_amount,
+ "payment_term": payment_term.payment_term,
+ "allocated_amount": payment_term_outstanding,
+ }
+ )
return references
+
def get_paid_amount(dt, dn, party_type, party, account, due_date):
- if party_type=="Customer":
+ if party_type == "Customer":
dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
else:
dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
- paid_amount = frappe.db.sql("""
+ paid_amount = frappe.db.sql(
+ """
select ifnull(sum({dr_or_cr}), 0) as paid_amount
from `tabGL Entry`
where against_voucher_type = %s
@@ -1782,41 +2118,58 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date):
and account = %s
and due_date = %s
and {dr_or_cr} > 0
- """.format(dr_or_cr=dr_or_cr), (dt, dn, party_type, party, account, due_date))
+ """.format(
+ dr_or_cr=dr_or_cr
+ ),
+ (dt, dn, party_type, party, account, due_date),
+ )
return paid_amount[0][0] if paid_amount else 0
+
@frappe.whitelist()
-def get_party_and_account_balance(company, date, paid_from=None, paid_to=None, ptype=None, pty=None, cost_center=None):
- return frappe._dict({
- "party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center),
- "paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center),
- "paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center)
- })
+def get_party_and_account_balance(
+ company, date, paid_from=None, paid_to=None, ptype=None, pty=None, cost_center=None
+):
+ return frappe._dict(
+ {
+ "party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center),
+ "paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center),
+ "paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center),
+ }
+ )
+
@frappe.whitelist()
def make_payment_order(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
+
def set_missing_values(source, target):
target.payment_order_type = "Payment Entry"
- target.append('references', dict(
- reference_doctype="Payment Entry",
- reference_name=source.name,
- bank_account=source.party_bank_account,
- amount=source.paid_amount,
- account=source.paid_to,
- supplier=source.party,
- mode_of_payment=source.mode_of_payment,
- ))
+ target.append(
+ "references",
+ dict(
+ reference_doctype="Payment Entry",
+ reference_name=source.name,
+ bank_account=source.party_bank_account,
+ amount=source.paid_amount,
+ account=source.paid_to,
+ supplier=source.party,
+ mode_of_payment=source.mode_of_payment,
+ ),
+ )
- doclist = get_mapped_doc("Payment Entry", source_name, {
- "Payment Entry": {
- "doctype": "Payment Order",
- "validation": {
- "docstatus": ["=", 1]
- },
- }
-
- }, target_doc, set_missing_values)
+ doclist = get_mapped_doc(
+ "Payment Entry",
+ source_name,
+ {
+ "Payment Entry": {
+ "doctype": "Payment Order",
+ "validation": {"docstatus": ["=", 1]},
+ }
+ },
+ target_doc,
+ set_missing_values,
+ )
return doclist
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 349b8bb5b1b..5b70b510d2b 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -32,10 +32,9 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert()
pe.submit()
- expected_gle = dict((d[0], d) for d in [
- ["Debtors - _TC", 0, 1000, so.name],
- ["_Test Cash - _TC", 1000.0, 0, None]
- ])
+ expected_gle = dict(
+ (d[0], d) for d in [["Debtors - _TC", 0, 1000, so.name], ["_Test Cash - _TC", 1000.0, 0, None]]
+ )
self.validate_gl_entries(pe.name, expected_gle)
@@ -48,9 +47,9 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(so_advance_paid, 0)
def test_payment_entry_for_blocked_supplier_invoice(self):
- supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1
- supplier.hold_type = 'Invoices'
+ supplier.hold_type = "Invoices"
supplier.save()
self.assertRaises(frappe.ValidationError, make_purchase_invoice)
@@ -59,32 +58,40 @@ class TestPaymentEntry(unittest.TestCase):
supplier.save()
def test_payment_entry_for_blocked_supplier_payments(self):
- supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1
- supplier.hold_type = 'Payments'
+ supplier.hold_type = "Payments"
supplier.save()
pi = make_purchase_invoice()
self.assertRaises(
- frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name,
- bank_account="_Test Bank - _TC")
+ frappe.ValidationError,
+ get_payment_entry,
+ dt="Purchase Invoice",
+ dn=pi.name,
+ bank_account="_Test Bank - _TC",
+ )
supplier.on_hold = 0
supplier.save()
def test_payment_entry_for_blocked_supplier_payments_today_date(self):
- supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1
- supplier.hold_type = 'Payments'
+ supplier.hold_type = "Payments"
supplier.release_date = nowdate()
supplier.save()
pi = make_purchase_invoice()
self.assertRaises(
- frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name,
- bank_account="_Test Bank - _TC")
+ frappe.ValidationError,
+ get_payment_entry,
+ dt="Purchase Invoice",
+ dn=pi.name,
+ bank_account="_Test Bank - _TC",
+ )
supplier.on_hold = 0
supplier.save()
@@ -93,15 +100,15 @@ class TestPaymentEntry(unittest.TestCase):
# this test is meant to fail only if something fails in the try block
with self.assertRaises(Exception):
try:
- supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1
- supplier.hold_type = 'Payments'
- supplier.release_date = '2018-03-01'
+ supplier.hold_type = "Payments"
+ supplier.release_date = "2018-03-01"
supplier.save()
pi = make_purchase_invoice()
- get_payment_entry('Purchase Invoice', pi.name, bank_account="_Test Bank - _TC")
+ get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
supplier.on_hold = 0
supplier.save()
@@ -111,8 +118,12 @@ class TestPaymentEntry(unittest.TestCase):
raise Exception
def test_payment_entry_against_si_usd_to_usd(self):
- si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
- currency="USD", conversion_rate=50)
+ si = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
@@ -120,10 +131,13 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert()
pe.submit()
- expected_gle = dict((d[0], d) for d in [
- ["_Test Receivable USD - _TC", 0, 5000, si.name],
- ["_Test Bank USD - _TC", 5000.0, 0, None]
- ])
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["_Test Receivable USD - _TC", 0, 5000, si.name],
+ ["_Test Bank USD - _TC", 5000.0, 0, None],
+ ]
+ )
self.validate_gl_entries(pe.name, expected_gle)
@@ -136,8 +150,12 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(outstanding_amount, 100)
def test_payment_entry_against_pi(self):
- pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC",
- currency="USD", conversion_rate=50)
+ pi = make_purchase_invoice(
+ supplier="_Test Supplier USD",
+ debit_to="_Test Payable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
@@ -145,20 +163,26 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert()
pe.submit()
- expected_gle = dict((d[0], d) for d in [
- ["_Test Payable USD - _TC", 12500, 0, pi.name],
- ["_Test Bank USD - _TC", 0, 12500, None]
- ])
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["_Test Payable USD - _TC", 12500, 0, pi.name],
+ ["_Test Bank USD - _TC", 0, 12500, None],
+ ]
+ )
self.validate_gl_entries(pe.name, expected_gle)
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", pi.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 0)
-
def test_payment_against_sales_invoice_to_check_status(self):
- si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
- currency="USD", conversion_rate=50)
+ si = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
@@ -167,28 +191,35 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert()
pe.submit()
- outstanding_amount, status = frappe.db.get_value("Sales Invoice", si.name, ["outstanding_amount", "status"])
+ outstanding_amount, status = frappe.db.get_value(
+ "Sales Invoice", si.name, ["outstanding_amount", "status"]
+ )
self.assertEqual(flt(outstanding_amount), 0)
- self.assertEqual(status, 'Paid')
+ self.assertEqual(status, "Paid")
pe.cancel()
- outstanding_amount, status = frappe.db.get_value("Sales Invoice", si.name, ["outstanding_amount", "status"])
+ outstanding_amount, status = frappe.db.get_value(
+ "Sales Invoice", si.name, ["outstanding_amount", "status"]
+ )
self.assertEqual(flt(outstanding_amount), 100)
- self.assertEqual(status, 'Unpaid')
+ self.assertEqual(status, "Unpaid")
def test_payment_entry_against_payment_terms(self):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
create_payment_terms_template()
- si.payment_terms_template = 'Test Receivable Template'
+ si.payment_terms_template = "Test Receivable Template"
- si.append('taxes', {
- "charge_type": "On Net Total",
- "account_head": "_Test Account Service Tax - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Service Tax",
- "rate": 18
- })
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 18,
+ },
+ )
si.save()
si.submit()
@@ -197,25 +228,28 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit()
si.load_from_db()
- self.assertEqual(pe.references[0].payment_term, 'Basic Amount Receivable')
- self.assertEqual(pe.references[1].payment_term, 'Tax Receivable')
+ self.assertEqual(pe.references[0].payment_term, "Basic Amount Receivable")
+ self.assertEqual(pe.references[1].payment_term, "Tax Receivable")
self.assertEqual(si.payment_schedule[0].paid_amount, 200.0)
self.assertEqual(si.payment_schedule[1].paid_amount, 36.0)
def test_payment_entry_against_payment_terms_with_discount(self):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
create_payment_terms_template_with_discount()
- si.payment_terms_template = 'Test Discount Template'
+ si.payment_terms_template = "Test Discount Template"
- frappe.db.set_value('Company', si.company, 'default_discount_account', 'Write Off - _TC')
+ frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
- si.append('taxes', {
- "charge_type": "On Net Total",
- "account_head": "_Test Account Service Tax - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Service Tax",
- "rate": 18
- })
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 18,
+ },
+ )
si.save()
si.submit()
@@ -224,16 +258,19 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit()
si.load_from_db()
- self.assertEqual(pe.references[0].payment_term, '30 Credit Days with 10% Discount')
+ self.assertEqual(pe.references[0].payment_term, "30 Credit Days with 10% Discount")
self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
self.assertEqual(si.payment_schedule[0].paid_amount, 212.40)
self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
-
def test_payment_against_purchase_invoice_to_check_status(self):
- pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC",
- currency="USD", conversion_rate=50)
+ pi = make_purchase_invoice(
+ supplier="_Test Supplier USD",
+ debit_to="_Test Payable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
@@ -242,21 +279,27 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert()
pe.submit()
- outstanding_amount, status = frappe.db.get_value("Purchase Invoice", pi.name, ["outstanding_amount", "status"])
+ outstanding_amount, status = frappe.db.get_value(
+ "Purchase Invoice", pi.name, ["outstanding_amount", "status"]
+ )
self.assertEqual(flt(outstanding_amount), 0)
- self.assertEqual(status, 'Paid')
+ self.assertEqual(status, "Paid")
pe.cancel()
- outstanding_amount, status = frappe.db.get_value("Purchase Invoice", pi.name, ["outstanding_amount", "status"])
+ outstanding_amount, status = frappe.db.get_value(
+ "Purchase Invoice", pi.name, ["outstanding_amount", "status"]
+ )
self.assertEqual(flt(outstanding_amount), 250)
- self.assertEqual(status, 'Unpaid')
+ self.assertEqual(status, "Unpaid")
def test_payment_entry_against_ec(self):
- payable = frappe.get_cached_value('Company', "_Test Company", 'default_payable_account')
+ payable = frappe.get_cached_value("Company", "_Test Company", "default_payable_account")
ec = make_expense_claim(payable, 300, 300, "_Test Company", "Travel Expenses - _TC")
- pe = get_payment_entry("Expense Claim", ec.name, bank_account="_Test Bank USD - _TC", bank_amount=300)
+ pe = get_payment_entry(
+ "Expense Claim", ec.name, bank_account="_Test Bank USD - _TC", bank_amount=300
+ )
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
pe.source_exchange_rate = 1
@@ -264,68 +307,87 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert()
pe.submit()
- expected_gle = dict((d[0], d) for d in [
- [payable, 300, 0, ec.name],
- ["_Test Bank USD - _TC", 0, 300, None]
- ])
+ expected_gle = dict(
+ (d[0], d) for d in [[payable, 300, 0, ec.name], ["_Test Bank USD - _TC", 0, 300, None]]
+ )
self.validate_gl_entries(pe.name, expected_gle)
- outstanding_amount = flt(frappe.db.get_value("Expense Claim", ec.name, "total_sanctioned_amount")) - \
- flt(frappe.db.get_value("Expense Claim", ec.name, "total_amount_reimbursed"))
+ outstanding_amount = flt(
+ frappe.db.get_value("Expense Claim", ec.name, "total_sanctioned_amount")
+ ) - flt(frappe.db.get_value("Expense Claim", ec.name, "total_amount_reimbursed"))
self.assertEqual(outstanding_amount, 0)
def test_payment_entry_against_si_usd_to_inr(self):
- si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
- currency="USD", conversion_rate=50)
- pe = get_payment_entry("Sales Invoice", si.name, party_amount=20,
- bank_account="_Test Bank - _TC", bank_amount=900)
+ si = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
+ pe = get_payment_entry(
+ "Sales Invoice", si.name, party_amount=20, bank_account="_Test Bank - _TC", bank_amount=900
+ )
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
self.assertEqual(pe.difference_amount, 100)
- pe.append("deductions", {
- "account": "_Test Exchange Gain/Loss - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "amount": 100
- })
+ pe.append(
+ "deductions",
+ {
+ "account": "_Test Exchange Gain/Loss - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "amount": 100,
+ },
+ )
pe.insert()
pe.submit()
- expected_gle = dict((d[0], d) for d in [
- ["_Test Receivable USD - _TC", 0, 1000, si.name],
- ["_Test Bank - _TC", 900, 0, None],
- ["_Test Exchange Gain/Loss - _TC", 100.0, 0, None],
- ])
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["_Test Receivable USD - _TC", 0, 1000, si.name],
+ ["_Test Bank - _TC", 900, 0, None],
+ ["_Test Exchange Gain/Loss - _TC", 100.0, 0, None],
+ ]
+ )
self.validate_gl_entries(pe.name, expected_gle)
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 80)
- def test_payment_entry_against_si_usd_to_usd_with_deduction_in_base_currency (self):
- si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
- currency="USD", conversion_rate=50, do_not_save=1)
+ def test_payment_entry_against_si_usd_to_usd_with_deduction_in_base_currency(self):
+ si = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ do_not_save=1,
+ )
si.plc_conversion_rate = 50
si.save()
si.submit()
- pe = get_payment_entry("Sales Invoice", si.name, party_amount=20,
- bank_account="_Test Bank USD - _TC", bank_amount=900)
+ pe = get_payment_entry(
+ "Sales Invoice", si.name, party_amount=20, bank_account="_Test Bank USD - _TC", bank_amount=900
+ )
pe.source_exchange_rate = 45.263
pe.target_exchange_rate = 45.263
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
-
- pe.append("deductions", {
- "account": "_Test Exchange Gain/Loss - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "amount": 94.80
- })
+ pe.append(
+ "deductions",
+ {
+ "account": "_Test Exchange Gain/Loss - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "amount": 94.80,
+ },
+ )
pe.save()
@@ -359,8 +421,7 @@ class TestPaymentEntry(unittest.TestCase):
pe.set_amounts()
self.assertEqual(
- pe.source_exchange_rate, 65.1,
- "{0} is not equal to {1}".format(pe.source_exchange_rate, 65.1)
+ pe.source_exchange_rate, 65.1, "{0} is not equal to {1}".format(pe.source_exchange_rate, 65.1)
)
def test_internal_transfer_usd_to_inr(self):
@@ -382,20 +443,26 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(pe.difference_amount, 500)
- pe.append("deductions", {
- "account": "_Test Exchange Gain/Loss - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "amount": 500
- })
+ pe.append(
+ "deductions",
+ {
+ "account": "_Test Exchange Gain/Loss - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "amount": 500,
+ },
+ )
pe.insert()
pe.submit()
- expected_gle = dict((d[0], d) for d in [
- ["_Test Bank USD - _TC", 0, 5000, None],
- ["_Test Bank - _TC", 4500, 0, None],
- ["_Test Exchange Gain/Loss - _TC", 500.0, 0, None],
- ])
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["_Test Bank USD - _TC", 0, 5000, None],
+ ["_Test Bank - _TC", 4500, 0, None],
+ ["_Test Exchange Gain/Loss - _TC", 500.0, 0, None],
+ ]
+ )
self.validate_gl_entries(pe.name, expected_gle)
@@ -435,10 +502,9 @@ class TestPaymentEntry(unittest.TestCase):
pe3.insert()
pe3.submit()
- expected_gle = dict((d[0], d) for d in [
- ["Debtors - _TC", 100, 0, si1.name],
- ["_Test Cash - _TC", 0, 100, None]
- ])
+ expected_gle = dict(
+ (d[0], d) for d in [["Debtors - _TC", 100, 0, si1.name], ["_Test Cash - _TC", 0, 100, None]]
+ )
self.validate_gl_entries(pe3.name, expected_gle)
@@ -462,12 +528,16 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(expected_gle[gle.account][3], gle.against_voucher)
def get_gle(self, voucher_no):
- return frappe.db.sql("""select account, debit, credit, against_voucher
+ return frappe.db.sql(
+ """select account, debit, credit, against_voucher
from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s
- order by account asc""", voucher_no, as_dict=1)
+ order by account asc""",
+ voucher_no,
+ as_dict=1,
+ )
def test_payment_entry_write_off_difference(self):
- si = create_sales_invoice()
+ si = create_sales_invoice()
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
@@ -477,11 +547,10 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(pe.unallocated_amount, 10)
pe.received_amount = pe.paid_amount = 95
- pe.append("deductions", {
- "account": "_Test Write Off - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "amount": 5
- })
+ pe.append(
+ "deductions",
+ {"account": "_Test Write Off - _TC", "cost_center": "_Test Cost Center - _TC", "amount": 5},
+ )
pe.save()
self.assertEqual(pe.unallocated_amount, 0)
@@ -489,27 +558,37 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit()
- expected_gle = dict((d[0], d) for d in [
- ["Debtors - _TC", 0, 100, si.name],
- ["_Test Cash - _TC", 95, 0, None],
- ["_Test Write Off - _TC", 5, 0, None]
- ])
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["Debtors - _TC", 0, 100, si.name],
+ ["_Test Cash - _TC", 95, 0, None],
+ ["_Test Write Off - _TC", 5, 0, None],
+ ]
+ )
self.validate_gl_entries(pe.name, expected_gle)
def test_payment_entry_exchange_gain_loss(self):
- si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
- currency="USD", conversion_rate=50)
+ si = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
pe.source_exchange_rate = 55
- pe.append("deductions", {
- "account": "_Test Exchange Gain/Loss - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "amount": -500
- })
+ pe.append(
+ "deductions",
+ {
+ "account": "_Test Exchange Gain/Loss - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "amount": -500,
+ },
+ )
pe.save()
self.assertEqual(pe.unallocated_amount, 0)
@@ -517,11 +596,14 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit()
- expected_gle = dict((d[0], d) for d in [
- ["_Test Receivable USD - _TC", 0, 5000, si.name],
- ["_Test Bank USD - _TC", 5500, 0, None],
- ["_Test Exchange Gain/Loss - _TC", 0, 500, None],
- ])
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["_Test Receivable USD - _TC", 0, 5000, si.name],
+ ["_Test Bank USD - _TC", 5500, 0, None],
+ ["_Test Exchange Gain/Loss - _TC", 0, 500, None],
+ ]
+ )
self.validate_gl_entries(pe.name, expected_gle)
@@ -530,10 +612,11 @@ class TestPaymentEntry(unittest.TestCase):
def test_payment_entry_against_sales_invoice_with_cost_centre(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+
cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
- si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC")
+ si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC")
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
self.assertEqual(pe.cost_center, si.cost_center)
@@ -546,18 +629,18 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit()
expected_values = {
- "_Test Bank - _TC": {
- "cost_center": cost_center
- },
- "Debtors - _TC": {
- "cost_center": cost_center
- }
+ "_Test Bank - _TC": {"cost_center": cost_center},
+ "Debtors - _TC": {"cost_center": cost_center},
}
- gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, cost_center, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s
- order by account asc""", pe.name, as_dict=1)
+ order by account asc""",
+ pe.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -566,10 +649,13 @@ class TestPaymentEntry(unittest.TestCase):
def test_payment_entry_against_purchase_invoice_with_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+
cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
- pi = make_purchase_invoice_against_cost_center(cost_center=cost_center, credit_to="Creditors - _TC")
+ pi = make_purchase_invoice_against_cost_center(
+ cost_center=cost_center, credit_to="Creditors - _TC"
+ )
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
self.assertEqual(pe.cost_center, pi.cost_center)
@@ -582,18 +668,18 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit()
expected_values = {
- "_Test Bank - _TC": {
- "cost_center": cost_center
- },
- "Creditors - _TC": {
- "cost_center": cost_center
- }
+ "_Test Bank - _TC": {"cost_center": cost_center},
+ "Creditors - _TC": {"cost_center": cost_center},
}
- gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, cost_center, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s
- order by account asc""", pe.name, as_dict=1)
+ order by account asc""",
+ pe.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -603,13 +689,16 @@ class TestPaymentEntry(unittest.TestCase):
def test_payment_entry_account_and_party_balance_with_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.utils import get_balance_on
+
cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
- si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC")
+ si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC")
account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=si.cost_center)
- party_balance = get_balance_on(party_type="Customer", party=si.customer, cost_center=si.cost_center)
+ party_balance = get_balance_on(
+ party_type="Customer", party=si.customer, cost_center=si.cost_center
+ )
party_account_balance = get_balance_on(si.debit_to, cost_center=si.cost_center)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
@@ -634,94 +723,109 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(flt(expected_party_account_balance), party_account_balance)
def test_multi_currency_payment_entry_with_taxes(self):
- payment_entry = create_payment_entry(party='_Test Supplier USD', paid_to = '_Test Payable USD - _TC',
- save=True)
- payment_entry.append('taxes', {
- 'account_head': '_Test Account Service Tax - _TC',
- 'charge_type': 'Actual',
- 'tax_amount': 10,
- 'add_deduct_tax': 'Add',
- 'description': 'Test'
- })
+ payment_entry = create_payment_entry(
+ party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True
+ )
+ payment_entry.append(
+ "taxes",
+ {
+ "account_head": "_Test Account Service Tax - _TC",
+ "charge_type": "Actual",
+ "tax_amount": 10,
+ "add_deduct_tax": "Add",
+ "description": "Test",
+ },
+ )
payment_entry.save()
self.assertEqual(payment_entry.base_total_taxes_and_charges, 10)
- self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2))
+ self.assertEqual(
+ flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)
+ )
+
def create_payment_entry(**args):
- payment_entry = frappe.new_doc('Payment Entry')
- payment_entry.company = args.get('company') or '_Test Company'
- payment_entry.payment_type = args.get('payment_type') or 'Pay'
- payment_entry.party_type = args.get('party_type') or 'Supplier'
- payment_entry.party = args.get('party') or '_Test Supplier'
- payment_entry.paid_from = args.get('paid_from') or '_Test Bank - _TC'
- payment_entry.paid_to = args.get('paid_to') or 'Creditors - _TC'
- payment_entry.paid_amount = args.get('paid_amount') or 1000
+ payment_entry = frappe.new_doc("Payment Entry")
+ payment_entry.company = args.get("company") or "_Test Company"
+ payment_entry.payment_type = args.get("payment_type") or "Pay"
+ payment_entry.party_type = args.get("party_type") or "Supplier"
+ payment_entry.party = args.get("party") or "_Test Supplier"
+ payment_entry.paid_from = args.get("paid_from") or "_Test Bank - _TC"
+ payment_entry.paid_to = args.get("paid_to") or "Creditors - _TC"
+ payment_entry.paid_amount = args.get("paid_amount") or 1000
payment_entry.setup_party_account_field()
payment_entry.set_missing_values()
payment_entry.set_exchange_rate()
payment_entry.received_amount = payment_entry.paid_amount / payment_entry.target_exchange_rate
- payment_entry.reference_no = 'Test001'
+ payment_entry.reference_no = "Test001"
payment_entry.reference_date = nowdate()
- if args.get('save'):
+ if args.get("save"):
payment_entry.save()
- if args.get('submit'):
+ if args.get("submit"):
payment_entry.submit()
return payment_entry
+
def create_payment_terms_template():
- create_payment_term('Basic Amount Receivable')
- create_payment_term('Tax Receivable')
+ create_payment_term("Basic Amount Receivable")
+ create_payment_term("Tax Receivable")
- if not frappe.db.exists('Payment Terms Template', 'Test Receivable Template'):
- payment_term_template = frappe.get_doc({
- 'doctype': 'Payment Terms Template',
- 'template_name': 'Test Receivable Template',
- 'allocate_payment_based_on_payment_terms': 1,
- 'terms': [{
- 'doctype': 'Payment Terms Template Detail',
- 'payment_term': 'Basic Amount Receivable',
- 'invoice_portion': 84.746,
- 'credit_days_based_on': 'Day(s) after invoice date',
- 'credit_days': 1
- },
+ if not frappe.db.exists("Payment Terms Template", "Test Receivable Template"):
+ payment_term_template = frappe.get_doc(
{
- 'doctype': 'Payment Terms Template Detail',
- 'payment_term': 'Tax Receivable',
- 'invoice_portion': 15.254,
- 'credit_days_based_on': 'Day(s) after invoice date',
- 'credit_days': 2
- }]
- }).insert()
+ "doctype": "Payment Terms Template",
+ "template_name": "Test Receivable Template",
+ "allocate_payment_based_on_payment_terms": 1,
+ "terms": [
+ {
+ "doctype": "Payment Terms Template Detail",
+ "payment_term": "Basic Amount Receivable",
+ "invoice_portion": 84.746,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": 1,
+ },
+ {
+ "doctype": "Payment Terms Template Detail",
+ "payment_term": "Tax Receivable",
+ "invoice_portion": 15.254,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": 2,
+ },
+ ],
+ }
+ ).insert()
+
def create_payment_terms_template_with_discount():
- create_payment_term('30 Credit Days with 10% Discount')
+ create_payment_term("30 Credit Days with 10% Discount")
+
+ if not frappe.db.exists("Payment Terms Template", "Test Discount Template"):
+ payment_term_template = frappe.get_doc(
+ {
+ "doctype": "Payment Terms Template",
+ "template_name": "Test Discount Template",
+ "allocate_payment_based_on_payment_terms": 1,
+ "terms": [
+ {
+ "doctype": "Payment Terms Template Detail",
+ "payment_term": "30 Credit Days with 10% Discount",
+ "invoice_portion": 100,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": 2,
+ "discount": 10,
+ "discount_validity_based_on": "Day(s) after invoice date",
+ "discount_validity": 1,
+ }
+ ],
+ }
+ ).insert()
- if not frappe.db.exists('Payment Terms Template', 'Test Discount Template'):
- payment_term_template = frappe.get_doc({
- 'doctype': 'Payment Terms Template',
- 'template_name': 'Test Discount Template',
- 'allocate_payment_based_on_payment_terms': 1,
- 'terms': [{
- 'doctype': 'Payment Terms Template Detail',
- 'payment_term': '30 Credit Days with 10% Discount',
- 'invoice_portion': 100,
- 'credit_days_based_on': 'Day(s) after invoice date',
- 'credit_days': 2,
- 'discount': 10,
- 'discount_validity_based_on': 'Day(s) after invoice date',
- 'discount_validity': 1
- }]
- }).insert()
def create_payment_term(name):
- if not frappe.db.exists('Payment Term', name):
- frappe.get_doc({
- 'doctype': 'Payment Term',
- 'payment_term_name': name
- }).insert()
+ if not frappe.db.exists("Payment Term", name):
+ frappe.get_doc({"doctype": "Payment Term", "payment_term_name": name}).insert()
diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py
index 25dc4e6a60d..ab47b6151cd 100644
--- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py
+++ b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py
@@ -18,10 +18,13 @@ class PaymentGatewayAccount(Document):
def update_default_payment_gateway(self):
if self.is_default:
- frappe.db.sql("""update `tabPayment Gateway Account` set is_default = 0
- where is_default = 1 """)
+ frappe.db.sql(
+ """update `tabPayment Gateway Account` set is_default = 0
+ where is_default = 1 """
+ )
def set_as_default_if_not_set(self):
- if not frappe.db.get_value("Payment Gateway Account",
- {"is_default": 1, "name": ("!=", self.name)}, "name"):
+ if not frappe.db.get_value(
+ "Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name)}, "name"
+ ):
self.is_default = 1
diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account_dashboard.py b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account_dashboard.py
index bb0fc975cac..d0aaee88350 100644
--- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account_dashboard.py
+++ b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account_dashboard.py
@@ -1,17 +1,6 @@
-
-
def get_data():
return {
- 'fieldname': 'payment_gateway_account',
- 'non_standard_fieldnames': {
- 'Subscription Plan': 'payment_gateway'
- },
- 'transactions': [
- {
- 'items': ['Payment Request']
- },
- {
- 'items': ['Subscription Plan']
- }
- ]
+ "fieldname": "payment_gateway_account",
+ "non_standard_fieldnames": {"Subscription Plan": "payment_gateway"},
+ "transactions": [{"items": ["Payment Request"]}, {"items": ["Subscription Plan"]}],
}
diff --git a/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py b/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py
index 1895c12ad7c..7a8cdf73355 100644
--- a/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py
+++ b/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Payment Gateway Account')
+
class TestPaymentGatewayAccount(unittest.TestCase):
pass
diff --git a/erpnext/accounts/doctype/payment_order/payment_order.js b/erpnext/accounts/doctype/payment_order/payment_order.js
index 9074defa577..7d85d89c452 100644
--- a/erpnext/accounts/doctype/payment_order/payment_order.js
+++ b/erpnext/accounts/doctype/payment_order/payment_order.js
@@ -12,7 +12,6 @@ frappe.ui.form.on('Payment Order', {
});
frm.set_df_property('references', 'cannot_add_rows', true);
- frm.set_df_property('references', 'cannot_delete_rows', true);
},
refresh: function(frm) {
if (frm.doc.docstatus == 0) {
diff --git a/erpnext/accounts/doctype/payment_order/payment_order.py b/erpnext/accounts/doctype/payment_order/payment_order.py
index 50a58b8a0ab..3c45d20770d 100644
--- a/erpnext/accounts/doctype/payment_order/payment_order.py
+++ b/erpnext/accounts/doctype/payment_order/payment_order.py
@@ -18,9 +18,9 @@ class PaymentOrder(Document):
self.update_payment_status(cancel=True)
def update_payment_status(self, cancel=False):
- status = 'Payment Ordered'
+ status = "Payment Ordered"
if cancel:
- status = 'Initiated'
+ status = "Initiated"
if self.payment_order_type == "Payment Request":
ref_field = "status"
@@ -32,67 +32,67 @@ class PaymentOrder(Document):
for d in self.references:
frappe.db.set_value(self.payment_order_type, d.get(ref_doc_field), ref_field, status)
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_mop_query(doctype, txt, searchfield, start, page_len, filters):
- return frappe.db.sql(""" select mode_of_payment from `tabPayment Order Reference`
+ return frappe.db.sql(
+ """ select mode_of_payment from `tabPayment Order Reference`
where parent = %(parent)s and mode_of_payment like %(txt)s
- limit %(start)s, %(page_len)s""", {
- 'parent': filters.get("parent"),
- 'start': start,
- 'page_len': page_len,
- 'txt': "%%%s%%" % txt
- })
+ limit %(start)s, %(page_len)s""",
+ {"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
+ )
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_supplier_query(doctype, txt, searchfield, start, page_len, filters):
- return frappe.db.sql(""" select supplier from `tabPayment Order Reference`
+ return frappe.db.sql(
+ """ select supplier from `tabPayment Order Reference`
where parent = %(parent)s and supplier like %(txt)s and
(payment_reference is null or payment_reference='')
- limit %(start)s, %(page_len)s""", {
- 'parent': filters.get("parent"),
- 'start': start,
- 'page_len': page_len,
- 'txt': "%%%s%%" % txt
- })
+ limit %(start)s, %(page_len)s""",
+ {"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
+ )
+
@frappe.whitelist()
def make_payment_records(name, supplier, mode_of_payment=None):
- doc = frappe.get_doc('Payment Order', name)
+ doc = frappe.get_doc("Payment Order", name)
make_journal_entry(doc, supplier, mode_of_payment)
+
def make_journal_entry(doc, supplier, mode_of_payment=None):
- je = frappe.new_doc('Journal Entry')
+ je = frappe.new_doc("Journal Entry")
je.payment_order = doc.name
je.posting_date = nowdate()
- mode_of_payment_type = frappe._dict(frappe.get_all('Mode of Payment',
- fields = ["name", "type"], as_list=1))
+ mode_of_payment_type = frappe._dict(
+ frappe.get_all("Mode of Payment", fields=["name", "type"], as_list=1)
+ )
- je.voucher_type = 'Bank Entry'
- if mode_of_payment and mode_of_payment_type.get(mode_of_payment) == 'Cash':
+ je.voucher_type = "Bank Entry"
+ if mode_of_payment and mode_of_payment_type.get(mode_of_payment) == "Cash":
je.voucher_type = "Cash Entry"
paid_amt = 0
- party_account = get_party_account('Supplier', supplier, doc.company)
+ party_account = get_party_account("Supplier", supplier, doc.company)
for d in doc.references:
- if (d.supplier == supplier
- and (not mode_of_payment or mode_of_payment == d.mode_of_payment)):
- je.append('accounts', {
- 'account': party_account,
- 'debit_in_account_currency': d.amount,
- 'party_type': 'Supplier',
- 'party': supplier,
- 'reference_type': d.reference_doctype,
- 'reference_name': d.reference_name
- })
+ if d.supplier == supplier and (not mode_of_payment or mode_of_payment == d.mode_of_payment):
+ je.append(
+ "accounts",
+ {
+ "account": party_account,
+ "debit_in_account_currency": d.amount,
+ "party_type": "Supplier",
+ "party": supplier,
+ "reference_type": d.reference_doctype,
+ "reference_name": d.reference_name,
+ },
+ )
paid_amt += d.amount
- je.append('accounts', {
- 'account': doc.account,
- 'credit_in_account_currency': paid_amt
- })
+ je.append("accounts", {"account": doc.account, "credit_in_account_currency": paid_amt})
je.flags.ignore_mandatory = True
je.save()
diff --git a/erpnext/accounts/doctype/payment_order/payment_order_dashboard.py b/erpnext/accounts/doctype/payment_order/payment_order_dashboard.py
index 02da9793895..f82886e7c26 100644
--- a/erpnext/accounts/doctype/payment_order/payment_order_dashboard.py
+++ b/erpnext/accounts/doctype/payment_order/payment_order_dashboard.py
@@ -1,11 +1,5 @@
-
-
def get_data():
return {
- 'fieldname': 'payment_order',
- 'transactions': [
- {
- 'items': ['Payment Entry', 'Journal Entry']
- }
- ]
+ "fieldname": "payment_order",
+ "transactions": [{"items": ["Payment Entry", "Journal Entry"]}],
}
diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py
index 3f4d89b4eaf..0dcb1794b9a 100644
--- a/erpnext/accounts/doctype/payment_order/test_payment_order.py
+++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py
@@ -26,7 +26,9 @@ class TestPaymentOrder(unittest.TestCase):
def test_payment_order_creation_against_payment_entry(self):
purchase_invoice = make_purchase_invoice()
- payment_entry = get_payment_entry("Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC")
+ payment_entry = get_payment_entry(
+ "Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC"
+ )
payment_entry.reference_no = "_Test_Payment_Order"
payment_entry.reference_date = getdate()
payment_entry.party_bank_account = "Checking Account - Citi Bank"
@@ -40,13 +42,16 @@ class TestPaymentOrder(unittest.TestCase):
self.assertEqual(reference_doc.supplier, "_Test Supplier")
self.assertEqual(reference_doc.amount, 250)
+
def create_payment_order_against_payment_entry(ref_doc, order_type):
- payment_order = frappe.get_doc(dict(
- doctype="Payment Order",
- company="_Test Company",
- payment_order_type=order_type,
- company_bank_account="Checking Account - Citi Bank"
- ))
+ payment_order = frappe.get_doc(
+ dict(
+ doctype="Payment Order",
+ company="_Test Company",
+ payment_order_type=order_type,
+ company_bank_account="Checking Account - Citi Bank",
+ )
+ )
doc = make_payment_order(ref_doc.name, payment_order)
doc.save()
doc.submit()
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
index 648d2da754e..867fcc7f13e 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
// For license information, please see license.txt
frappe.provide("erpnext.accounts");
@@ -38,6 +38,15 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
]
};
});
+
+ this.frm.set_query("cost_center", () => {
+ return {
+ "filters": {
+ "company": this.frm.doc.company,
+ "is_group": 0
+ }
+ }
+ });
},
refresh: function() {
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
index eb0c20f92d9..18d34850850 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
@@ -24,6 +24,7 @@
"invoice_limit",
"payment_limit",
"bank_cash_account",
+ "cost_center",
"sec_break1",
"invoices",
"column_break_15",
@@ -178,13 +179,19 @@
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
}
],
"hide_toolbar": 1,
"icon": "icon-resize-horizontal",
"issingle": 1,
"links": [],
- "modified": "2021-10-04 20:27:11.114194",
+ "modified": "2022-04-29 15:37:10.246831",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",
@@ -209,5 +216,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 548571d1d76..e5b942fb6ef 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -32,30 +32,43 @@ class PaymentReconciliation(Document):
non_reconciled_payments = payment_entries + journal_entries + dr_or_cr_notes
if self.payment_limit:
- non_reconciled_payments = non_reconciled_payments[:self.payment_limit]
+ non_reconciled_payments = non_reconciled_payments[: self.payment_limit]
- non_reconciled_payments = sorted(non_reconciled_payments, key=lambda k: k['posting_date'] or getdate(nowdate()))
+ non_reconciled_payments = sorted(
+ non_reconciled_payments, key=lambda k: k["posting_date"] or getdate(nowdate())
+ )
self.add_payment_entries(non_reconciled_payments)
def get_payment_entries(self):
- order_doctype = "Sales Order" if self.party_type=="Customer" else "Purchase Order"
+ order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
condition = self.get_conditions(get_payments=True)
- payment_entries = get_advance_payment_entries(self.party_type, self.party,
- self.receivable_payable_account, order_doctype, against_all_orders=True, limit=self.payment_limit,
- condition=condition)
+ payment_entries = get_advance_payment_entries(
+ self.party_type,
+ self.party,
+ self.receivable_payable_account,
+ order_doctype,
+ against_all_orders=True,
+ limit=self.payment_limit,
+ condition=condition,
+ )
return payment_entries
def get_jv_entries(self):
condition = self.get_conditions()
- dr_or_cr = ("credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable'
- else "debit_in_account_currency")
+ dr_or_cr = (
+ "credit_in_account_currency"
+ if erpnext.get_party_account_type(self.party_type) == "Receivable"
+ else "debit_in_account_currency"
+ )
- bank_account_condition = "t2.against_account like %(bank_cash_account)s" \
- if self.bank_cash_account else "1=1"
+ bank_account_condition = (
+ "t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
+ )
- journal_entries = frappe.db.sql("""
+ journal_entries = frappe.db.sql(
+ """
select
"Journal Entry" as reference_type, t1.name as reference_name,
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
@@ -76,31 +89,42 @@ class PaymentReconciliation(Document):
ELSE {bank_account_condition}
END)
order by t1.posting_date
- """.format(**{
- "dr_or_cr": dr_or_cr,
- "bank_account_condition": bank_account_condition,
- "condition": condition
- }), {
+ """.format(
+ **{
+ "dr_or_cr": dr_or_cr,
+ "bank_account_condition": bank_account_condition,
+ "condition": condition,
+ }
+ ),
+ {
"party_type": self.party_type,
"party": self.party,
"account": self.receivable_payable_account,
- "bank_cash_account": "%%%s%%" % self.bank_cash_account
- }, as_dict=1)
+ "bank_cash_account": "%%%s%%" % self.bank_cash_account,
+ },
+ as_dict=1,
+ )
return list(journal_entries)
def get_dr_or_cr_notes(self):
condition = self.get_conditions(get_return_invoices=True)
- dr_or_cr = ("credit_in_account_currency"
- if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency")
+ dr_or_cr = (
+ "credit_in_account_currency"
+ if erpnext.get_party_account_type(self.party_type) == "Receivable"
+ else "debit_in_account_currency"
+ )
- reconciled_dr_or_cr = ("debit_in_account_currency"
- if dr_or_cr == "credit_in_account_currency" else "credit_in_account_currency")
+ reconciled_dr_or_cr = (
+ "debit_in_account_currency"
+ if dr_or_cr == "credit_in_account_currency"
+ else "credit_in_account_currency"
+ )
- voucher_type = ('Sales Invoice'
- if self.party_type == 'Customer' else "Purchase Invoice")
+ voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
- return frappe.db.sql(""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type,
+ return frappe.db.sql(
+ """ SELECT doc.name as reference_name, %(voucher_type)s as reference_type,
(sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date,
account_currency as currency
FROM `tab{doc}` doc, `tabGL Entry` gl
@@ -117,106 +141,115 @@ class PaymentReconciliation(Document):
amount > 0
ORDER BY doc.posting_date
""".format(
- doc=voucher_type,
- dr_or_cr=dr_or_cr,
- reconciled_dr_or_cr=reconciled_dr_or_cr,
- party_type_field=frappe.scrub(self.party_type),
- condition=condition or ""),
+ doc=voucher_type,
+ dr_or_cr=dr_or_cr,
+ reconciled_dr_or_cr=reconciled_dr_or_cr,
+ party_type_field=frappe.scrub(self.party_type),
+ condition=condition or "",
+ ),
{
- 'party': self.party,
- 'party_type': self.party_type,
- 'voucher_type': voucher_type,
- 'account': self.receivable_payable_account
- }, as_dict=1)
+ "party": self.party,
+ "party_type": self.party_type,
+ "voucher_type": voucher_type,
+ "account": self.receivable_payable_account,
+ },
+ as_dict=1,
+ )
def add_payment_entries(self, non_reconciled_payments):
- self.set('payments', [])
+ self.set("payments", [])
for payment in non_reconciled_payments:
- row = self.append('payments', {})
+ row = self.append("payments", {})
row.update(payment)
def get_invoice_entries(self):
- #Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
+ # Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
condition = self.get_conditions(get_invoices=True)
- non_reconciled_invoices = get_outstanding_invoices(self.party_type, self.party,
- self.receivable_payable_account, condition=condition)
+ non_reconciled_invoices = get_outstanding_invoices(
+ self.party_type, self.party, self.receivable_payable_account, condition=condition
+ )
if self.invoice_limit:
- non_reconciled_invoices = non_reconciled_invoices[:self.invoice_limit]
+ non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
self.add_invoice_entries(non_reconciled_invoices)
def add_invoice_entries(self, non_reconciled_invoices):
- #Populate 'invoices' with JVs and Invoices to reconcile against
- self.set('invoices', [])
+ # Populate 'invoices' with JVs and Invoices to reconcile against
+ self.set("invoices", [])
for entry in non_reconciled_invoices:
- inv = self.append('invoices', {})
- inv.invoice_type = entry.get('voucher_type')
- inv.invoice_number = entry.get('voucher_no')
- inv.invoice_date = entry.get('posting_date')
- inv.amount = flt(entry.get('invoice_amount'))
- inv.currency = entry.get('currency')
- inv.outstanding_amount = flt(entry.get('outstanding_amount'))
+ inv = self.append("invoices", {})
+ inv.invoice_type = entry.get("voucher_type")
+ inv.invoice_number = entry.get("voucher_no")
+ inv.invoice_date = entry.get("posting_date")
+ inv.amount = flt(entry.get("invoice_amount"))
+ inv.currency = entry.get("currency")
+ inv.outstanding_amount = flt(entry.get("outstanding_amount"))
@frappe.whitelist()
def allocate_entries(self, args):
self.validate_entries()
entries = []
- for pay in args.get('payments'):
- pay.update({'unreconciled_amount': pay.get('amount')})
- for inv in args.get('invoices'):
- if pay.get('amount') >= inv.get('outstanding_amount'):
- res = self.get_allocated_entry(pay, inv, inv['outstanding_amount'])
- pay['amount'] = flt(pay.get('amount')) - flt(inv.get('outstanding_amount'))
- inv['outstanding_amount'] = 0
+ for pay in args.get("payments"):
+ pay.update({"unreconciled_amount": pay.get("amount")})
+ for inv in args.get("invoices"):
+ if pay.get("amount") >= inv.get("outstanding_amount"):
+ res = self.get_allocated_entry(pay, inv, inv["outstanding_amount"])
+ pay["amount"] = flt(pay.get("amount")) - flt(inv.get("outstanding_amount"))
+ inv["outstanding_amount"] = 0
else:
- res = self.get_allocated_entry(pay, inv, pay['amount'])
- inv['outstanding_amount'] = flt(inv.get('outstanding_amount')) - flt(pay.get('amount'))
- pay['amount'] = 0
- if pay.get('amount') == 0:
+ res = self.get_allocated_entry(pay, inv, pay["amount"])
+ inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
+ pay["amount"] = 0
+ if pay.get("amount") == 0:
entries.append(res)
break
- elif inv.get('outstanding_amount') == 0:
+ elif inv.get("outstanding_amount") == 0:
entries.append(res)
continue
else:
break
- self.set('allocation', [])
+ self.set("allocation", [])
for entry in entries:
- if entry['allocated_amount'] != 0:
- row = self.append('allocation', {})
+ if entry["allocated_amount"] != 0:
+ row = self.append("allocation", {})
row.update(entry)
def get_allocated_entry(self, pay, inv, allocated_amount):
- return frappe._dict({
- 'reference_type': pay.get('reference_type'),
- 'reference_name': pay.get('reference_name'),
- 'reference_row': pay.get('reference_row'),
- 'invoice_type': inv.get('invoice_type'),
- 'invoice_number': inv.get('invoice_number'),
- 'unreconciled_amount': pay.get('unreconciled_amount'),
- 'amount': pay.get('amount'),
- 'allocated_amount': allocated_amount,
- 'difference_amount': pay.get('difference_amount')
- })
+ return frappe._dict(
+ {
+ "reference_type": pay.get("reference_type"),
+ "reference_name": pay.get("reference_name"),
+ "reference_row": pay.get("reference_row"),
+ "invoice_type": inv.get("invoice_type"),
+ "invoice_number": inv.get("invoice_number"),
+ "unreconciled_amount": pay.get("unreconciled_amount"),
+ "amount": pay.get("amount"),
+ "allocated_amount": allocated_amount,
+ "difference_amount": pay.get("difference_amount"),
+ }
+ )
@frappe.whitelist()
def reconcile(self):
self.validate_allocation()
- dr_or_cr = ("credit_in_account_currency"
- if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency")
+ dr_or_cr = (
+ "credit_in_account_currency"
+ if erpnext.get_party_account_type(self.party_type) == "Receivable"
+ else "debit_in_account_currency"
+ )
entry_list = []
dr_or_cr_notes = []
- for row in self.get('allocation'):
+ for row in self.get("allocation"):
reconciled_entry = []
if row.invoice_number and row.allocated_amount:
- if row.reference_type in ['Sales Invoice', 'Purchase Invoice']:
+ if row.reference_type in ["Sales Invoice", "Purchase Invoice"]:
reconciled_entry = dr_or_cr_notes
else:
reconciled_entry = entry_list
@@ -233,23 +266,25 @@ class PaymentReconciliation(Document):
self.get_unreconciled_entries()
def get_payment_details(self, row, dr_or_cr):
- return frappe._dict({
- 'voucher_type': row.get('reference_type'),
- 'voucher_no' : row.get('reference_name'),
- 'voucher_detail_no' : row.get('reference_row'),
- 'against_voucher_type' : row.get('invoice_type'),
- 'against_voucher' : row.get('invoice_number'),
- 'account' : self.receivable_payable_account,
- 'party_type': self.party_type,
- 'party': self.party,
- 'is_advance' : row.get('is_advance'),
- 'dr_or_cr' : dr_or_cr,
- 'unreconciled_amount': flt(row.get('unreconciled_amount')),
- 'unadjusted_amount' : flt(row.get('amount')),
- 'allocated_amount' : flt(row.get('allocated_amount')),
- 'difference_amount': flt(row.get('difference_amount')),
- 'difference_account': row.get('difference_account')
- })
+ return frappe._dict(
+ {
+ "voucher_type": row.get("reference_type"),
+ "voucher_no": row.get("reference_name"),
+ "voucher_detail_no": row.get("reference_row"),
+ "against_voucher_type": row.get("invoice_type"),
+ "against_voucher": row.get("invoice_number"),
+ "account": self.receivable_payable_account,
+ "party_type": self.party_type,
+ "party": self.party,
+ "is_advance": row.get("is_advance"),
+ "dr_or_cr": dr_or_cr,
+ "unreconciled_amount": flt(row.get("unreconciled_amount")),
+ "unadjusted_amount": flt(row.get("amount")),
+ "allocated_amount": flt(row.get("allocated_amount")),
+ "difference_amount": flt(row.get("difference_amount")),
+ "difference_account": row.get("difference_account"),
+ }
+ )
def check_mandatory_to_fetch(self):
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
@@ -267,7 +302,9 @@ class PaymentReconciliation(Document):
unreconciled_invoices = frappe._dict()
for inv in self.get("invoices"):
- unreconciled_invoices.setdefault(inv.invoice_type, {}).setdefault(inv.invoice_number, inv.outstanding_amount)
+ unreconciled_invoices.setdefault(inv.invoice_type, {}).setdefault(
+ inv.invoice_number, inv.outstanding_amount
+ )
invoices_to_reconcile = []
for row in self.get("allocation"):
@@ -275,13 +312,19 @@ class PaymentReconciliation(Document):
invoices_to_reconcile.append(row.invoice_number)
if flt(row.amount) - flt(row.allocated_amount) < 0:
- frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}")
- .format(row.idx, row.allocated_amount, row.amount))
+ frappe.throw(
+ _(
+ "Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}"
+ ).format(row.idx, row.allocated_amount, row.amount)
+ )
invoice_outstanding = unreconciled_invoices.get(row.invoice_type, {}).get(row.invoice_number)
if flt(row.allocated_amount) - invoice_outstanding > 0.009:
- frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equal to invoice outstanding amount {2}")
- .format(row.idx, row.allocated_amount, invoice_outstanding))
+ frappe.throw(
+ _(
+ "Row {0}: Allocated amount {1} must be less than or equal to invoice outstanding amount {2}"
+ ).format(row.idx, row.allocated_amount, invoice_outstanding)
+ )
if not invoices_to_reconcile:
frappe.throw(_("No records found in Allocation table"))
@@ -289,79 +332,134 @@ class PaymentReconciliation(Document):
def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False):
condition = " and company = '{0}' ".format(self.company)
+ if self.get("cost_center") and (get_invoices or get_payments or get_return_invoices):
+ condition = " and cost_center = '{0}' ".format(self.cost_center)
+
if get_invoices:
- condition += " and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date)) if self.from_invoice_date else ""
- condition += " and posting_date <= {0}".format(frappe.db.escape(self.to_invoice_date)) if self.to_invoice_date else ""
- dr_or_cr = ("debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable'
- else "credit_in_account_currency")
+ condition += (
+ " and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date))
+ if self.from_invoice_date
+ else ""
+ )
+ condition += (
+ " and posting_date <= {0}".format(frappe.db.escape(self.to_invoice_date))
+ if self.to_invoice_date
+ else ""
+ )
+ dr_or_cr = (
+ "debit_in_account_currency"
+ if erpnext.get_party_account_type(self.party_type) == "Receivable"
+ else "credit_in_account_currency"
+ )
if self.minimum_invoice_amount:
- condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_invoice_amount))
+ condition += " and {dr_or_cr} >= {amount}".format(
+ dr_or_cr=dr_or_cr, amount=flt(self.minimum_invoice_amount)
+ )
if self.maximum_invoice_amount:
- condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_invoice_amount))
+ condition += " and {dr_or_cr} <= {amount}".format(
+ dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount)
+ )
elif get_return_invoices:
condition = " and doc.company = '{0}' ".format(self.company)
- condition += " and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) if self.from_payment_date else ""
- condition += " and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) if self.to_payment_date else ""
- dr_or_cr = ("gl.debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable'
- else "gl.credit_in_account_currency")
+ condition += (
+ " and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
+ if self.from_payment_date
+ else ""
+ )
+ condition += (
+ " and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
+ if self.to_payment_date
+ else ""
+ )
+ dr_or_cr = (
+ "debit_in_account_currency"
+ if erpnext.get_party_account_type(self.party_type) == "Receivable"
+ else "credit_in_account_currency"
+ )
if self.minimum_invoice_amount:
- condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_payment_amount))
+ condition += " and gl.{dr_or_cr} >= {amount}".format(
+ dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount)
+ )
if self.maximum_invoice_amount:
- condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_payment_amount))
+ condition += " and gl.{dr_or_cr} <= {amount}".format(
+ dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount)
+ )
else:
- condition += " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) if self.from_payment_date else ""
- condition += " and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) if self.to_payment_date else ""
+ condition += (
+ " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
+ if self.from_payment_date
+ else ""
+ )
+ condition += (
+ " and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
+ if self.to_payment_date
+ else ""
+ )
if self.minimum_payment_amount:
- condition += " and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount)) if get_payments \
+ condition += (
+ " and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
+ if get_payments
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
+ )
if self.maximum_payment_amount:
- condition += " and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount)) if get_payments \
+ condition += (
+ " and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
+ if get_payments
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
+ )
return condition
+
def reconcile_dr_cr_note(dr_cr_notes, company):
for inv in dr_cr_notes:
- voucher_type = ('Credit Note'
- if inv.voucher_type == 'Sales Invoice' else 'Debit Note')
+ voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
- reconcile_dr_or_cr = ('debit_in_account_currency'
- if inv.dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency')
+ reconcile_dr_or_cr = (
+ "debit_in_account_currency"
+ if inv.dr_or_cr == "credit_in_account_currency"
+ else "credit_in_account_currency"
+ )
company_currency = erpnext.get_company_currency(company)
- jv = frappe.get_doc({
- "doctype": "Journal Entry",
- "voucher_type": voucher_type,
- "posting_date": today(),
- "company": company,
- "multi_currency": 1 if inv.currency != company_currency else 0,
- "accounts": [
- {
- 'account': inv.account,
- 'party': inv.party,
- 'party_type': inv.party_type,
- inv.dr_or_cr: abs(inv.allocated_amount),
- 'reference_type': inv.against_voucher_type,
- 'reference_name': inv.against_voucher,
- 'cost_center': erpnext.get_default_cost_center(company)
- },
- {
- 'account': inv.account,
- 'party': inv.party,
- 'party_type': inv.party_type,
- reconcile_dr_or_cr: (abs(inv.allocated_amount)
- if abs(inv.unadjusted_amount) > abs(inv.allocated_amount) else abs(inv.unadjusted_amount)),
- 'reference_type': inv.voucher_type,
- 'reference_name': inv.voucher_no,
- 'cost_center': erpnext.get_default_cost_center(company)
- }
- ]
- })
+ jv = frappe.get_doc(
+ {
+ "doctype": "Journal Entry",
+ "voucher_type": voucher_type,
+ "posting_date": today(),
+ "company": company,
+ "multi_currency": 1 if inv.currency != company_currency else 0,
+ "accounts": [
+ {
+ "account": inv.account,
+ "party": inv.party,
+ "party_type": inv.party_type,
+ inv.dr_or_cr: abs(inv.allocated_amount),
+ "reference_type": inv.against_voucher_type,
+ "reference_name": inv.against_voucher,
+ "cost_center": erpnext.get_default_cost_center(company),
+ },
+ {
+ "account": inv.account,
+ "party": inv.party,
+ "party_type": inv.party_type,
+ reconcile_dr_or_cr: (
+ abs(inv.allocated_amount)
+ if abs(inv.unadjusted_amount) > abs(inv.allocated_amount)
+ else abs(inv.unadjusted_amount)
+ ),
+ "reference_type": inv.voucher_type,
+ "reference_name": inv.voucher_no,
+ "cost_center": erpnext.get_default_cost_center(company),
+ },
+ ],
+ }
+ )
jv.flags.ignore_mandatory = True
jv.submit()
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index 2271f48a2b9..d2374b77a63 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -1,9 +1,96 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-# import frappe
import unittest
+import frappe
+from frappe.utils import add_days, getdate
+
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+
class TestPaymentReconciliation(unittest.TestCase):
- pass
+ @classmethod
+ def setUpClass(cls):
+ make_customer()
+ make_invoice_and_payment()
+
+ def test_payment_reconciliation(self):
+ payment_reco = frappe.get_doc("Payment Reconciliation")
+ payment_reco.company = "_Test Company"
+ payment_reco.party_type = "Customer"
+ payment_reco.party = "_Test Payment Reco Customer"
+ payment_reco.receivable_payable_account = "Debtors - _TC"
+ payment_reco.from_invoice_date = add_days(getdate(), -1)
+ payment_reco.to_invoice_date = getdate()
+ payment_reco.from_payment_date = add_days(getdate(), -1)
+ payment_reco.to_payment_date = getdate()
+ payment_reco.maximum_invoice_amount = 1000
+ payment_reco.maximum_payment_amount = 1000
+ payment_reco.invoice_limit = 10
+ payment_reco.payment_limit = 10
+ payment_reco.bank_cash_account = "_Test Bank - _TC"
+ payment_reco.cost_center = "_Test Cost Center - _TC"
+ payment_reco.get_unreconciled_entries()
+
+ self.assertEqual(len(payment_reco.get("invoices")), 1)
+ self.assertEqual(len(payment_reco.get("payments")), 1)
+
+ payment_entry = payment_reco.get("payments")[0].reference_name
+ invoice = payment_reco.get("invoices")[0].invoice_number
+
+ payment_reco.allocate_entries(
+ {
+ "payments": [payment_reco.get("payments")[0].as_dict()],
+ "invoices": [payment_reco.get("invoices")[0].as_dict()],
+ }
+ )
+ payment_reco.reconcile()
+
+ payment_entry_doc = frappe.get_doc("Payment Entry", payment_entry)
+ self.assertEqual(payment_entry_doc.get("references")[0].reference_name, invoice)
+
+
+def make_customer():
+ if not frappe.db.get_value("Customer", "_Test Payment Reco Customer"):
+ frappe.get_doc(
+ {
+ "doctype": "Customer",
+ "customer_name": "_Test Payment Reco Customer",
+ "customer_type": "Individual",
+ "customer_group": "_Test Customer Group",
+ "territory": "_Test Territory",
+ }
+ ).insert()
+
+
+def make_invoice_and_payment():
+ si = create_sales_invoice(
+ customer="_Test Payment Reco Customer", qty=1, rate=690, do_not_save=True
+ )
+ si.cost_center = "_Test Cost Center - _TC"
+ si.save()
+ si.submit()
+
+ pe = frappe.get_doc(
+ {
+ "doctype": "Payment Entry",
+ "payment_type": "Receive",
+ "party_type": "Customer",
+ "party": "_Test Payment Reco Customer",
+ "company": "_Test Company",
+ "paid_from_account_currency": "INR",
+ "paid_to_account_currency": "INR",
+ "source_exchange_rate": 1,
+ "target_exchange_rate": 1,
+ "reference_no": "1",
+ "reference_date": getdate(),
+ "received_amount": 690,
+ "paid_amount": 690,
+ "paid_from": "Debtors - _TC",
+ "paid_to": "_Test Bank - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ }
+ )
+ pe.insert()
+ pe.submit()
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 1a833a4008e..f05dd5ba496 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -24,7 +24,7 @@ from erpnext.erpnext_integrations.stripe_integration import create_stripe_subscr
class PaymentRequest(Document):
def validate(self):
if self.get("__islocal"):
- self.status = 'Draft'
+ self.status = "Draft"
self.validate_reference_document()
self.validate_payment_request_amount()
self.validate_currency()
@@ -35,51 +35,67 @@ class PaymentRequest(Document):
frappe.throw(_("To create a Payment Request reference document is required"))
def validate_payment_request_amount(self):
- existing_payment_request_amount = \
- get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
+ existing_payment_request_amount = get_existing_payment_request_amount(
+ self.reference_doctype, self.reference_name
+ )
if existing_payment_request_amount:
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
- if (hasattr(ref_doc, "order_type") \
- and getattr(ref_doc, "order_type") != "Shopping Cart"):
+ if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") != "Shopping Cart":
ref_amount = get_amount(ref_doc, self.payment_account)
- if existing_payment_request_amount + flt(self.grand_total)> ref_amount:
- frappe.throw(_("Total Payment Request amount cannot be greater than {0} amount")
- .format(self.reference_doctype))
+ if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
+ frappe.throw(
+ _("Total Payment Request amount cannot be greater than {0} amount").format(
+ self.reference_doctype
+ )
+ )
def validate_currency(self):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
- if self.payment_account and ref_doc.currency != frappe.db.get_value("Account", self.payment_account, "account_currency"):
+ if self.payment_account and ref_doc.currency != frappe.db.get_value(
+ "Account", self.payment_account, "account_currency"
+ ):
frappe.throw(_("Transaction currency must be same as Payment Gateway currency"))
def validate_subscription_details(self):
if self.is_a_subscription:
amount = 0
for subscription_plan in self.subscription_plans:
- payment_gateway = frappe.db.get_value("Subscription Plan", subscription_plan.plan, "payment_gateway")
+ payment_gateway = frappe.db.get_value(
+ "Subscription Plan", subscription_plan.plan, "payment_gateway"
+ )
if payment_gateway != self.payment_gateway_account:
- frappe.throw(_('The payment gateway account in plan {0} is different from the payment gateway account in this payment request').format(subscription_plan.name))
+ frappe.throw(
+ _(
+ "The payment gateway account in plan {0} is different from the payment gateway account in this payment request"
+ ).format(subscription_plan.name)
+ )
rate = get_plan_rate(subscription_plan.plan, quantity=subscription_plan.qty)
amount += rate
if amount != self.grand_total:
- frappe.msgprint(_("The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document.").format(self.grand_total, amount))
+ frappe.msgprint(
+ _(
+ "The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document."
+ ).format(self.grand_total, amount)
+ )
def on_submit(self):
- if self.payment_request_type == 'Outward':
- self.db_set('status', 'Initiated')
+ if self.payment_request_type == "Outward":
+ self.db_set("status", "Initiated")
return
- elif self.payment_request_type == 'Inward':
- self.db_set('status', 'Requested')
+ elif self.payment_request_type == "Inward":
+ self.db_set("status", "Requested")
send_mail = self.payment_gateway_validation() if self.payment_gateway else None
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
- if (hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart") \
- or self.flags.mute_email:
+ if (
+ hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart"
+ ) or self.flags.mute_email:
send_mail = False
if send_mail and self.payment_channel != "Phone":
@@ -101,23 +117,27 @@ class PaymentRequest(Document):
request_amount=request_amount,
sender=self.email_to,
currency=self.currency,
- payment_gateway=self.payment_gateway
+ payment_gateway=self.payment_gateway,
)
controller.validate_transaction_currency(self.currency)
controller.request_for_payment(**payment_record)
def get_request_amount(self):
- data_of_completed_requests = frappe.get_all("Integration Request", filters={
- 'reference_doctype': self.doctype,
- 'reference_docname': self.name,
- 'status': 'Completed'
- }, pluck="data")
+ data_of_completed_requests = frappe.get_all(
+ "Integration Request",
+ filters={
+ "reference_doctype": self.doctype,
+ "reference_docname": self.name,
+ "status": "Completed",
+ },
+ pluck="data",
+ )
if not data_of_completed_requests:
return self.grand_total
- request_amounts = sum(json.loads(d).get('request_amount') for d in data_of_completed_requests)
+ request_amounts = sum(json.loads(d).get("request_amount") for d in data_of_completed_requests)
return request_amounts
def on_cancel(self):
@@ -126,8 +146,9 @@ class PaymentRequest(Document):
def make_invoice(self):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
- if (hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart"):
+ if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart":
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
+
si = make_sales_invoice(self.reference_name, ignore_permissions=True)
si.allocate_advances_automatically = True
si = si.insert(ignore_permissions=True)
@@ -136,7 +157,7 @@ class PaymentRequest(Document):
def payment_gateway_validation(self):
try:
controller = get_payment_gateway_controller(self.payment_gateway)
- if hasattr(controller, 'on_payment_request_submission'):
+ if hasattr(controller, "on_payment_request_submission"):
return controller.on_payment_request_submission(self)
else:
return True
@@ -148,36 +169,45 @@ class PaymentRequest(Document):
self.payment_url = self.get_payment_url()
if self.payment_url:
- self.db_set('payment_url', self.payment_url)
+ self.db_set("payment_url", self.payment_url)
- if self.payment_url or not self.payment_gateway_account \
- or (self.payment_gateway_account and self.payment_channel == "Phone"):
- self.db_set('status', 'Initiated')
+ if (
+ self.payment_url
+ or not self.payment_gateway_account
+ or (self.payment_gateway_account and self.payment_channel == "Phone")
+ ):
+ self.db_set("status", "Initiated")
def get_payment_url(self):
if self.reference_doctype != "Fees":
- data = frappe.db.get_value(self.reference_doctype, self.reference_name, ["company", "customer_name"], as_dict=1)
+ data = frappe.db.get_value(
+ self.reference_doctype, self.reference_name, ["company", "customer_name"], as_dict=1
+ )
else:
- data = frappe.db.get_value(self.reference_doctype, self.reference_name, ["student_name"], as_dict=1)
+ data = frappe.db.get_value(
+ self.reference_doctype, self.reference_name, ["student_name"], as_dict=1
+ )
data.update({"company": frappe.defaults.get_defaults().company})
controller = get_payment_gateway_controller(self.payment_gateway)
controller.validate_transaction_currency(self.currency)
- if hasattr(controller, 'validate_minimum_transaction_amount'):
+ if hasattr(controller, "validate_minimum_transaction_amount"):
controller.validate_minimum_transaction_amount(self.currency, self.grand_total)
- return controller.get_payment_url(**{
- "amount": flt(self.grand_total, self.precision("grand_total")),
- "title": data.company.encode("utf-8"),
- "description": self.subject.encode("utf-8"),
- "reference_doctype": "Payment Request",
- "reference_docname": self.name,
- "payer_email": self.email_to or frappe.session.user,
- "payer_name": frappe.safe_encode(data.customer_name),
- "order_id": self.name,
- "currency": self.currency
- })
+ return controller.get_payment_url(
+ **{
+ "amount": flt(self.grand_total, self.precision("grand_total")),
+ "title": data.company.encode("utf-8"),
+ "description": self.subject.encode("utf-8"),
+ "reference_doctype": "Payment Request",
+ "reference_docname": self.name,
+ "payer_email": self.email_to or frappe.session.user,
+ "payer_name": frappe.safe_encode(data.customer_name),
+ "order_id": self.name,
+ "currency": self.currency,
+ }
+ )
def set_as_paid(self):
if self.payment_channel == "Phone":
@@ -202,32 +232,47 @@ class PaymentRequest(Document):
else:
party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company)
- party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account)
+ party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(
+ party_account
+ )
bank_amount = self.grand_total
- if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
+ if (
+ party_account_currency == ref_doc.company_currency and party_account_currency != self.currency
+ ):
party_amount = ref_doc.base_grand_total
else:
party_amount = self.grand_total
- payment_entry = get_payment_entry(self.reference_doctype, self.reference_name, party_amount=party_amount,
- bank_account=self.payment_account, bank_amount=bank_amount)
+ payment_entry = get_payment_entry(
+ self.reference_doctype,
+ self.reference_name,
+ party_amount=party_amount,
+ bank_account=self.payment_account,
+ bank_amount=bank_amount,
+ )
- payment_entry.update({
- "reference_no": self.name,
- "reference_date": nowdate(),
- "remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(self.reference_doctype,
- self.reference_name, self.name)
- })
+ payment_entry.update(
+ {
+ "reference_no": self.name,
+ "reference_date": nowdate(),
+ "remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(
+ self.reference_doctype, self.reference_name, self.name
+ ),
+ }
+ )
if payment_entry.difference_amount:
company_details = get_company_defaults(ref_doc.company)
- payment_entry.append("deductions", {
- "account": company_details.exchange_gain_loss_account,
- "cost_center": company_details.cost_center,
- "amount": payment_entry.difference_amount
- })
+ payment_entry.append(
+ "deductions",
+ {
+ "account": company_details.exchange_gain_loss_account,
+ "cost_center": company_details.cost_center,
+ "amount": payment_entry.difference_amount,
+ },
+ )
if submit:
payment_entry.insert(ignore_permissions=True)
@@ -243,16 +288,23 @@ class PaymentRequest(Document):
"subject": self.subject,
"message": self.get_message(),
"now": True,
- "attachments": [frappe.attach_print(self.reference_doctype, self.reference_name,
- file_name=self.reference_name, print_format=self.print_format)]}
- enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args)
+ "attachments": [
+ frappe.attach_print(
+ self.reference_doctype,
+ self.reference_name,
+ file_name=self.reference_name,
+ print_format=self.print_format,
+ )
+ ],
+ }
+ enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
def get_message(self):
"""return message with payment gateway link"""
context = {
"doc": frappe.get_doc(self.reference_doctype, self.reference_name),
- "payment_url": self.payment_url
+ "payment_url": self.payment_url,
}
if self.message:
@@ -266,22 +318,26 @@ class PaymentRequest(Document):
def check_if_payment_entry_exists(self):
if self.status == "Paid":
- if frappe.get_all("Payment Entry Reference",
+ if frappe.get_all(
+ "Payment Entry Reference",
filters={"reference_name": self.reference_name, "docstatus": ["<", 2]},
fields=["parent"],
- limit=1):
- frappe.throw(_("Payment Entry already exists"), title=_('Error'))
+ limit=1,
+ ):
+ frappe.throw(_("Payment Entry already exists"), title=_("Error"))
def make_communication_entry(self):
"""Make communication entry"""
- comm = frappe.get_doc({
- "doctype":"Communication",
- "subject": self.subject,
- "content": self.get_message(),
- "sent_or_received": "Sent",
- "reference_doctype": self.reference_doctype,
- "reference_name": self.reference_name
- })
+ comm = frappe.get_doc(
+ {
+ "doctype": "Communication",
+ "subject": self.subject,
+ "content": self.get_message(),
+ "sent_or_received": "Sent",
+ "reference_doctype": self.reference_doctype,
+ "reference_name": self.reference_name,
+ }
+ )
comm.insert(ignore_permissions=True)
def get_payment_success_url(self):
@@ -298,16 +354,17 @@ class PaymentRequest(Document):
self.set_as_paid()
# if shopping cart enabled and in session
- if (shopping_cart_settings.enabled and hasattr(frappe.local, "session")
- and frappe.local.session.user != "Guest") and self.payment_channel != "Phone":
+ if (
+ shopping_cart_settings.enabled
+ and hasattr(frappe.local, "session")
+ and frappe.local.session.user != "Guest"
+ ) and self.payment_channel != "Phone":
success_url = shopping_cart_settings.payment_success_url
if success_url:
- redirect_to = ({
- "Orders": "/orders",
- "Invoices": "/invoices",
- "My Account": "/me"
- }).get(success_url, "/me")
+ redirect_to = ({"Orders": "/orders", "Invoices": "/invoices", "My Account": "/me"}).get(
+ success_url, "/me"
+ )
else:
redirect_to = get_url("/orders/{0}".format(self.reference_name))
@@ -317,6 +374,7 @@ class PaymentRequest(Document):
if payment_provider == "stripe":
return create_stripe_subscription(gateway_controller, data)
+
@frappe.whitelist(allow_guest=True)
def make_payment_request(**args):
"""Make payment request"""
@@ -329,55 +387,68 @@ def make_payment_request(**args):
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
if args.loyalty_points and args.dt == "Sales Order":
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
+
loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points))
- frappe.db.set_value("Sales Order", args.dn, "loyalty_points", int(args.loyalty_points), update_modified=False)
- frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False)
+ frappe.db.set_value(
+ "Sales Order", args.dn, "loyalty_points", int(args.loyalty_points), update_modified=False
+ )
+ frappe.db.set_value(
+ "Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False
+ )
grand_total = grand_total - loyalty_amount
- bank_account = (get_party_bank_account(args.get('party_type'), args.get('party'))
- if args.get('party_type') else '')
+ bank_account = (
+ get_party_bank_account(args.get("party_type"), args.get("party"))
+ if args.get("party_type")
+ else ""
+ )
existing_payment_request = None
if args.order_type == "Shopping Cart":
- existing_payment_request = frappe.db.get_value("Payment Request",
- {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)})
+ existing_payment_request = frappe.db.get_value(
+ "Payment Request",
+ {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)},
+ )
if existing_payment_request:
- frappe.db.set_value("Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False)
+ frappe.db.set_value(
+ "Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False
+ )
pr = frappe.get_doc("Payment Request", existing_payment_request)
else:
if args.order_type != "Shopping Cart":
- existing_payment_request_amount = \
- get_existing_payment_request_amount(args.dt, args.dn)
+ existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
if existing_payment_request_amount:
grand_total -= existing_payment_request_amount
pr = frappe.new_doc("Payment Request")
- pr.update({
- "payment_gateway_account": gateway_account.get("name"),
- "payment_gateway": gateway_account.get("payment_gateway"),
- "payment_account": gateway_account.get("payment_account"),
- "payment_channel": gateway_account.get("payment_channel"),
- "payment_request_type": args.get("payment_request_type"),
- "currency": ref_doc.currency,
- "grand_total": grand_total,
- "mode_of_payment": args.mode_of_payment,
- "email_to": args.recipient_id or ref_doc.owner,
- "subject": _("Payment Request for {0}").format(args.dn),
- "message": gateway_account.get("message") or get_dummy_message(ref_doc),
- "reference_doctype": args.dt,
- "reference_name": args.dn,
- "party_type": args.get("party_type") or "Customer",
- "party": args.get("party") or ref_doc.get("customer"),
- "bank_account": bank_account
- })
+ pr.update(
+ {
+ "payment_gateway_account": gateway_account.get("name"),
+ "payment_gateway": gateway_account.get("payment_gateway"),
+ "payment_account": gateway_account.get("payment_account"),
+ "payment_channel": gateway_account.get("payment_channel"),
+ "payment_request_type": args.get("payment_request_type"),
+ "currency": ref_doc.currency,
+ "grand_total": grand_total,
+ "mode_of_payment": args.mode_of_payment,
+ "email_to": args.recipient_id or ref_doc.owner,
+ "subject": _("Payment Request for {0}").format(args.dn),
+ "message": gateway_account.get("message") or get_dummy_message(ref_doc),
+ "reference_doctype": args.dt,
+ "reference_name": args.dn,
+ "party_type": args.get("party_type") or "Customer",
+ "party": args.get("party") or ref_doc.get("customer"),
+ "bank_account": bank_account,
+ }
+ )
if args.order_type == "Shopping Cart" or args.mute_email:
pr.flags.mute_email = True
- pr.insert(ignore_permissions=True)
if args.submit_doc:
+ pr.insert(ignore_permissions=True)
pr.submit()
if args.order_type == "Shopping Cart":
@@ -390,11 +461,15 @@ def make_payment_request(**args):
return pr.as_dict()
+
def get_amount(ref_doc, payment_account=None):
"""get amount based on doctype"""
dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]:
- grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
+ if ref_doc.party_account_currency == ref_doc.currency:
+ grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
+ else:
+ grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) / ref_doc.conversion_rate
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if ref_doc.party_account_currency == ref_doc.currency:
@@ -411,18 +486,20 @@ def get_amount(ref_doc, payment_account=None):
elif dt == "Fees":
grand_total = ref_doc.outstanding_amount
- if grand_total > 0 :
+ if grand_total > 0:
return grand_total
else:
frappe.throw(_("Payment Entry is already created"))
+
def get_existing_payment_request_amount(ref_dt, ref_dn):
"""
Get the existing payment request which are unpaid or partially paid for payment channel other than Phone
and get the summation of existing paid payment request for Phone payment channel.
"""
- existing_payment_request_amount = frappe.db.sql("""
+ existing_payment_request_amount = frappe.db.sql(
+ """
select sum(grand_total)
from `tabPayment Request`
where
@@ -432,9 +509,12 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
and (status != 'Paid'
or (payment_channel = 'Phone'
and status = 'Paid'))
- """, (ref_dt, ref_dn))
+ """,
+ (ref_dt, ref_dn),
+ )
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
+
def get_gateway_details(args):
"""return gateway and payment account of default payment gateway"""
if args.get("payment_gateway_account"):
@@ -448,58 +528,74 @@ def get_gateway_details(args):
return gateway_account
+
def get_payment_gateway_account(args):
- return frappe.db.get_value("Payment Gateway Account", args,
+ return frappe.db.get_value(
+ "Payment Gateway Account",
+ args,
["name", "payment_gateway", "payment_account", "message"],
- as_dict=1)
+ as_dict=1,
+ )
+
@frappe.whitelist()
def get_print_format_list(ref_doctype):
print_format_list = ["Standard"]
- print_format_list.extend([p.name for p in frappe.get_all("Print Format",
- filters={"doc_type": ref_doctype})])
+ print_format_list.extend(
+ [p.name for p in frappe.get_all("Print Format", filters={"doc_type": ref_doctype})]
+ )
+
+ return {"print_format": print_format_list}
- return {
- "print_format": print_format_list
- }
@frappe.whitelist(allow_guest=True)
def resend_payment_email(docname):
return frappe.get_doc("Payment Request", docname).send_email()
+
@frappe.whitelist()
def make_payment_entry(docname):
doc = frappe.get_doc("Payment Request", docname)
return doc.create_payment_entry(submit=False).as_dict()
+
def update_payment_req_status(doc, method):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details
for ref in doc.references:
- payment_request_name = frappe.db.get_value("Payment Request",
- {"reference_doctype": ref.reference_doctype, "reference_name": ref.reference_name,
- "docstatus": 1})
+ payment_request_name = frappe.db.get_value(
+ "Payment Request",
+ {
+ "reference_doctype": ref.reference_doctype,
+ "reference_name": ref.reference_name,
+ "docstatus": 1,
+ },
+ )
if payment_request_name:
- ref_details = get_reference_details(ref.reference_doctype, ref.reference_name, doc.party_account_currency)
- pay_req_doc = frappe.get_doc('Payment Request', payment_request_name)
+ ref_details = get_reference_details(
+ ref.reference_doctype, ref.reference_name, doc.party_account_currency
+ )
+ pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
status = pay_req_doc.status
if status != "Paid" and not ref_details.outstanding_amount:
- status = 'Paid'
+ status = "Paid"
elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount:
- status = 'Partially Paid'
+ status = "Partially Paid"
elif ref_details.outstanding_amount == ref_details.total_amount:
- if pay_req_doc.payment_request_type == 'Outward':
- status = 'Initiated'
- elif pay_req_doc.payment_request_type == 'Inward':
- status = 'Requested'
+ if pay_req_doc.payment_request_type == "Outward":
+ status = "Initiated"
+ elif pay_req_doc.payment_request_type == "Inward":
+ status = "Requested"
+
+ pay_req_doc.db_set("status", status)
- pay_req_doc.db_set('status', status)
def get_dummy_message(doc):
- return frappe.render_template("""{% if doc.contact_person -%}
+ return frappe.render_template(
+ """{% if doc.contact_person -%}
{{ _("If you have any questions, please get back to us.") }}
{{ _("Thank you for your business!") }}
-""", dict(doc=doc, payment_url = '{{ payment_url }}'))
+""",
+ dict(doc=doc, payment_url="{{ payment_url }}"),
+ )
+
@frappe.whitelist()
def get_subscription_details(reference_doctype, reference_name):
if reference_doctype == "Sales Invoice":
- subscriptions = frappe.db.sql("""SELECT parent as sub_name FROM `tabSubscription Invoice` WHERE invoice=%s""",reference_name, as_dict=1)
+ subscriptions = frappe.db.sql(
+ """SELECT parent as sub_name FROM `tabSubscription Invoice` WHERE invoice=%s""",
+ reference_name,
+ as_dict=1,
+ )
subscription_plans = []
for subscription in subscriptions:
plans = frappe.get_doc("Subscription", subscription.sub_name).plans
@@ -524,38 +627,50 @@ def get_subscription_details(reference_doctype, reference_name):
subscription_plans.append(plan)
return subscription_plans
+
@frappe.whitelist()
def make_payment_order(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
+
def set_missing_values(source, target):
target.payment_order_type = "Payment Request"
- target.append('references', {
- 'reference_doctype': source.reference_doctype,
- 'reference_name': source.reference_name,
- 'amount': source.grand_total,
- 'supplier': source.party,
- 'payment_request': source_name,
- 'mode_of_payment': source.mode_of_payment,
- 'bank_account': source.bank_account,
- 'account': source.account
- })
+ target.append(
+ "references",
+ {
+ "reference_doctype": source.reference_doctype,
+ "reference_name": source.reference_name,
+ "amount": source.grand_total,
+ "supplier": source.party,
+ "payment_request": source_name,
+ "mode_of_payment": source.mode_of_payment,
+ "bank_account": source.bank_account,
+ "account": source.account,
+ },
+ )
- doclist = get_mapped_doc("Payment Request", source_name, {
- "Payment Request": {
- "doctype": "Payment Order",
- }
- }, target_doc, set_missing_values)
+ doclist = get_mapped_doc(
+ "Payment Request",
+ source_name,
+ {
+ "Payment Request": {
+ "doctype": "Payment Order",
+ }
+ },
+ target_doc,
+ set_missing_values,
+ )
return doclist
+
def validate_payment(doc, method=None):
if doc.reference_doctype != "Payment Request" or (
- frappe.db.get_value(doc.reference_doctype, doc.reference_docname, 'status')
- != "Paid"
+ frappe.db.get_value(doc.reference_doctype, doc.reference_docname, "status") != "Paid"
):
return
frappe.throw(
- _("The Payment Request {0} is already paid, cannot process payment twice")
- .format(doc.reference_docname)
+ _("The Payment Request {0} is already paid, cannot process payment twice").format(
+ doc.reference_docname
+ )
)
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index f679ccfe4ff..355784716a0 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -12,10 +12,7 @@ from erpnext.setup.utils import get_exchange_rate
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
-payment_gateway = {
- "doctype": "Payment Gateway",
- "gateway": "_Test Gateway"
-}
+payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"}
payment_method = [
{
@@ -23,30 +20,38 @@ payment_method = [
"is_default": 1,
"payment_gateway": "_Test Gateway",
"payment_account": "_Test Bank - _TC",
- "currency": "INR"
+ "currency": "INR",
},
{
"doctype": "Payment Gateway Account",
"payment_gateway": "_Test Gateway",
"payment_account": "_Test Bank USD - _TC",
- "currency": "USD"
- }
+ "currency": "USD",
+ },
]
+
class TestPaymentRequest(unittest.TestCase):
def setUp(self):
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
for method in payment_method:
- if not frappe.db.get_value("Payment Gateway Account", {"payment_gateway": method["payment_gateway"],
- "currency": method["currency"]}, "name"):
+ if not frappe.db.get_value(
+ "Payment Gateway Account",
+ {"payment_gateway": method["payment_gateway"], "currency": method["currency"]},
+ "name",
+ ):
frappe.get_doc(method).insert(ignore_permissions=True)
def test_payment_request_linkings(self):
so_inr = make_sales_order(currency="INR")
- pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com",
- payment_gateway_account="_Test Gateway - INR")
+ pr = make_payment_request(
+ dt="Sales Order",
+ dn=so_inr.name,
+ recipient_id="saurabh@erpnext.com",
+ payment_gateway_account="_Test Gateway - INR",
+ )
self.assertEqual(pr.reference_doctype, "Sales Order")
self.assertEqual(pr.reference_name, so_inr.name)
@@ -55,45 +60,75 @@ class TestPaymentRequest(unittest.TestCase):
conversion_rate = get_exchange_rate("USD", "INR")
si_usd = create_sales_invoice(currency="USD", conversion_rate=conversion_rate)
- pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
- payment_gateway_account="_Test Gateway - USD")
+ pr = make_payment_request(
+ dt="Sales Invoice",
+ dn=si_usd.name,
+ recipient_id="saurabh@erpnext.com",
+ payment_gateway_account="_Test Gateway - USD",
+ )
self.assertEqual(pr.reference_doctype, "Sales Invoice")
self.assertEqual(pr.reference_name, si_usd.name)
self.assertEqual(pr.currency, "USD")
def test_payment_entry(self):
- frappe.db.set_value("Company", "_Test Company",
- "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC")
+ frappe.db.set_value(
+ "Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
+ )
frappe.db.set_value("Company", "_Test Company", "write_off_account", "_Test Write Off - _TC")
frappe.db.set_value("Company", "_Test Company", "cost_center", "_Test Cost Center - _TC")
so_inr = make_sales_order(currency="INR")
- pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com",
- mute_email=1, payment_gateway_account="_Test Gateway - INR", submit_doc=1, return_doc=1)
+ pr = make_payment_request(
+ dt="Sales Order",
+ dn=so_inr.name,
+ recipient_id="saurabh@erpnext.com",
+ mute_email=1,
+ payment_gateway_account="_Test Gateway - INR",
+ submit_doc=1,
+ return_doc=1,
+ )
pe = pr.set_as_paid()
so_inr = frappe.get_doc("Sales Order", so_inr.name)
self.assertEqual(so_inr.advance_paid, 1000)
- si_usd = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
- currency="USD", conversion_rate=50)
+ si_usd = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
- pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
- mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1)
+ pr = make_payment_request(
+ dt="Sales Invoice",
+ dn=si_usd.name,
+ recipient_id="saurabh@erpnext.com",
+ mute_email=1,
+ payment_gateway_account="_Test Gateway - USD",
+ submit_doc=1,
+ return_doc=1,
+ )
pe = pr.set_as_paid()
- expected_gle = dict((d[0], d) for d in [
- ["_Test Receivable USD - _TC", 0, 5000, si_usd.name],
- [pr.payment_account, 6290.0, 0, None],
- ["_Test Exchange Gain/Loss - _TC", 0, 1290, None]
- ])
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["_Test Receivable USD - _TC", 0, 5000, si_usd.name],
+ [pr.payment_account, 6290.0, 0, None],
+ ["_Test Exchange Gain/Loss - _TC", 0, 1290, None],
+ ]
+ )
- gl_entries = frappe.db.sql("""select account, debit, credit, against_voucher
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit, against_voucher
from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s
- order by account asc""", pe.name, as_dict=1)
+ order by account asc""",
+ pe.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -104,35 +139,49 @@ class TestPaymentRequest(unittest.TestCase):
self.assertEqual(expected_gle[gle.account][3], gle.against_voucher)
def test_status(self):
- si_usd = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
- currency="USD", conversion_rate=50)
+ si_usd = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
- pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
- mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1)
+ pr = make_payment_request(
+ dt="Sales Invoice",
+ dn=si_usd.name,
+ recipient_id="saurabh@erpnext.com",
+ mute_email=1,
+ payment_gateway_account="_Test Gateway - USD",
+ submit_doc=1,
+ return_doc=1,
+ )
pe = pr.create_payment_entry()
pr.load_from_db()
- self.assertEqual(pr.status, 'Paid')
+ self.assertEqual(pr.status, "Paid")
pe.cancel()
pr.load_from_db()
- self.assertEqual(pr.status, 'Requested')
+ self.assertEqual(pr.status, "Requested")
def test_multiple_payment_entries_against_sales_order(self):
# Make Sales Order, grand_total = 1000
so = make_sales_order()
# Payment Request amount = 200
- pr1 = make_payment_request(dt="Sales Order", dn=so.name,
- recipient_id="nabin@erpnext.com", return_doc=1)
+ pr1 = make_payment_request(
+ dt="Sales Order", dn=so.name, recipient_id="nabin@erpnext.com", return_doc=1
+ )
pr1.grand_total = 200
+ pr1.insert()
pr1.submit()
# Make a 2nd Payment Request
- pr2 = make_payment_request(dt="Sales Order", dn=so.name,
- recipient_id="nabin@erpnext.com", return_doc=1)
+ pr2 = make_payment_request(
+ dt="Sales Order", dn=so.name, recipient_id="nabin@erpnext.com", return_doc=1
+ )
self.assertEqual(pr2.grand_total, 800)
diff --git a/erpnext/accounts/doctype/payment_term/payment_term_dashboard.py b/erpnext/accounts/doctype/payment_term/payment_term_dashboard.py
index 7f5b96c8a8a..8df97bf6c77 100644
--- a/erpnext/accounts/doctype/payment_term/payment_term_dashboard.py
+++ b/erpnext/accounts/doctype/payment_term/payment_term_dashboard.py
@@ -1,21 +1,12 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'payment_term',
- 'transactions': [
- {
- 'label': _('Sales'),
- 'items': ['Sales Invoice', 'Sales Order', 'Quotation']
- },
- {
- 'label': _('Purchase'),
- 'items': ['Purchase Invoice', 'Purchase Order']
- },
- {
- 'items': ['Payment Terms Template']
- }
- ]
+ "fieldname": "payment_term",
+ "transactions": [
+ {"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Quotation"]},
+ {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order"]},
+ {"items": ["Payment Terms Template"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
index 3a6999c5799..ea3b76c5243 100644
--- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
+++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
@@ -16,10 +16,12 @@ class PaymentTermsTemplate(Document):
def validate_invoice_portion(self):
total_portion = 0
for term in self.terms:
- total_portion += flt(term.get('invoice_portion', 0))
+ total_portion += flt(term.get("invoice_portion", 0))
if flt(total_portion, 2) != 100.00:
- frappe.msgprint(_('Combined invoice portion must equal 100%'), raise_exception=1, indicator='red')
+ frappe.msgprint(
+ _("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red"
+ )
def check_duplicate_terms(self):
terms = []
@@ -27,8 +29,9 @@ class PaymentTermsTemplate(Document):
term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on)
if term_info in terms:
frappe.msgprint(
- _('The Payment Term at row {0} is possibly a duplicate.').format(term.idx),
- raise_exception=1, indicator='red'
+ _("The Payment Term at row {0} is possibly a duplicate.").format(term.idx),
+ raise_exception=1,
+ indicator="red",
)
else:
terms.append(term_info)
diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py
index aa5de2ca3f8..34ac773febc 100644
--- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py
+++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py
@@ -1,32 +1,19 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'payment_terms_template',
- 'non_standard_fieldnames': {
- 'Customer Group': 'payment_terms',
- 'Supplier Group': 'payment_terms',
- 'Supplier': 'payment_terms',
- 'Customer': 'payment_terms'
+ "fieldname": "payment_terms_template",
+ "non_standard_fieldnames": {
+ "Customer Group": "payment_terms",
+ "Supplier Group": "payment_terms",
+ "Supplier": "payment_terms",
+ "Customer": "payment_terms",
},
- 'transactions': [
- {
- 'label': _('Sales'),
- 'items': ['Sales Invoice', 'Sales Order', 'Quotation']
- },
- {
- 'label': _('Purchase'),
- 'items': ['Purchase Invoice', 'Purchase Order']
- },
- {
- 'label': _('Party'),
- 'items': ['Customer', 'Supplier']
- },
- {
- 'label': _('Group'),
- 'items': ['Customer Group', 'Supplier Group']
- }
- ]
+ "transactions": [
+ {"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Quotation"]},
+ {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order"]},
+ {"label": _("Party"), "items": ["Customer", "Supplier"]},
+ {"label": _("Group"), "items": ["Customer Group", "Supplier Group"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.py
index 8529ef5c3d5..9717f2009a0 100644
--- a/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.py
+++ b/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.py
@@ -8,64 +8,76 @@ import frappe
class TestPaymentTermsTemplate(unittest.TestCase):
def tearDown(self):
- frappe.delete_doc('Payment Terms Template', '_Test Payment Terms Template For Test', force=1)
+ frappe.delete_doc("Payment Terms Template", "_Test Payment Terms Template For Test", force=1)
def test_create_template(self):
- template = frappe.get_doc({
- 'doctype': 'Payment Terms Template',
- 'template_name': '_Test Payment Terms Template For Test',
- 'terms': [{
- 'doctype': 'Payment Terms Template Detail',
- 'invoice_portion': 50.00,
- 'credit_days_based_on': 'Day(s) after invoice date',
- 'credit_days': 30
- }]
- })
+ template = frappe.get_doc(
+ {
+ "doctype": "Payment Terms Template",
+ "template_name": "_Test Payment Terms Template For Test",
+ "terms": [
+ {
+ "doctype": "Payment Terms Template Detail",
+ "invoice_portion": 50.00,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": 30,
+ }
+ ],
+ }
+ )
self.assertRaises(frappe.ValidationError, template.insert)
- template.append('terms', {
- 'doctype': 'Payment Terms Template Detail',
- 'invoice_portion': 50.00,
- 'credit_days_based_on': 'Day(s) after invoice date',
- 'credit_days': 0
- })
+ template.append(
+ "terms",
+ {
+ "doctype": "Payment Terms Template Detail",
+ "invoice_portion": 50.00,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": 0,
+ },
+ )
template.insert()
def test_credit_days(self):
- template = frappe.get_doc({
- 'doctype': 'Payment Terms Template',
- 'template_name': '_Test Payment Terms Template For Test',
- 'terms': [{
- 'doctype': 'Payment Terms Template Detail',
- 'invoice_portion': 100.00,
- 'credit_days_based_on': 'Day(s) after invoice date',
- 'credit_days': -30
- }]
- })
+ template = frappe.get_doc(
+ {
+ "doctype": "Payment Terms Template",
+ "template_name": "_Test Payment Terms Template For Test",
+ "terms": [
+ {
+ "doctype": "Payment Terms Template Detail",
+ "invoice_portion": 100.00,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": -30,
+ }
+ ],
+ }
+ )
self.assertRaises(frappe.ValidationError, template.insert)
def test_duplicate_terms(self):
- template = frappe.get_doc({
- 'doctype': 'Payment Terms Template',
- 'template_name': '_Test Payment Terms Template For Test',
- 'terms': [
- {
- 'doctype': 'Payment Terms Template Detail',
- 'invoice_portion': 50.00,
- 'credit_days_based_on': 'Day(s) after invoice date',
- 'credit_days': 30
- },
- {
- 'doctype': 'Payment Terms Template Detail',
- 'invoice_portion': 50.00,
- 'credit_days_based_on': 'Day(s) after invoice date',
- 'credit_days': 30
- }
-
- ]
- })
+ template = frappe.get_doc(
+ {
+ "doctype": "Payment Terms Template",
+ "template_name": "_Test Payment Terms Template For Test",
+ "terms": [
+ {
+ "doctype": "Payment Terms Template Detail",
+ "invoice_portion": 50.00,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": 30,
+ },
+ {
+ "doctype": "Payment Terms Template Detail",
+ "invoice_portion": 50.00,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": 30,
+ },
+ ],
+ }
+ )
self.assertRaises(frappe.ValidationError, template.insert)
diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
index d8a024248be..53b1c64c460 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
@@ -23,40 +23,52 @@ class PeriodClosingVoucher(AccountsController):
self.make_gl_entries()
def on_cancel(self):
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
from erpnext.accounts.general_ledger import make_reverse_gl_entries
+
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
def validate_account_head(self):
closing_account_type = frappe.db.get_value("Account", self.closing_account_head, "root_type")
if closing_account_type not in ["Liability", "Equity"]:
- frappe.throw(_("Closing Account {0} must be of type Liability / Equity")
- .format(self.closing_account_head))
+ frappe.throw(
+ _("Closing Account {0} must be of type Liability / Equity").format(self.closing_account_head)
+ )
account_currency = get_account_currency(self.closing_account_head)
- company_currency = frappe.get_cached_value('Company', self.company, "default_currency")
+ company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
if account_currency != company_currency:
frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency))
def validate_posting_date(self):
from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year
- validate_fiscal_year(self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self)
+ validate_fiscal_year(
+ self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self
+ )
- self.year_start_date = get_fiscal_year(self.posting_date, self.fiscal_year, company=self.company)[1]
+ self.year_start_date = get_fiscal_year(
+ self.posting_date, self.fiscal_year, company=self.company
+ )[1]
- pce = frappe.db.sql("""select name from `tabPeriod Closing Voucher`
+ pce = frappe.db.sql(
+ """select name from `tabPeriod Closing Voucher`
where posting_date > %s and fiscal_year = %s and docstatus = 1""",
- (self.posting_date, self.fiscal_year))
+ (self.posting_date, self.fiscal_year),
+ )
if pce and pce[0][0]:
- frappe.throw(_("Another Period Closing Entry {0} has been made after {1}")
- .format(pce[0][0], self.posting_date))
+ frappe.throw(
+ _("Another Period Closing Entry {0} has been made after {1}").format(
+ pce[0][0], self.posting_date
+ )
+ )
def make_gl_entries(self):
gl_entries = self.get_gl_entries()
if gl_entries:
from erpnext.accounts.general_ledger import make_gl_entries
+
make_gl_entries(gl_entries)
def get_gl_entries(self):
@@ -65,16 +77,29 @@ class PeriodClosingVoucher(AccountsController):
for acc in pl_accounts:
if flt(acc.bal_in_company_currency):
- gl_entries.append(self.get_gl_dict({
- "account": acc.account,
- "cost_center": acc.cost_center,
- "finance_book": acc.finance_book,
- "account_currency": acc.account_currency,
- "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0,
- "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
- "credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0,
- "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0
- }, item=acc))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": acc.account,
+ "cost_center": acc.cost_center,
+ "finance_book": acc.finance_book,
+ "account_currency": acc.account_currency,
+ "debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
+ if flt(acc.bal_in_account_currency) < 0
+ else 0,
+ "debit": abs(flt(acc.bal_in_company_currency))
+ if flt(acc.bal_in_company_currency) < 0
+ else 0,
+ "credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
+ if flt(acc.bal_in_account_currency) > 0
+ else 0,
+ "credit": abs(flt(acc.bal_in_company_currency))
+ if flt(acc.bal_in_company_currency) > 0
+ else 0,
+ },
+ item=acc,
+ )
+ )
if gl_entries:
gle_for_net_pl_bal = self.get_pnl_gl_entry(pl_accounts)
@@ -89,16 +114,27 @@ class PeriodClosingVoucher(AccountsController):
for acc in pl_accounts:
if flt(acc.bal_in_company_currency):
cost_center = acc.cost_center if self.cost_center_wise_pnl else company_cost_center
- gl_entry = self.get_gl_dict({
- "account": self.closing_account_head,
- "cost_center": cost_center,
- "finance_book": acc.finance_book,
- "account_currency": acc.account_currency,
- "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0,
- "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
- "credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0,
- "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0
- }, item=acc)
+ gl_entry = self.get_gl_dict(
+ {
+ "account": self.closing_account_head,
+ "cost_center": cost_center,
+ "finance_book": acc.finance_book,
+ "account_currency": acc.account_currency,
+ "debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
+ if flt(acc.bal_in_account_currency) > 0
+ else 0,
+ "debit": abs(flt(acc.bal_in_company_currency))
+ if flt(acc.bal_in_company_currency) > 0
+ else 0,
+ "credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
+ if flt(acc.bal_in_account_currency) < 0
+ else 0,
+ "credit": abs(flt(acc.bal_in_company_currency))
+ if flt(acc.bal_in_company_currency) < 0
+ else 0,
+ },
+ item=acc,
+ )
self.update_default_dimensions(gl_entry)
@@ -112,27 +148,31 @@ class PeriodClosingVoucher(AccountsController):
_, default_dimensions = get_dimensions()
for dimension in self.accounting_dimensions:
- gl_entry.update({
- dimension: default_dimensions.get(self.company, {}).get(dimension)
- })
+ gl_entry.update({dimension: default_dimensions.get(self.company, {}).get(dimension)})
def get_pl_balances(self):
"""Get balance for dimension-wise pl accounts"""
- dimension_fields = ['t1.cost_center', 't1.finance_book']
+ dimension_fields = ["t1.cost_center", "t1.finance_book"]
self.accounting_dimensions = get_accounting_dimensions()
for dimension in self.accounting_dimensions:
- dimension_fields.append('t1.{0}'.format(dimension))
+ dimension_fields.append("t1.{0}".format(dimension))
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
select
t1.account, t2.account_currency, {dimension_fields},
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
from `tabGL Entry` t1, `tabAccount` t2
- where t1.account = t2.name and t2.report_type = 'Profit and Loss'
+ where t1.is_cancelled = 0 and t1.account = t2.name and t2.report_type = 'Profit and Loss'
and t2.docstatus < 2 and t2.company = %s
and t1.posting_date between %s and %s
group by t1.account, {dimension_fields}
- """.format(dimension_fields = ', '.join(dimension_fields)), (self.company, self.get("year_start_date"), self.posting_date), as_dict=1)
+ """.format(
+ dimension_fields=", ".join(dimension_fields)
+ ),
+ (self.company, self.get("year_start_date"), self.posting_date),
+ as_dict=1,
+ )
diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
index 030b4caf7ca..8e0e62d5f8c 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
@@ -2,7 +2,6 @@
# License: GNU General Public License v3. See license.txt
-
import unittest
import frappe
@@ -19,7 +18,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
company = create_company()
- cost_center = create_cost_center('Test Cost Center 1')
+ cost_center = create_cost_center("Test Cost Center 1")
jv1 = make_journal_entry(
amount=400,
@@ -27,7 +26,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
account2="Sales - TPC",
cost_center=cost_center,
posting_date=now(),
- save=False
+ save=False,
)
jv1.company = company
jv1.save()
@@ -39,7 +38,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
account2="Cash - TPC",
cost_center=cost_center,
posting_date=now(),
- save=False
+ save=False,
)
jv2.company = company
jv2.save()
@@ -49,14 +48,17 @@ class TestPeriodClosingVoucher(unittest.TestCase):
surplus_account = pcv.closing_account_head
expected_gle = (
- ('Cost of Goods Sold - TPC', 0.0, 600.0),
+ ("Cost of Goods Sold - TPC", 0.0, 600.0),
(surplus_account, 600.0, 400.0),
- ('Sales - TPC', 400.0, 0.0)
+ ("Sales - TPC", 400.0, 0.0),
)
- pcv_gle = frappe.db.sql("""
+ pcv_gle = frappe.db.sql(
+ """
select account, debit, credit from `tabGL Entry` where voucher_no=%s order by account
- """, (pcv.name))
+ """,
+ (pcv.name),
+ )
self.assertEqual(pcv_gle, expected_gle)
@@ -75,7 +77,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
rate=400,
- debit_to="Debtors - TPC"
+ debit_to="Debtors - TPC",
)
create_sales_invoice(
company=company,
@@ -83,7 +85,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
rate=200,
- debit_to="Debtors - TPC"
+ debit_to="Debtors - TPC",
)
pcv = self.make_period_closing_voucher(submit=False)
@@ -95,15 +97,18 @@ class TestPeriodClosingVoucher(unittest.TestCase):
expected_gle = (
(surplus_account, 0.0, 400.0, cost_center1),
(surplus_account, 0.0, 200.0, cost_center2),
- ('Sales - TPC', 400.0, 0.0, cost_center1),
- ('Sales - TPC', 200.0, 0.0, cost_center2),
+ ("Sales - TPC", 400.0, 0.0, cost_center1),
+ ("Sales - TPC", 200.0, 0.0, cost_center2),
)
- pcv_gle = frappe.db.sql("""
+ pcv_gle = frappe.db.sql(
+ """
select account, debit, credit, cost_center
from `tabGL Entry` where voucher_no=%s
order by account, cost_center
- """, (pcv.name))
+ """,
+ (pcv.name),
+ )
self.assertEqual(pcv_gle, expected_gle)
@@ -120,14 +125,14 @@ class TestPeriodClosingVoucher(unittest.TestCase):
expense_account="Cost of Goods Sold - TPC",
cost_center=cost_center,
rate=400,
- debit_to="Debtors - TPC"
+ debit_to="Debtors - TPC",
)
jv = make_journal_entry(
account1="Cash - TPC",
account2="Sales - TPC",
amount=400,
cost_center=cost_center,
- posting_date=now()
+ posting_date=now(),
)
jv.company = company
jv.finance_book = create_finance_book().name
@@ -140,69 +145,84 @@ class TestPeriodClosingVoucher(unittest.TestCase):
expected_gle = (
(surplus_account, 0.0, 400.0, None),
(surplus_account, 0.0, 400.0, jv.finance_book),
- ('Sales - TPC', 400.0, 0.0, None),
- ('Sales - TPC', 400.0, 0.0, jv.finance_book)
+ ("Sales - TPC", 400.0, 0.0, None),
+ ("Sales - TPC", 400.0, 0.0, jv.finance_book),
)
- pcv_gle = frappe.db.sql("""
+ pcv_gle = frappe.db.sql(
+ """
select account, debit, credit, finance_book
from `tabGL Entry` where voucher_no=%s
order by account, finance_book
- """, (pcv.name))
+ """,
+ (pcv.name),
+ )
self.assertEqual(pcv_gle, expected_gle)
def make_period_closing_voucher(self, submit=True):
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
- pcv = frappe.get_doc({
- "doctype": "Period Closing Voucher",
- "transaction_date": today(),
- "posting_date": today(),
- "company": "Test PCV Company",
- "fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0],
- "cost_center": cost_center,
- "closing_account_head": surplus_account,
- "remarks": "test"
- })
+ pcv = frappe.get_doc(
+ {
+ "doctype": "Period Closing Voucher",
+ "transaction_date": today(),
+ "posting_date": today(),
+ "company": "Test PCV Company",
+ "fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0],
+ "cost_center": cost_center,
+ "closing_account_head": surplus_account,
+ "remarks": "test",
+ }
+ )
pcv.insert()
if submit:
pcv.submit()
return pcv
+
def create_company():
- company = frappe.get_doc({
- 'doctype': 'Company',
- 'company_name': "Test PCV Company",
- 'country': 'United States',
- 'default_currency': 'USD'
- })
- company.insert(ignore_if_duplicate = True)
+ company = frappe.get_doc(
+ {
+ "doctype": "Company",
+ "company_name": "Test PCV Company",
+ "country": "United States",
+ "default_currency": "USD",
+ }
+ )
+ company.insert(ignore_if_duplicate=True)
return company.name
+
def create_account():
- account = frappe.get_doc({
- "account_name": "Reserve and Surplus",
- "is_group": 0,
- "company": "Test PCV Company",
- "root_type": "Liability",
- "report_type": "Balance Sheet",
- "account_currency": "USD",
- "parent_account": "Current Liabilities - TPC",
- "doctype": "Account"
- }).insert(ignore_if_duplicate = True)
+ account = frappe.get_doc(
+ {
+ "account_name": "Reserve and Surplus",
+ "is_group": 0,
+ "company": "Test PCV Company",
+ "root_type": "Liability",
+ "report_type": "Balance Sheet",
+ "account_currency": "USD",
+ "parent_account": "Current Liabilities - TPC",
+ "doctype": "Account",
+ }
+ ).insert(ignore_if_duplicate=True)
return account.name
+
def create_cost_center(cc_name):
- costcenter = frappe.get_doc({
- "company": "Test PCV Company",
- "cost_center_name": cc_name,
- "doctype": "Cost Center",
- "parent_cost_center": "Test PCV Company - TPC"
- })
- costcenter.insert(ignore_if_duplicate = True)
+ costcenter = frappe.get_doc(
+ {
+ "company": "Test PCV Company",
+ "cost_center_name": cc_name,
+ "doctype": "Cost Center",
+ "parent_cost_center": "Test PCV Company - TPC",
+ }
+ )
+ costcenter.insert(ignore_if_duplicate=True)
return costcenter.name
+
test_dependencies = ["Customer", "Cost Center"]
test_records = frappe.get_test_records("Period Closing Voucher")
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
index 07059cb7aa3..49aab0d0bbf 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
@@ -23,21 +23,33 @@ class POSClosingEntry(StatusUpdater):
def validate_pos_invoices(self):
invalid_rows = []
for d in self.pos_transactions:
- invalid_row = {'idx': d.idx}
- pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice,
- ["consolidated_invoice", "pos_profile", "docstatus", "owner"], as_dict=1)[0]
+ invalid_row = {"idx": d.idx}
+ pos_invoice = frappe.db.get_values(
+ "POS Invoice",
+ d.pos_invoice,
+ ["consolidated_invoice", "pos_profile", "docstatus", "owner"],
+ as_dict=1,
+ )[0]
if pos_invoice.consolidated_invoice:
- invalid_row.setdefault('msg', []).append(_('POS Invoice is {}').format(frappe.bold("already consolidated")))
+ invalid_row.setdefault("msg", []).append(
+ _("POS Invoice is {}").format(frappe.bold("already consolidated"))
+ )
invalid_rows.append(invalid_row)
continue
if pos_invoice.pos_profile != self.pos_profile:
- invalid_row.setdefault('msg', []).append(_("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile)))
+ invalid_row.setdefault("msg", []).append(
+ _("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile))
+ )
if pos_invoice.docstatus != 1:
- invalid_row.setdefault('msg', []).append(_('POS Invoice is not {}').format(frappe.bold("submitted")))
+ invalid_row.setdefault("msg", []).append(
+ _("POS Invoice is not {}").format(frappe.bold("submitted"))
+ )
if pos_invoice.owner != self.user:
- invalid_row.setdefault('msg', []).append(_("POS Invoice isn't created by user {}").format(frappe.bold(self.owner)))
+ invalid_row.setdefault("msg", []).append(
+ _("POS Invoice isn't created by user {}").format(frappe.bold(self.owner))
+ )
- if invalid_row.get('msg'):
+ if invalid_row.get("msg"):
invalid_rows.append(invalid_row)
if not invalid_rows:
@@ -45,16 +57,18 @@ class POSClosingEntry(StatusUpdater):
error_list = []
for row in invalid_rows:
- for msg in row.get('msg'):
- error_list.append(_("Row #{}: {}").format(row.get('idx'), msg))
+ for msg in row.get("msg"):
+ error_list.append(_("Row #{}: {}").format(row.get("idx"), msg))
frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
@frappe.whitelist()
def get_payment_reconciliation_details(self):
- currency = frappe.get_cached_value('Company', self.company, "default_currency")
- return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html",
- {"data": self, "currency": currency})
+ currency = frappe.get_cached_value("Company", self.company, "default_currency")
+ return frappe.render_template(
+ "erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html",
+ {"data": self, "currency": currency},
+ )
def on_submit(self):
consolidate_pos_invoices(closing_entry=self)
@@ -72,29 +86,38 @@ class POSClosingEntry(StatusUpdater):
opening_entry.set_status()
opening_entry.save()
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_cashiers(doctype, txt, searchfield, start, page_len, filters):
- cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=['user'], as_list=1)
+ cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=["user"], as_list=1)
return [c for c in cashiers_list]
+
@frappe.whitelist()
def get_pos_invoices(start, end, pos_profile, user):
- data = frappe.db.sql("""
+ data = frappe.db.sql(
+ """
select
name, timestamp(posting_date, posting_time) as "timestamp"
from
`tabPOS Invoice`
where
owner = %s and docstatus = 1 and pos_profile = %s and ifnull(consolidated_invoice,'') = ''
- """, (user, pos_profile), as_dict=1)
+ """,
+ (user, pos_profile),
+ as_dict=1,
+ )
- data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data))
+ data = list(
+ filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data)
+ )
# need to get taxes and payments so can't avoid get_doc
data = [frappe.get_doc("POS Invoice", d.name).as_dict() for d in data]
return data
+
def make_closing_entry_from_opening(opening_entry):
closing_entry = frappe.new_doc("POS Closing Entry")
closing_entry.pos_opening_entry = opening_entry.name
@@ -107,26 +130,38 @@ def make_closing_entry_from_opening(opening_entry):
closing_entry.net_total = 0
closing_entry.total_quantity = 0
- invoices = get_pos_invoices(closing_entry.period_start_date, closing_entry.period_end_date,
- closing_entry.pos_profile, closing_entry.user)
+ invoices = get_pos_invoices(
+ closing_entry.period_start_date,
+ closing_entry.period_end_date,
+ closing_entry.pos_profile,
+ closing_entry.user,
+ )
pos_transactions = []
taxes = []
payments = []
for detail in opening_entry.balance_details:
- payments.append(frappe._dict({
- 'mode_of_payment': detail.mode_of_payment,
- 'opening_amount': detail.opening_amount,
- 'expected_amount': detail.opening_amount
- }))
+ payments.append(
+ frappe._dict(
+ {
+ "mode_of_payment": detail.mode_of_payment,
+ "opening_amount": detail.opening_amount,
+ "expected_amount": detail.opening_amount,
+ }
+ )
+ )
for d in invoices:
- pos_transactions.append(frappe._dict({
- 'pos_invoice': d.name,
- 'posting_date': d.posting_date,
- 'grand_total': d.grand_total,
- 'customer': d.customer
- }))
+ pos_transactions.append(
+ frappe._dict(
+ {
+ "pos_invoice": d.name,
+ "posting_date": d.posting_date,
+ "grand_total": d.grand_total,
+ "customer": d.customer,
+ }
+ )
+ )
closing_entry.grand_total += flt(d.grand_total)
closing_entry.net_total += flt(d.net_total)
closing_entry.total_quantity += flt(d.total_qty)
@@ -134,24 +169,22 @@ def make_closing_entry_from_opening(opening_entry):
for t in d.taxes:
existing_tax = [tx for tx in taxes if tx.account_head == t.account_head and tx.rate == t.rate]
if existing_tax:
- existing_tax[0].amount += flt(t.tax_amount);
+ existing_tax[0].amount += flt(t.tax_amount)
else:
- taxes.append(frappe._dict({
- 'account_head': t.account_head,
- 'rate': t.rate,
- 'amount': t.tax_amount
- }))
+ taxes.append(
+ frappe._dict({"account_head": t.account_head, "rate": t.rate, "amount": t.tax_amount})
+ )
for p in d.payments:
existing_pay = [pay for pay in payments if pay.mode_of_payment == p.mode_of_payment]
if existing_pay:
- existing_pay[0].expected_amount += flt(p.amount);
+ existing_pay[0].expected_amount += flt(p.amount)
else:
- payments.append(frappe._dict({
- 'mode_of_payment': p.mode_of_payment,
- 'opening_amount': 0,
- 'expected_amount': p.amount
- }))
+ payments.append(
+ frappe._dict(
+ {"mode_of_payment": p.mode_of_payment, "opening_amount": 0, "expected_amount": p.amount}
+ )
+ )
closing_entry.set("pos_transactions", pos_transactions)
closing_entry.set("payment_reconciliation", payments)
diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
index c40cd363d8f..2d4acc4d047 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
@@ -28,24 +28,20 @@ class TestPOSClosingEntry(unittest.TestCase):
opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
- pos_inv1.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3500
- })
+ pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
pos_inv1.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
- pos_inv2.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
- })
+ pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv2.submit()
pcv_doc = make_closing_entry_from_opening(opening_entry)
payment = pcv_doc.payment_reconciliation[0]
- self.assertEqual(payment.mode_of_payment, 'Cash')
+ self.assertEqual(payment.mode_of_payment, "Cash")
for d in pcv_doc.payment_reconciliation:
- if d.mode_of_payment == 'Cash':
+ if d.mode_of_payment == "Cash":
d.closing_amount = 6700
pcv_doc.submit()
@@ -58,24 +54,20 @@ class TestPOSClosingEntry(unittest.TestCase):
opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
- pos_inv1.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3500
- })
+ pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
pos_inv1.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
- pos_inv2.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
- })
+ pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv2.submit()
pcv_doc = make_closing_entry_from_opening(opening_entry)
payment = pcv_doc.payment_reconciliation[0]
- self.assertEqual(payment.mode_of_payment, 'Cash')
+ self.assertEqual(payment.mode_of_payment, "Cash")
for d in pcv_doc.payment_reconciliation:
- if d.mode_of_payment == 'Cash':
+ if d.mode_of_payment == "Cash":
d.closing_amount = 6700
pcv_doc.submit()
@@ -91,22 +83,19 @@ class TestPOSClosingEntry(unittest.TestCase):
si_doc.load_from_db()
pos_inv1.load_from_db()
self.assertEqual(si_doc.docstatus, 2)
- self.assertEqual(pos_inv1.status, 'Paid')
+ self.assertEqual(pos_inv1.status, "Paid")
def init_user_and_profile(**args):
- user = 'test@example.com'
- test_user = frappe.get_doc('User', user)
+ user = "test@example.com"
+ test_user = frappe.get_doc("User", user)
roles = ("Accounts Manager", "Accounts User", "Sales Manager")
test_user.add_roles(*roles)
frappe.set_user(user)
pos_profile = make_pos_profile(**args)
- pos_profile.append('applicable_for_users', {
- 'default': 1,
- 'user': user
- })
+ pos_profile.append("applicable_for_users", {"default": 1, "user": user})
pos_profile.save()
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
index 0c6e7edeb02..b8500270d1a 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
@@ -264,7 +264,6 @@
"print_hide": 1
},
{
- "allow_on_submit": 1,
"default": "0",
"fieldname": "is_return",
"fieldtype": "Check",
@@ -1573,7 +1572,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
- "modified": "2021-10-05 12:11:53.871828",
+ "modified": "2022-03-22 13:00:24.166684",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
@@ -1623,6 +1622,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"timeline_field": "customer",
"title_field": "title",
"track_changes": 1,
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 9d585411582..96975e9d116 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -17,7 +17,11 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
)
from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty
-from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
+from erpnext.stock.doctype.serial_no.serial_no import (
+ get_delivered_serial_nos,
+ get_pos_reserved_serial_nos,
+ get_serial_nos,
+)
class POSInvoice(SalesInvoice):
@@ -26,7 +30,9 @@ class POSInvoice(SalesInvoice):
def validate(self):
if not cint(self.is_pos):
- frappe.throw(_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment")))
+ frappe.throw(
+ _("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment"))
+ )
# run on validate method of selling controller
super(SalesInvoice, self).validate()
@@ -50,11 +56,12 @@ class POSInvoice(SalesInvoice):
self.validate_loyalty_transaction()
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
+
validate_coupon_code(self.coupon_code)
def on_submit(self):
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
- if self.loyalty_program:
+ if not self.is_return and self.loyalty_program:
self.make_loyalty_point_entry()
elif self.is_return and self.return_against and self.loyalty_program:
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
@@ -67,28 +74,32 @@ class POSInvoice(SalesInvoice):
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count
- update_coupon_code_count(self.coupon_code,'used')
+
+ update_coupon_code_count(self.coupon_code, "used")
def before_cancel(self):
- if self.consolidated_invoice and frappe.db.get_value('Sales Invoice', self.consolidated_invoice, 'docstatus') == 1:
+ if (
+ self.consolidated_invoice
+ and frappe.db.get_value("Sales Invoice", self.consolidated_invoice, "docstatus") == 1
+ ):
pos_closing_entry = frappe.get_all(
"POS Invoice Reference",
ignore_permissions=True,
- filters={ 'pos_invoice': self.name },
+ filters={"pos_invoice": self.name},
pluck="parent",
- limit=1
+ limit=1,
)
frappe.throw(
- _('You need to cancel POS Closing Entry {} to be able to cancel this document.').format(
+ _("You need to cancel POS Closing Entry {} to be able to cancel this document.").format(
get_link_to_form("POS Closing Entry", pos_closing_entry[0])
),
- title=_('Not Allowed')
+ title=_("Not Allowed"),
)
def on_cancel(self):
# run on cancel method of selling controller
super(SalesInvoice, self).on_cancel()
- if self.loyalty_program:
+ if not self.is_return and self.loyalty_program:
self.delete_loyalty_point_entry()
elif self.is_return and self.return_against and self.loyalty_program:
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
@@ -97,16 +108,22 @@ class POSInvoice(SalesInvoice):
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count
- update_coupon_code_count(self.coupon_code,'cancelled')
+
+ update_coupon_code_count(self.coupon_code, "cancelled")
def check_phone_payments(self):
for pay in self.payments:
if pay.type == "Phone" and pay.amount >= 0:
- paid_amt = frappe.db.get_value("Payment Request",
+ paid_amt = frappe.db.get_value(
+ "Payment Request",
filters=dict(
- reference_doctype="POS Invoice", reference_name=self.name,
- mode_of_payment=pay.mode_of_payment, status="Paid"),
- fieldname="grand_total")
+ reference_doctype="POS Invoice",
+ reference_name=self.name,
+ mode_of_payment=pay.mode_of_payment,
+ status="Paid",
+ ),
+ fieldname="grand_total",
+ )
if paid_amt and pay.amount != paid_amt:
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
@@ -120,52 +137,73 @@ class POSInvoice(SalesInvoice):
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
- bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos))
+ bold_invalid_serial_nos = frappe.bold(", ".join(invalid_serial_nos))
if len(invalid_serial_nos) == 1:
- frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
- .format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
+ frappe.throw(
+ _(
+ "Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no."
+ ).format(item.idx, bold_invalid_serial_nos),
+ title=_("Item Unavailable"),
+ )
elif invalid_serial_nos:
- frappe.throw(_("Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no.")
- .format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
+ frappe.throw(
+ _(
+ "Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no."
+ ).format(item.idx, bold_invalid_serial_nos),
+ title=_("Item Unavailable"),
+ )
def validate_pos_reserved_batch_qty(self, item):
- filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no":item.batch_no}
+ filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no": item.batch_no}
available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code)
reserved_batch_qty = get_pos_reserved_batch_qty(filters)
bold_item_name = frappe.bold(item.item_name)
- bold_extra_batch_qty_needed = frappe.bold(abs(available_batch_qty - reserved_batch_qty - item.qty))
+ bold_extra_batch_qty_needed = frappe.bold(
+ abs(available_batch_qty - reserved_batch_qty - item.qty)
+ )
bold_invalid_batch_no = frappe.bold(item.batch_no)
if (available_batch_qty - reserved_batch_qty) == 0:
- frappe.throw(_("Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no.")
- .format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable"))
+ frappe.throw(
+ _(
+ "Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no."
+ ).format(item.idx, bold_invalid_batch_no, bold_item_name),
+ title=_("Item Unavailable"),
+ )
elif (available_batch_qty - reserved_batch_qty - item.qty) < 0:
- frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required")
- .format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable"))
+ frappe.throw(
+ _(
+ "Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
+ ).format(
+ item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed
+ ),
+ title=_("Item Unavailable"),
+ )
def validate_delivered_serial_nos(self, item):
- serial_nos = get_serial_nos(item.serial_no)
- delivered_serial_nos = frappe.db.get_list('Serial No', {
- 'item_code': item.item_code,
- 'name': ['in', serial_nos],
- 'sales_invoice': ['is', 'set']
- }, pluck='name')
+ delivered_serial_nos = get_delivered_serial_nos(item.serial_no)
if delivered_serial_nos:
- bold_delivered_serial_nos = frappe.bold(', '.join(delivered_serial_nos))
- frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.")
- .format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable"))
+ bold_delivered_serial_nos = frappe.bold(", ".join(delivered_serial_nos))
+ frappe.throw(
+ _(
+ "Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no."
+ ).format(item.idx, bold_delivered_serial_nos),
+ title=_("Item Unavailable"),
+ )
def validate_invalid_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
error_msg = []
invalid_serials, msg = "", ""
for serial_no in serial_nos:
- if not frappe.db.exists('Serial No', serial_no):
+ if not frappe.db.exists("Serial No", serial_no):
invalid_serials = invalid_serials + (", " if invalid_serials else "") + serial_no
- msg = (_("Row #{}: Following Serial numbers for item {} are Invalid: {}").format(item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials)))
+ msg = _("Row #{}: Following Serial numbers for item {} are Invalid: {}").format(
+ item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials)
+ )
if invalid_serials:
error_msg.append(msg)
@@ -173,11 +211,18 @@ class POSInvoice(SalesInvoice):
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
def validate_stock_availablility(self):
- if self.is_return or self.docstatus != 1:
+ if self.is_return:
return
- allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
- for d in self.get('items'):
- is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
+
+ if self.docstatus == 0 and not frappe.db.get_value(
+ "POS Profile", self.pos_profile, "validate_stock_on_save"
+ ):
+ return
+
+ allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock")
+
+ for d in self.get("items"):
+ is_service_item = not (frappe.db.get_value("Item", d.get("item_code"), "is_stock_item"))
if is_service_item:
return
if d.serial_no:
@@ -192,13 +237,25 @@ class POSInvoice(SalesInvoice):
available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
- item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
+ item_code, warehouse, qty = (
+ frappe.bold(d.item_code),
+ frappe.bold(d.warehouse),
+ frappe.bold(d.qty),
+ )
if flt(available_stock) <= 0:
- frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.')
- .format(d.idx, item_code, warehouse), title=_("Item Unavailable"))
+ frappe.throw(
+ _("Row #{}: Item Code: {} is not available under warehouse {}.").format(
+ d.idx, item_code, warehouse
+ ),
+ title=_("Item Unavailable"),
+ )
elif flt(available_stock) < flt(d.qty):
- frappe.throw(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
- .format(d.idx, item_code, warehouse, available_stock), title=_("Item Unavailable"))
+ frappe.throw(
+ _(
+ "Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
+ ).format(d.idx, item_code, warehouse, available_stock),
+ title=_("Item Unavailable"),
+ )
def validate_serialised_or_batched_item(self):
error_msg = []
@@ -212,16 +269,21 @@ class POSInvoice(SalesInvoice):
item_code = frappe.bold(d.item_code)
serial_nos = get_serial_nos(d.serial_no)
if serialized and batched and (no_batch_selected or no_serial_selected):
- msg = (_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.')
- .format(d.idx, item_code))
+ msg = _(
+ "Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction."
+ ).format(d.idx, item_code)
elif serialized and no_serial_selected:
- msg = (_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.')
- .format(d.idx, item_code))
+ msg = _(
+ "Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction."
+ ).format(d.idx, item_code)
elif batched and no_batch_selected:
- msg = (_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.')
- .format(d.idx, item_code))
+ msg = _(
+ "Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction."
+ ).format(d.idx, item_code)
elif serialized and not no_serial_selected and len(serial_nos) != d.qty:
- msg = (_("Row #{}: You must select {} serial numbers for item {}.").format(d.idx, frappe.bold(cint(d.qty)), item_code))
+ msg = _("Row #{}: You must select {} serial numbers for item {}.").format(
+ d.idx, frappe.bold(cint(d.qty)), item_code
+ )
if msg:
error_msg.append(msg)
@@ -230,18 +292,22 @@ class POSInvoice(SalesInvoice):
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
def validate_return_items_qty(self):
- if not self.get("is_return"): return
+ if not self.get("is_return"):
+ return
for d in self.get("items"):
if d.get("qty") > 0:
frappe.throw(
- _("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.")
- .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")
+ _(
+ "Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return."
+ ).format(d.idx, frappe.bold(d.item_code)),
+ title=_("Invalid Item"),
)
if d.get("serial_no"):
serial_nos = get_serial_nos(d.serial_no)
for sr in serial_nos:
- serial_no_exists = frappe.db.sql("""
+ serial_no_exists = frappe.db.sql(
+ """
SELECT name
FROM `tabPOS Invoice Item`
WHERE
@@ -251,14 +317,17 @@ class POSInvoice(SalesInvoice):
or serial_no like %s
or serial_no like %s
)
- """, (self.return_against, sr, sr+'\n%', '%\n'+sr, '%\n'+sr+'\n%'))
+ """,
+ (self.return_against, sr, sr + "\n%", "%\n" + sr, "%\n" + sr + "\n%"),
+ )
if not serial_no_exists:
bold_return_against = frappe.bold(self.return_against)
bold_serial_no = frappe.bold(sr)
frappe.throw(
- _("Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}")
- .format(d.idx, bold_serial_no, bold_return_against)
+ _(
+ "Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}"
+ ).format(d.idx, bold_serial_no, bold_return_against)
)
def validate_mode_of_payment(self):
@@ -266,16 +335,25 @@ class POSInvoice(SalesInvoice):
frappe.throw(_("At least one mode of payment is required for POS invoice."))
def validate_change_account(self):
- if self.change_amount and self.account_for_change_amount and \
- frappe.db.get_value("Account", self.account_for_change_amount, "company") != self.company:
- frappe.throw(_("The selected change account {} doesn't belongs to Company {}.").format(self.account_for_change_amount, self.company))
+ if (
+ self.change_amount
+ and self.account_for_change_amount
+ and frappe.db.get_value("Account", self.account_for_change_amount, "company") != self.company
+ ):
+ frappe.throw(
+ _("The selected change account {} doesn't belongs to Company {}.").format(
+ self.account_for_change_amount, self.company
+ )
+ )
def validate_change_amount(self):
grand_total = flt(self.rounded_total) or flt(self.grand_total)
base_grand_total = flt(self.base_rounded_total) or flt(self.base_grand_total)
if not flt(self.change_amount) and grand_total < flt(self.paid_amount):
self.change_amount = flt(self.paid_amount - grand_total + flt(self.write_off_amount))
- self.base_change_amount = flt(self.base_paid_amount) - base_grand_total + flt(self.base_write_off_amount)
+ self.base_change_amount = (
+ flt(self.base_paid_amount) - base_grand_total + flt(self.base_write_off_amount)
+ )
if flt(self.change_amount) and not self.account_for_change_amount:
frappe.msgprint(_("Please enter Account for Change Amount"), raise_exception=1)
@@ -295,8 +373,12 @@ class POSInvoice(SalesInvoice):
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
def validate_loyalty_transaction(self):
- if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center):
- expense_account, cost_center = frappe.db.get_value('Loyalty Program', self.loyalty_program, ["expense_account", "cost_center"])
+ if self.redeem_loyalty_points and (
+ not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center
+ ):
+ expense_account, cost_center = frappe.db.get_value(
+ "Loyalty Program", self.loyalty_program, ["expense_account", "cost_center"]
+ )
if not self.loyalty_redemption_account:
self.loyalty_redemption_account = expense_account
if not self.loyalty_redemption_cost_center:
@@ -307,8 +389,8 @@ class POSInvoice(SalesInvoice):
def set_status(self, update=False, status=None, update_modified=True):
if self.is_new():
- if self.get('amended_from'):
- self.status = 'Draft'
+ if self.get("amended_from"):
+ self.status = "Draft"
return
if not status:
@@ -317,19 +399,35 @@ class POSInvoice(SalesInvoice):
elif self.docstatus == 1:
if self.consolidated_invoice:
self.status = "Consolidated"
- elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) < getdate(nowdate()) and self.is_discounted and self.get_discounting_status()=='Disbursed':
+ elif (
+ flt(self.outstanding_amount) > 0
+ and getdate(self.due_date) < getdate(nowdate())
+ and self.is_discounted
+ and self.get_discounting_status() == "Disbursed"
+ ):
self.status = "Overdue and Discounted"
elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) < getdate(nowdate()):
self.status = "Overdue"
- elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) >= getdate(nowdate()) and self.is_discounted and self.get_discounting_status()=='Disbursed':
+ elif (
+ flt(self.outstanding_amount) > 0
+ and getdate(self.due_date) >= getdate(nowdate())
+ and self.is_discounted
+ and self.get_discounting_status() == "Disbursed"
+ ):
self.status = "Unpaid and Discounted"
elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) >= getdate(nowdate()):
self.status = "Unpaid"
- elif flt(self.outstanding_amount) <= 0 and self.is_return == 0 and frappe.db.get_value('POS Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
+ elif (
+ flt(self.outstanding_amount) <= 0
+ and self.is_return == 0
+ and frappe.db.get_value(
+ "POS Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
+ )
+ ):
self.status = "Credit Note Issued"
elif self.is_return == 1:
self.status = "Return"
- elif flt(self.outstanding_amount)<=0:
+ elif flt(self.outstanding_amount) <= 0:
self.status = "Paid"
else:
self.status = "Submitted"
@@ -337,22 +435,23 @@ class POSInvoice(SalesInvoice):
self.status = "Draft"
if update:
- self.db_set('status', self.status, update_modified = update_modified)
+ self.db_set("status", self.status, update_modified=update_modified)
def set_pos_fields(self, for_validate=False):
"""Set retail related fields from POS Profiles"""
from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details
+
if not self.pos_profile:
pos_profile = get_pos_profile(self.company) or {}
if not pos_profile:
frappe.throw(_("No POS Profile found. Please create a New POS Profile first"))
- self.pos_profile = pos_profile.get('name')
+ self.pos_profile = pos_profile.get("name")
profile = {}
if self.pos_profile:
- profile = frappe.get_doc('POS Profile', self.pos_profile)
+ profile = frappe.get_doc("POS Profile", self.pos_profile)
- if not self.get('payments') and not for_validate:
+ if not self.get("payments") and not for_validate:
update_multi_mode_option(self, profile)
if self.is_return and not for_validate:
@@ -362,35 +461,55 @@ class POSInvoice(SalesInvoice):
if not for_validate and not self.customer:
self.customer = profile.customer
- self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
- self.set_warehouse = profile.get('warehouse') or self.set_warehouse
+ self.account_for_change_amount = (
+ profile.get("account_for_change_amount") or self.account_for_change_amount
+ )
+ self.set_warehouse = profile.get("warehouse") or self.set_warehouse
- for fieldname in ('currency', 'letter_head', 'tc_name',
- 'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges',
- 'write_off_cost_center', 'apply_discount_on', 'cost_center', 'tax_category',
- 'ignore_pricing_rule', 'company_address', 'update_stock'):
- if not for_validate:
- self.set(fieldname, profile.get(fieldname))
+ for fieldname in (
+ "currency",
+ "letter_head",
+ "tc_name",
+ "company",
+ "select_print_heading",
+ "write_off_account",
+ "taxes_and_charges",
+ "write_off_cost_center",
+ "apply_discount_on",
+ "cost_center",
+ "tax_category",
+ "ignore_pricing_rule",
+ "company_address",
+ "update_stock",
+ ):
+ if not for_validate:
+ self.set(fieldname, profile.get(fieldname))
if self.customer:
customer_price_list, customer_group, customer_currency = frappe.db.get_value(
- "Customer", self.customer, ['default_price_list', 'customer_group', 'default_currency']
+ "Customer", self.customer, ["default_price_list", "customer_group", "default_currency"]
)
- customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list')
- selling_price_list = customer_price_list or customer_group_price_list or profile.get('selling_price_list')
- if customer_currency != profile.get('currency'):
- self.set('currency', customer_currency)
+ customer_group_price_list = frappe.db.get_value(
+ "Customer Group", customer_group, "default_price_list"
+ )
+ selling_price_list = (
+ customer_price_list or customer_group_price_list or profile.get("selling_price_list")
+ )
+ if customer_currency != profile.get("currency"):
+ self.set("currency", customer_currency)
else:
- selling_price_list = profile.get('selling_price_list')
+ selling_price_list = profile.get("selling_price_list")
if selling_price_list:
- self.set('selling_price_list', selling_price_list)
+ self.set("selling_price_list", selling_price_list)
# set pos values in items
for item in self.get("items"):
- if item.get('item_code'):
- profile_details = get_pos_profile_item_details(profile.get("company"), frappe._dict(item.as_dict()), profile)
+ if item.get("item_code"):
+ profile_details = get_pos_profile_item_details(
+ profile.get("company"), frappe._dict(item.as_dict()), profile
+ )
for fname, val in iteritems(profile_details):
if (not for_validate) or (for_validate and not item.get(fname)):
item.set(fname, val)
@@ -404,7 +523,9 @@ class POSInvoice(SalesInvoice):
self.set_taxes()
if not self.account_for_change_amount:
- self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
+ self.account_for_change_amount = frappe.get_cached_value(
+ "Company", self.company, "default_cash_account"
+ )
return profile
@@ -414,27 +535,29 @@ class POSInvoice(SalesInvoice):
if not self.debit_to:
self.debit_to = get_party_account("Customer", self.customer, self.company)
- self.party_account_currency = frappe.db.get_value("Account", self.debit_to, "account_currency", cache=True)
+ self.party_account_currency = frappe.db.get_value(
+ "Account", self.debit_to, "account_currency", cache=True
+ )
if not self.due_date and self.customer:
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
super(SalesInvoice, self).set_missing_values(for_validate)
print_format = profile.get("print_format") if profile else None
- if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')):
- print_format = 'POS Invoice'
+ if not print_format and not cint(frappe.db.get_value("Print Format", "POS Invoice", "disabled")):
+ print_format = "POS Invoice"
if profile:
return {
"print_format": print_format,
"campaign": profile.get("campaign"),
- "allow_print_before_pay": profile.get("allow_print_before_pay")
+ "allow_print_before_pay": profile.get("allow_print_before_pay"),
}
@frappe.whitelist()
def reset_mode_of_payments(self):
if self.pos_profile:
- pos_profile = frappe.get_cached_doc('POS Profile', self.pos_profile)
+ pos_profile = frappe.get_cached_doc("POS Profile", self.pos_profile)
update_multi_mode_option(self, pos_profile)
self.paid_amount = 0
@@ -456,6 +579,7 @@ class POSInvoice(SalesInvoice):
pay_req = self.get_existing_payment_request(pay)
if not pay_req:
pay_req = self.get_new_payment_request(pay)
+ pay_req.insert()
pay_req.submit()
else:
pay_req.request_phone_payment()
@@ -463,9 +587,13 @@ class POSInvoice(SalesInvoice):
return pay_req
def get_new_payment_request(self, mop):
- payment_gateway_account = frappe.db.get_value("Payment Gateway Account", {
- "payment_account": mop.account,
- }, ["name"])
+ payment_gateway_account = frappe.db.get_value(
+ "Payment Gateway Account",
+ {
+ "payment_account": mop.account,
+ },
+ ["name"],
+ )
args = {
"dt": "POS Invoice",
@@ -476,36 +604,41 @@ class POSInvoice(SalesInvoice):
"payment_request_type": "Inward",
"party_type": "Customer",
"party": self.customer,
- "return_doc": True
+ "return_doc": True,
}
return make_payment_request(**args)
def get_existing_payment_request(self, pay):
- payment_gateway_account = frappe.db.get_value("Payment Gateway Account", {
- "payment_account": pay.account,
- }, ["name"])
+ payment_gateway_account = frappe.db.get_value(
+ "Payment Gateway Account",
+ {
+ "payment_account": pay.account,
+ },
+ ["name"],
+ )
args = {
- 'doctype': 'Payment Request',
- 'reference_doctype': 'POS Invoice',
- 'reference_name': self.name,
- 'payment_gateway_account': payment_gateway_account,
- 'email_to': self.contact_mobile
+ "doctype": "Payment Request",
+ "reference_doctype": "POS Invoice",
+ "reference_name": self.name,
+ "payment_gateway_account": payment_gateway_account,
+ "email_to": self.contact_mobile,
}
pr = frappe.db.exists(args)
if pr:
- return frappe.get_doc('Payment Request', pr[0][0])
+ return frappe.get_doc("Payment Request", pr[0][0])
+
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
- if frappe.db.get_value('Item', item_code, 'is_stock_item'):
+ if frappe.db.get_value("Item", item_code, "is_stock_item"):
is_stock_item = True
bin_qty = get_bin_qty(item_code, warehouse)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty, is_stock_item
else:
is_stock_item = False
- if frappe.db.exists('Product Bundle', item_code):
+ if frappe.db.exists("Product Bundle", item_code):
return get_bundle_availability(item_code, warehouse), is_stock_item
else:
# Is a service item
@@ -513,7 +646,7 @@ def get_stock_availability(item_code, warehouse):
def get_bundle_availability(bundle_item_code, warehouse):
- product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)
+ product_bundle = frappe.get_doc("Product Bundle", bundle_item_code)
bundle_bin_qty = 1000000
for item in product_bundle.items:
@@ -528,30 +661,43 @@ def get_bundle_availability(bundle_item_code, warehouse):
pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse)
return bundle_bin_qty - pos_sales_qty
+
def get_bin_qty(item_code, warehouse):
- bin_qty = frappe.db.sql("""select actual_qty from `tabBin`
+ bin_qty = frappe.db.sql(
+ """select actual_qty from `tabBin`
where item_code = %s and warehouse = %s
- limit 1""", (item_code, warehouse), as_dict=1)
+ limit 1""",
+ (item_code, warehouse),
+ as_dict=1,
+ )
return bin_qty[0].actual_qty or 0 if bin_qty else 0
+
def get_pos_reserved_qty(item_code, warehouse):
- reserved_qty = frappe.db.sql("""select sum(p_item.qty) as qty
+ reserved_qty = frappe.db.sql(
+ """select sum(p_item.qty) as qty
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
where p.name = p_item.parent
and ifnull(p.consolidated_invoice, '') = ''
and p_item.docstatus = 1
and p_item.item_code = %s
and p_item.warehouse = %s
- """, (item_code, warehouse), as_dict=1)
+ """,
+ (item_code, warehouse),
+ as_dict=1,
+ )
return reserved_qty[0].qty or 0 if reserved_qty else 0
+
@frappe.whitelist()
def make_sales_return(source_name, target_doc=None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
+
return make_return_doc("POS Invoice", source_name, target_doc)
+
@frappe.whitelist()
def make_merge_log(invoices):
import json
@@ -562,35 +708,42 @@ def make_merge_log(invoices):
invoices = json.loads(invoices)
if len(invoices) == 0:
- frappe.throw(_('Atleast one invoice has to be selected.'))
+ frappe.throw(_("Atleast one invoice has to be selected."))
merge_log = frappe.new_doc("POS Invoice Merge Log")
merge_log.posting_date = getdate(nowdate())
for inv in invoices:
- inv_data = frappe.db.get_values("POS Invoice", inv.get('name'),
- ["customer", "posting_date", "grand_total"], as_dict=1)[0]
+ inv_data = frappe.db.get_values(
+ "POS Invoice", inv.get("name"), ["customer", "posting_date", "grand_total"], as_dict=1
+ )[0]
merge_log.customer = inv_data.customer
- merge_log.append("pos_invoices", {
- 'pos_invoice': inv.get('name'),
- 'customer': inv_data.customer,
- 'posting_date': inv_data.posting_date,
- 'grand_total': inv_data.grand_total
- })
+ merge_log.append(
+ "pos_invoices",
+ {
+ "pos_invoice": inv.get("name"),
+ "customer": inv_data.customer,
+ "posting_date": inv_data.posting_date,
+ "grand_total": inv_data.grand_total,
+ },
+ )
- if merge_log.get('pos_invoices'):
+ if merge_log.get("pos_invoices"):
return merge_log.as_dict()
+
def add_return_modes(doc, pos_profile):
def append_payment(payment_mode):
- payment = doc.append('payments', {})
+ payment = doc.append("payments", {})
payment.default = payment_mode.default
payment.mode_of_payment = payment_mode.parent
payment.account = payment_mode.default_account
payment.type = payment_mode.type
- for pos_payment_method in pos_profile.get('payments'):
+ for pos_payment_method in pos_profile.get("payments"):
pos_payment_method = pos_payment_method.as_dict()
mode_of_payment = pos_payment_method.mode_of_payment
- if pos_payment_method.allow_in_returns and not [d for d in doc.get('payments') if d.mode_of_payment == mode_of_payment]:
+ if pos_payment_method.allow_in_returns and not [
+ d for d in doc.get("payments") if d.mode_of_payment == mode_of_payment
+ ]:
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
append_payment(payment_mode[0])
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index cf8affdd010..70f128e0e39 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -35,29 +35,34 @@ class TestPOSInvoice(unittest.TestCase):
w2 = frappe.get_doc(w.doctype, w.name)
import time
+
time.sleep(1)
w.save()
import time
+
time.sleep(1)
self.assertRaises(frappe.TimestampMismatchError, w2.save)
def test_change_naming_series(self):
inv = create_pos_invoice(do_not_submit=1)
- inv.naming_series = 'TEST-'
+ inv.naming_series = "TEST-"
self.assertRaises(frappe.CannotChangeConstantError, inv.save)
def test_discount_and_inclusive_tax(self):
inv = create_pos_invoice(qty=100, rate=50, do_not_save=1)
- inv.append("taxes", {
- "charge_type": "On Net Total",
- "account_head": "_Test Account Service Tax - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Service Tax",
- "rate": 14,
- 'included_in_print_rate': 1
- })
+ inv.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 14,
+ "included_in_print_rate": 1,
+ },
+ )
inv.insert()
self.assertEqual(inv.net_total, 4385.96)
@@ -66,7 +71,7 @@ class TestPOSInvoice(unittest.TestCase):
inv.reload()
inv.discount_amount = 100
- inv.apply_discount_on = 'Net Total'
+ inv.apply_discount_on = "Net Total"
inv.payment_schedule = []
inv.save()
@@ -77,7 +82,7 @@ class TestPOSInvoice(unittest.TestCase):
inv.reload()
inv.discount_amount = 100
- inv.apply_discount_on = 'Grand Total'
+ inv.apply_discount_on = "Grand Total"
inv.payment_schedule = []
inv.save()
@@ -93,14 +98,17 @@ class TestPOSInvoice(unittest.TestCase):
item_row_copy.qty = qty
inv.append("items", item_row_copy)
- inv.append("taxes", {
- "account_head": "_Test Account VAT - _TC",
- "charge_type": "On Net Total",
- "cost_center": "_Test Cost Center - _TC",
- "description": "VAT",
- "doctype": "Sales Taxes and Charges",
- "rate": 19
- })
+ inv.append(
+ "taxes",
+ {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 19,
+ },
+ )
inv.insert()
self.assertEqual(inv.net_total, 4600)
@@ -115,10 +123,10 @@ class TestPOSInvoice(unittest.TestCase):
item_row = inv.get("items")[0]
add_items = [
- (54, '_Test Account Excise Duty @ 12 - _TC'),
- (288, '_Test Account Excise Duty @ 15 - _TC'),
- (144, '_Test Account Excise Duty @ 20 - _TC'),
- (430, '_Test Item Tax Template 1 - _TC')
+ (54, "_Test Account Excise Duty @ 12 - _TC"),
+ (288, "_Test Account Excise Duty @ 15 - _TC"),
+ (144, "_Test Account Excise Duty @ 20 - _TC"),
+ (430, "_Test Item Tax Template 1 - _TC"),
]
for qty, item_tax_template in add_items:
item_row_copy = copy.deepcopy(item_row)
@@ -126,30 +134,39 @@ class TestPOSInvoice(unittest.TestCase):
item_row_copy.item_tax_template = item_tax_template
inv.append("items", item_row_copy)
- inv.append("taxes", {
- "account_head": "_Test Account Excise Duty - _TC",
- "charge_type": "On Net Total",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Excise Duty",
- "doctype": "Sales Taxes and Charges",
- "rate": 11
- })
- inv.append("taxes", {
- "account_head": "_Test Account Education Cess - _TC",
- "charge_type": "On Net Total",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Education Cess",
- "doctype": "Sales Taxes and Charges",
- "rate": 0
- })
- inv.append("taxes", {
- "account_head": "_Test Account S&H Education Cess - _TC",
- "charge_type": "On Net Total",
- "cost_center": "_Test Cost Center - _TC",
- "description": "S&H Education Cess",
- "doctype": "Sales Taxes and Charges",
- "rate": 3
- })
+ inv.append(
+ "taxes",
+ {
+ "account_head": "_Test Account Excise Duty - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Excise Duty",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 11,
+ },
+ )
+ inv.append(
+ "taxes",
+ {
+ "account_head": "_Test Account Education Cess - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Education Cess",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 0,
+ },
+ )
+ inv.append(
+ "taxes",
+ {
+ "account_head": "_Test Account S&H Education Cess - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "S&H Education Cess",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 3,
+ },
+ )
inv.insert()
self.assertEqual(inv.net_total, 4600)
@@ -179,14 +196,17 @@ class TestPOSInvoice(unittest.TestCase):
inv.apply_discount_on = "Net Total"
inv.discount_amount = 75.0
- inv.append("taxes", {
- "account_head": "_Test Account VAT - _TC",
- "charge_type": "On Net Total",
- "cost_center": "_Test Cost Center - _TC",
- "description": "VAT",
- "doctype": "Sales Taxes and Charges",
- "rate": 24
- })
+ inv.append(
+ "taxes",
+ {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 24,
+ },
+ )
inv.insert()
self.assertEqual(inv.total, 975)
@@ -198,11 +218,15 @@ class TestPOSInvoice(unittest.TestCase):
self.assertEqual(inv.grand_total, 1116.0)
def test_pos_returns_with_repayment(self):
- pos = create_pos_invoice(qty = 10, do_not_save=True)
+ pos = create_pos_invoice(qty=10, do_not_save=True)
- pos.set('payments', [])
- pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500})
- pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500, 'default': 1})
+ pos.set("payments", [])
+ pos.append(
+ "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500}
+ )
+ pos.append(
+ "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500, "default": 1}
+ )
pos.insert()
pos.submit()
@@ -211,25 +235,39 @@ class TestPOSInvoice(unittest.TestCase):
pos_return.insert()
pos_return.submit()
- self.assertEqual(pos_return.get('payments')[0].amount, -500)
- self.assertEqual(pos_return.get('payments')[1].amount, -500)
+ self.assertEqual(pos_return.get("payments")[0].amount, -500)
+ self.assertEqual(pos_return.get("payments")[1].amount, -500)
def test_pos_return_for_serialized_item(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
- se = make_serialized_item(company='_Test Company',
- target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
+ se = make_serialized_item(
+ company="_Test Company",
+ target_warehouse="Stores - _TC",
+ cost_center="Main - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ )
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
- pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
- account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
- expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
- item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
+ pos = create_pos_invoice(
+ company="_Test Company",
+ debit_to="Debtors - _TC",
+ account_for_change_amount="Cash - _TC",
+ warehouse="Stores - _TC",
+ income_account="Sales - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ cost_center="Main - _TC",
+ item=se.get("items")[0].item_code,
+ rate=1000,
+ do_not_save=1,
+ )
pos.get("items")[0].serial_no = serial_nos[0]
- pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1})
+ pos.append(
+ "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
+ )
pos.insert()
pos.submit()
@@ -238,24 +276,39 @@ class TestPOSInvoice(unittest.TestCase):
pos_return.insert()
pos_return.submit()
- self.assertEqual(pos_return.get('items')[0].serial_no, serial_nos[0])
+ self.assertEqual(pos_return.get("items")[0].serial_no, serial_nos[0])
def test_partial_pos_returns(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
- se = make_serialized_item(company='_Test Company',
- target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
+ se = make_serialized_item(
+ company="_Test Company",
+ target_warehouse="Stores - _TC",
+ cost_center="Main - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ )
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
- pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
- account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
- expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
- item=se.get("items")[0].item_code, qty=2, rate=1000, do_not_save=1)
+ pos = create_pos_invoice(
+ company="_Test Company",
+ debit_to="Debtors - _TC",
+ account_for_change_amount="Cash - _TC",
+ warehouse="Stores - _TC",
+ income_account="Sales - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ cost_center="Main - _TC",
+ item=se.get("items")[0].item_code,
+ qty=2,
+ rate=1000,
+ do_not_save=1,
+ )
pos.get("items")[0].serial_no = serial_nos[0] + "\n" + serial_nos[1]
- pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1})
+ pos.append(
+ "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
+ )
pos.insert()
pos.submit()
@@ -263,24 +316,34 @@ class TestPOSInvoice(unittest.TestCase):
pos_return1 = make_sales_return(pos.name)
# partial return 1
- pos_return1.get('items')[0].qty = -1
- pos_return1.get('items')[0].serial_no = serial_nos[0]
+ pos_return1.get("items")[0].qty = -1
+ pos_return1.get("items")[0].serial_no = serial_nos[0]
pos_return1.insert()
pos_return1.submit()
# partial return 2
pos_return2 = make_sales_return(pos.name)
- self.assertEqual(pos_return2.get('items')[0].qty, -1)
- self.assertEqual(pos_return2.get('items')[0].serial_no, serial_nos[1])
+ self.assertEqual(pos_return2.get("items")[0].qty, -1)
+ self.assertEqual(pos_return2.get("items")[0].serial_no, serial_nos[1])
def test_pos_change_amount(self):
- pos = create_pos_invoice(company= "_Test Company", debit_to="Debtors - _TC",
- income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105,
- cost_center = "Main - _TC", do_not_save=True)
+ pos = create_pos_invoice(
+ company="_Test Company",
+ debit_to="Debtors - _TC",
+ income_account="Sales - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ rate=105,
+ cost_center="Main - _TC",
+ do_not_save=True,
+ )
- pos.set('payments', [])
- pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 50})
- pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60, 'default': 1})
+ pos.set("payments", [])
+ pos.append(
+ "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 50}
+ )
+ pos.append(
+ "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60, "default": 1}
+ )
pos.insert()
pos.submit()
@@ -298,29 +361,53 @@ class TestPOSInvoice(unittest.TestCase):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
- se = make_serialized_item(company='_Test Company',
- target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
+ se = make_serialized_item(
+ company="_Test Company",
+ target_warehouse="Stores - _TC",
+ cost_center="Main - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ )
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
- pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
- account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
- expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
- item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
+ pos = create_pos_invoice(
+ company="_Test Company",
+ debit_to="Debtors - _TC",
+ account_for_change_amount="Cash - _TC",
+ warehouse="Stores - _TC",
+ income_account="Sales - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ cost_center="Main - _TC",
+ item=se.get("items")[0].item_code,
+ rate=1000,
+ do_not_save=1,
+ )
pos.get("items")[0].serial_no = serial_nos[0]
- pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
+ pos.append(
+ "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
+ )
pos.insert()
pos.submit()
- pos2 = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
- account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
- expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
- item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
+ pos2 = create_pos_invoice(
+ company="_Test Company",
+ debit_to="Debtors - _TC",
+ account_for_change_amount="Cash - _TC",
+ warehouse="Stores - _TC",
+ income_account="Sales - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ cost_center="Main - _TC",
+ item=se.get("items")[0].item_code,
+ rate=1000,
+ do_not_save=1,
+ )
pos2.get("items")[0].serial_no = serial_nos[0]
- pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
+ pos2.append(
+ "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
+ )
pos2.insert()
self.assertRaises(frappe.ValidationError, pos2.submit)
@@ -329,27 +416,50 @@ class TestPOSInvoice(unittest.TestCase):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
- se = make_serialized_item(company='_Test Company',
- target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
+ se = make_serialized_item(
+ company="_Test Company",
+ target_warehouse="Stores - _TC",
+ cost_center="Main - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ )
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
- si = create_sales_invoice(company='_Test Company', debit_to='Debtors - _TC',
- account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
- expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
- item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
+ si = create_sales_invoice(
+ company="_Test Company",
+ debit_to="Debtors - _TC",
+ account_for_change_amount="Cash - _TC",
+ warehouse="Stores - _TC",
+ income_account="Sales - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ cost_center="Main - _TC",
+ item=se.get("items")[0].item_code,
+ rate=1000,
+ do_not_save=1,
+ )
si.get("items")[0].serial_no = serial_nos[0]
+ si.update_stock = 1
si.insert()
si.submit()
- pos2 = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
- account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
- expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
- item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
+ pos2 = create_pos_invoice(
+ company="_Test Company",
+ debit_to="Debtors - _TC",
+ account_for_change_amount="Cash - _TC",
+ warehouse="Stores - _TC",
+ income_account="Sales - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ cost_center="Main - _TC",
+ item=se.get("items")[0].item_code,
+ rate=1000,
+ do_not_save=1,
+ )
pos2.get("items")[0].serial_no = serial_nos[0]
- pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
+ pos2.append(
+ "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
+ )
pos2.insert()
self.assertRaises(frappe.ValidationError, pos2.submit)
@@ -357,17 +467,30 @@ class TestPOSInvoice(unittest.TestCase):
def test_invalid_serial_no_validation(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
- se = make_serialized_item(company='_Test Company',
- target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
- serial_nos = se.get("items")[0].serial_no + 'wrong'
+ se = make_serialized_item(
+ company="_Test Company",
+ target_warehouse="Stores - _TC",
+ cost_center="Main - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ )
+ serial_nos = se.get("items")[0].serial_no + "wrong"
- pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
- account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
- expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
- item=se.get("items")[0].item_code, rate=1000, qty=2, do_not_save=1)
+ pos = create_pos_invoice(
+ company="_Test Company",
+ debit_to="Debtors - _TC",
+ account_for_change_amount="Cash - _TC",
+ warehouse="Stores - _TC",
+ income_account="Sales - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ cost_center="Main - _TC",
+ item=se.get("items")[0].item_code,
+ rate=1000,
+ qty=2,
+ do_not_save=1,
+ )
- pos.get('items')[0].has_serial_no = 1
- pos.get('items')[0].serial_no = serial_nos
+ pos.get("items")[0].has_serial_no = 1
+ pos.get("items")[0].serial_no = serial_nos
pos.insert()
self.assertRaises(frappe.ValidationError, pos.submit)
@@ -379,20 +502,31 @@ class TestPOSInvoice(unittest.TestCase):
from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records
create_records()
- frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
- before_lp_details = get_loyalty_program_details_with_points("Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty")
+ frappe.db.set_value(
+ "Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
+ )
+ before_lp_details = get_loyalty_program_details_with_points(
+ "Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty"
+ )
inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000)
- lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'POS Invoice', 'invoice': inv.name, 'customer': inv.customer})
- after_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program)
+ lpe = frappe.get_doc(
+ "Loyalty Point Entry",
+ {"invoice_type": "POS Invoice", "invoice": inv.name, "customer": inv.customer},
+ )
+ after_lp_details = get_loyalty_program_details_with_points(
+ inv.customer, company=inv.company, loyalty_program=inv.loyalty_program
+ )
- self.assertEqual(inv.get('loyalty_program'), "Test Single Loyalty")
+ self.assertEqual(inv.get("loyalty_program"), "Test Single Loyalty")
self.assertEqual(lpe.loyalty_points, 10)
self.assertEqual(after_lp_details.loyalty_points, before_lp_details.loyalty_points + 10)
inv.cancel()
- after_cancel_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program)
+ after_cancel_lp_details = get_loyalty_program_details_with_points(
+ inv.customer, company=inv.company, loyalty_program=inv.loyalty_program
+ )
self.assertEqual(after_cancel_lp_details.loyalty_points, before_lp_details.loyalty_points)
def test_loyalty_points_redeemption(self):
@@ -403,17 +537,24 @@ class TestPOSInvoice(unittest.TestCase):
# add 10 loyalty points
create_pos_invoice(customer="Test Loyalty Customer", rate=10000)
- before_lp_details = get_loyalty_program_details_with_points("Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty")
+ before_lp_details = get_loyalty_program_details_with_points(
+ "Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty"
+ )
inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
inv.redeem_loyalty_points = 1
inv.loyalty_points = before_lp_details.loyalty_points
inv.loyalty_amount = inv.loyalty_points * before_lp_details.conversion_factor
- inv.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 10000 - inv.loyalty_amount})
+ inv.append(
+ "payments",
+ {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000 - inv.loyalty_amount},
+ )
inv.paid_amount = 10000
inv.submit()
- after_redeem_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program)
+ after_redeem_lp_details = get_loyalty_program_details_with_points(
+ inv.customer, company=inv.company, loyalty_program=inv.loyalty_program
+ )
self.assertEqual(after_redeem_lp_details.loyalty_points, 9)
def test_merging_into_sales_invoice_with_discount(self):
@@ -427,21 +568,19 @@ class TestPOSInvoice(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
- pos_inv.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 270
- })
+ pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 270})
pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
- pos_inv2.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
- })
+ pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv2.submit()
consolidate_pos_invoices()
pos_inv.load_from_db()
- rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
+ rounded_total = frappe.db.get_value(
+ "Sales Invoice", pos_inv.consolidated_invoice, "rounded_total"
+ )
self.assertEqual(rounded_total, 3470)
def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self):
@@ -455,38 +594,42 @@ class TestPOSInvoice(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
- pos_inv.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
- })
- pos_inv.append('taxes', {
- "charge_type": "On Net Total",
- "account_head": "_Test Account Service Tax - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Service Tax",
- "rate": 14,
- 'included_in_print_rate': 1
- })
+ pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
+ pos_inv.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 14,
+ "included_in_print_rate": 1,
+ },
+ )
pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1)
pos_inv2.additional_discount_percentage = 10
- pos_inv2.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 540
- })
- pos_inv2.append('taxes', {
- "charge_type": "On Net Total",
- "account_head": "_Test Account Service Tax - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Service Tax",
- "rate": 14,
- 'included_in_print_rate': 1
- })
+ pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 540})
+ pos_inv2.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 14,
+ "included_in_print_rate": 1,
+ },
+ )
pos_inv2.submit()
consolidate_pos_invoices()
pos_inv.load_from_db()
- rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
+ rounded_total = frappe.db.get_value(
+ "Sales Invoice", pos_inv.consolidated_invoice, "rounded_total"
+ )
self.assertEqual(rounded_total, 840)
def test_merging_with_validate_selling_price(self):
@@ -506,64 +649,75 @@ class TestPOSInvoice(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1)
- pos_inv.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
- })
- pos_inv.append('taxes', {
- "charge_type": "On Net Total",
- "account_head": "_Test Account Service Tax - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Service Tax",
- "rate": 14,
- 'included_in_print_rate': 1
- })
+ pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
+ pos_inv.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 14,
+ "included_in_print_rate": 1,
+ },
+ )
self.assertRaises(frappe.ValidationError, pos_inv.submit)
pos_inv2 = create_pos_invoice(item=item, rate=400, do_not_submit=1)
- pos_inv2.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 400
- })
- pos_inv2.append('taxes', {
- "charge_type": "On Net Total",
- "account_head": "_Test Account Service Tax - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Service Tax",
- "rate": 14,
- 'included_in_print_rate': 1
- })
+ pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 400})
+ pos_inv2.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 14,
+ "included_in_print_rate": 1,
+ },
+ )
pos_inv2.submit()
consolidate_pos_invoices()
pos_inv2.load_from_db()
- rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
+ rounded_total = frappe.db.get_value(
+ "Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total"
+ )
self.assertEqual(rounded_total, 400)
def test_pos_batch_item_qty_validation(self):
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_batch_item_with_batch,
)
- create_batch_item_with_batch('_BATCH ITEM', 'TestBatch 01')
- item = frappe.get_doc('Item', '_BATCH ITEM')
- batch = frappe.get_doc('Batch', 'TestBatch 01')
+
+ create_batch_item_with_batch("_BATCH ITEM", "TestBatch 01")
+ item = frappe.get_doc("Item", "_BATCH ITEM")
+ batch = frappe.get_doc("Batch", "TestBatch 01")
batch.submit()
- item.batch_no = 'TestBatch 01'
+ item.batch_no = "TestBatch 01"
item.save()
- se = make_stock_entry(target="_Test Warehouse - _TC", item_code="_BATCH ITEM", qty=2, basic_rate=100, batch_no='TestBatch 01')
+ se = make_stock_entry(
+ target="_Test Warehouse - _TC",
+ item_code="_BATCH ITEM",
+ qty=2,
+ basic_rate=100,
+ batch_no="TestBatch 01",
+ )
pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1)
- pos_inv1.items[0].batch_no = 'TestBatch 01'
+ pos_inv1.items[0].batch_no = "TestBatch 01"
pos_inv1.save()
pos_inv1.submit()
pos_inv2 = create_pos_invoice(item=item.name, rate=300, qty=2, do_not_submit=1)
- pos_inv2.items[0].batch_no = 'TestBatch 01'
+ pos_inv2.items[0].batch_no = "TestBatch 01"
pos_inv2.save()
self.assertRaises(frappe.ValidationError, pos_inv2.submit)
- #teardown
+ # teardown
pos_inv1.reload()
pos_inv1.cancel()
pos_inv1.delete()
@@ -577,12 +731,14 @@ class TestPOSInvoice(unittest.TestCase):
def test_ignore_pricing_rule(self):
from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule
- item_price = frappe.get_doc({
- 'doctype': 'Item Price',
- 'item_code': '_Test Item',
- 'price_list': '_Test Price List',
- 'price_list_rate': '450',
- })
+ item_price = frappe.get_doc(
+ {
+ "doctype": "Item Price",
+ "item_code": "_Test Item",
+ "price_list": "_Test Price List",
+ "price_list_rate": "450",
+ }
+ )
item_price.insert()
pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10)
pr.save()
@@ -610,6 +766,76 @@ class TestPOSInvoice(unittest.TestCase):
pos_inv.delete()
pr.delete()
+ def test_delivered_serial_no_case(self):
+ from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
+ init_user_and_profile,
+ )
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+ from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+
+ frappe.db.savepoint("before_test_delivered_serial_no_case")
+ try:
+ se = make_serialized_item()
+ serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
+
+ dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no)
+
+ delivery_document_no = frappe.db.get_value("Serial No", serial_no, "delivery_document_no")
+ self.assertEquals(delivery_document_no, dn.name)
+
+ init_user_and_profile()
+
+ pos_inv = create_pos_invoice(
+ item_code="_Test Serialized Item With Series",
+ serial_no=serial_no,
+ qty=1,
+ rate=100,
+ do_not_submit=True,
+ )
+
+ self.assertRaises(frappe.ValidationError, pos_inv.submit)
+
+ finally:
+ frappe.db.rollback(save_point="before_test_delivered_serial_no_case")
+ frappe.set_user("Administrator")
+
+ def test_returned_serial_no_case(self):
+ from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
+ init_user_and_profile,
+ )
+ from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos
+ from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+
+ frappe.db.savepoint("before_test_returned_serial_no_case")
+ try:
+ se = make_serialized_item()
+ serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
+
+ init_user_and_profile()
+
+ pos_inv = create_pos_invoice(
+ item_code="_Test Serialized Item With Series",
+ serial_no=serial_no,
+ qty=1,
+ rate=100,
+ )
+
+ pos_return = make_sales_return(pos_inv.name)
+ pos_return.flags.ignore_validate = True
+ pos_return.insert()
+ pos_return.submit()
+
+ pos_reserved_serial_nos = get_pos_reserved_serial_nos(
+ {"item_code": "_Test Serialized Item With Series", "warehouse": "_Test Warehouse - _TC"}
+ )
+ self.assertTrue(serial_no not in pos_reserved_serial_nos)
+
+ finally:
+ frappe.db.rollback(save_point="before_test_returned_serial_no_case")
+ frappe.set_user("Administrator")
+
def create_pos_invoice(**args):
args = frappe._dict(args)
@@ -633,23 +859,26 @@ def create_pos_invoice(**args):
pos_inv.debit_to = args.debit_to or "Debtors - _TC"
pos_inv.is_return = args.is_return
pos_inv.return_against = args.return_against
- pos_inv.currency=args.currency or "INR"
+ pos_inv.currency = args.currency or "INR"
pos_inv.conversion_rate = args.conversion_rate or 1
pos_inv.account_for_change_amount = args.account_for_change_amount or "Cash - _TC"
pos_inv.set_missing_values()
- pos_inv.append("items", {
- "item_code": args.item or args.item_code or "_Test Item",
- "warehouse": args.warehouse or "_Test Warehouse - _TC",
- "qty": args.qty or 1,
- "rate": args.rate if args.get("rate") is not None else 100,
- "income_account": args.income_account or "Sales - _TC",
- "expense_account": args.expense_account or "Cost of Goods Sold - _TC",
- "cost_center": args.cost_center or "_Test Cost Center - _TC",
- "serial_no": args.serial_no,
- "batch_no": args.batch_no
- })
+ pos_inv.append(
+ "items",
+ {
+ "item_code": args.item or args.item_code or "_Test Item",
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "qty": args.qty or 1,
+ "rate": args.rate if args.get("rate") is not None else 100,
+ "income_account": args.income_account or "Sales - _TC",
+ "expense_account": args.expense_account or "Cost of Goods Sold - _TC",
+ "cost_center": args.cost_center or "_Test Cost Center - _TC",
+ "serial_no": args.serial_no,
+ "batch_no": args.batch_no,
+ },
+ )
if not args.do_not_save:
pos_inv.insert()
@@ -662,7 +891,9 @@ def create_pos_invoice(**args):
return pos_inv
+
def make_batch_item(item_name):
from erpnext.stock.doctype.item.test_item import make_item
+
if not frappe.db.exists(item_name):
- return make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1, is_stock_item=1))
\ No newline at end of file
+ return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1))
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index 40ab0c50deb..d3a81fe61dc 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -5,7 +5,6 @@
import json
import frappe
-import six
from frappe import _
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.model.document import Document
@@ -21,32 +20,44 @@ class POSInvoiceMergeLog(Document):
self.validate_pos_invoice_status()
def validate_customer(self):
- if self.merge_invoices_based_on == 'Customer Group':
+ if self.merge_invoices_based_on == "Customer Group":
return
for d in self.pos_invoices:
if d.customer != self.customer:
- frappe.throw(_("Row #{}: POS Invoice {} is not against customer {}").format(d.idx, d.pos_invoice, self.customer))
+ frappe.throw(
+ _("Row #{}: POS Invoice {} is not against customer {}").format(
+ d.idx, d.pos_invoice, self.customer
+ )
+ )
def validate_pos_invoice_status(self):
for d in self.pos_invoices:
status, docstatus, is_return, return_against = frappe.db.get_value(
- 'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against'])
+ "POS Invoice", d.pos_invoice, ["status", "docstatus", "is_return", "return_against"]
+ )
bold_pos_invoice = frappe.bold(d.pos_invoice)
bold_status = frappe.bold(status)
if docstatus != 1:
frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, bold_pos_invoice))
if status == "Consolidated":
- frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, bold_pos_invoice, bold_status))
- if is_return and return_against and return_against not in [d.pos_invoice for d in self.pos_invoices]:
+ frappe.throw(
+ _("Row #{}: POS Invoice {} has been {}").format(d.idx, bold_pos_invoice, bold_status)
+ )
+ if (
+ is_return
+ and return_against
+ and return_against not in [d.pos_invoice for d in self.pos_invoices]
+ ):
bold_return_against = frappe.bold(return_against)
- return_against_status = frappe.db.get_value('POS Invoice', return_against, "status")
+ return_against_status = frappe.db.get_value("POS Invoice", return_against, "status")
if return_against_status != "Consolidated":
# if return entry is not getting merged in the current pos closing and if it is not consolidated
bold_unconsolidated = frappe.bold("not Consolidated")
- msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}.")
- .format(d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated))
+ msg = _("Row #{}: Original Invoice {} of return invoice {} is {}.").format(
+ d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated
+ )
msg += " "
msg += _("Original invoice should be consolidated before or along with the return invoice.")
msg += "
"
@@ -54,10 +65,12 @@ class POSInvoiceMergeLog(Document):
frappe.throw(msg)
def on_submit(self):
- pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
+ pos_invoice_docs = [
+ frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices
+ ]
- returns = [d for d in pos_invoice_docs if d.get('is_return') == 1]
- sales = [d for d in pos_invoice_docs if d.get('is_return') == 0]
+ returns = [d for d in pos_invoice_docs if d.get("is_return") == 1]
+ sales = [d for d in pos_invoice_docs if d.get("is_return") == 0]
sales_invoice, credit_note = "", ""
if returns:
@@ -66,12 +79,14 @@ class POSInvoiceMergeLog(Document):
if sales:
sales_invoice = self.process_merging_into_sales_invoice(sales)
- self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
+ self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
def on_cancel(self):
- pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
+ pos_invoice_docs = [
+ frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices
+ ]
self.update_pos_invoices(pos_invoice_docs)
self.cancel_linked_invoices()
@@ -85,20 +100,12 @@ class POSInvoiceMergeLog(Document):
sales_invoice.set_posting_time = 1
sales_invoice.posting_date = getdate(self.posting_date)
sales_invoice.save()
- self.write_off_fractional_amount(sales_invoice, data)
sales_invoice.submit()
self.consolidated_invoice = sales_invoice.name
return sales_invoice.name
- def write_off_fractional_amount(self, invoice, data):
- pos_invoice_grand_total = sum(d.grand_total for d in data)
-
- if abs(pos_invoice_grand_total - invoice.grand_total) < 1:
- invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total)
- invoice.save()
-
def process_merging_into_credit_note(self, data):
credit_note = self.get_new_sales_invoice()
credit_note.is_return = 1
@@ -111,7 +118,6 @@ class POSInvoiceMergeLog(Document):
# TODO: return could be against multiple sales invoice which could also have been consolidated?
# credit_note.return_against = self.consolidated_invoice
credit_note.save()
- self.write_off_fractional_amount(credit_note, data)
credit_note.submit()
self.consolidated_credit_note = credit_note.name
@@ -128,9 +134,8 @@ class POSInvoiceMergeLog(Document):
loyalty_amount_sum, loyalty_points_sum, idx = 0, 0, 1
-
for doc in data:
- map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
+ map_doc(doc, invoice, table_map={"doctype": invoice.doctype})
if doc.redeem_loyalty_points:
invoice.loyalty_redemption_account = doc.loyalty_redemption_account
@@ -138,11 +143,17 @@ class POSInvoiceMergeLog(Document):
loyalty_points_sum += doc.loyalty_points
loyalty_amount_sum += doc.loyalty_amount
- for item in doc.get('items'):
+ for item in doc.get("items"):
found = False
for i in items:
- if (i.item_code == item.item_code and not i.serial_no and not i.batch_no and
- i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse):
+ if (
+ i.item_code == item.item_code
+ and not i.serial_no
+ and not i.batch_no
+ and i.uom == item.uom
+ and i.net_rate == item.net_rate
+ and i.warehouse == item.warehouse
+ ):
found = True
i.qty = i.qty + item.qty
i.amount = i.amount + item.net_amount
@@ -158,7 +169,7 @@ class POSInvoiceMergeLog(Document):
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
items.append(si_item)
- for tax in doc.get('taxes'):
+ for tax in doc.get("taxes"):
found = False
for t in taxes:
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
@@ -167,7 +178,7 @@ class POSInvoiceMergeLog(Document):
update_item_wise_tax_detail(t, tax)
found = True
if not found:
- tax.charge_type = 'Actual'
+ tax.charge_type = "Actual"
tax.idx = idx
idx += 1
tax.included_in_print_rate = 0
@@ -176,7 +187,7 @@ class POSInvoiceMergeLog(Document):
tax.item_wise_tax_detail = tax.item_wise_tax_detail
taxes.append(tax)
- for payment in doc.get('payments'):
+ for payment in doc.get("payments"):
found = False
for pay in payments:
if pay.account == payment.account and pay.mode_of_payment == payment.mode_of_payment:
@@ -191,52 +202,59 @@ class POSInvoiceMergeLog(Document):
base_rounding_adjustment += doc.base_rounding_adjustment
base_rounded_total += doc.base_rounded_total
-
if loyalty_points_sum:
invoice.redeem_loyalty_points = 1
invoice.loyalty_points = loyalty_points_sum
invoice.loyalty_amount = loyalty_amount_sum
- invoice.set('items', items)
- invoice.set('payments', payments)
- invoice.set('taxes', taxes)
- invoice.set('rounding_adjustment',rounding_adjustment)
- invoice.set('base_rounding_adjustment',base_rounding_adjustment)
- invoice.set('rounded_total',rounded_total)
- invoice.set('base_rounded_total',base_rounded_total)
+ invoice.set("items", items)
+ invoice.set("payments", payments)
+ invoice.set("taxes", taxes)
+ invoice.set("rounding_adjustment", rounding_adjustment)
+ invoice.set("base_rounding_adjustment", base_rounding_adjustment)
+ invoice.set("rounded_total", rounded_total)
+ invoice.set("base_rounded_total", base_rounded_total)
invoice.additional_discount_percentage = 0
invoice.discount_amount = 0.0
invoice.taxes_and_charges = None
invoice.ignore_pricing_rule = 1
invoice.customer = self.customer
- if self.merge_invoices_based_on == 'Customer Group':
+ if self.merge_invoices_based_on == "Customer Group":
invoice.flags.ignore_pos_profile = True
- invoice.pos_profile = ''
+ invoice.pos_profile = ""
return invoice
def get_new_sales_invoice(self):
- sales_invoice = frappe.new_doc('Sales Invoice')
+ sales_invoice = frappe.new_doc("Sales Invoice")
sales_invoice.customer = self.customer
sales_invoice.is_pos = 1
return sales_invoice
- def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''):
+ def update_pos_invoices(self, invoice_docs, sales_invoice="", credit_note=""):
for doc in invoice_docs:
doc.load_from_db()
- doc.update({ 'consolidated_invoice': None if self.docstatus==2 else (credit_note if doc.is_return else sales_invoice) })
+ doc.update(
+ {
+ "consolidated_invoice": None
+ if self.docstatus == 2
+ else (credit_note if doc.is_return else sales_invoice)
+ }
+ )
doc.set_status(update=True)
doc.save()
def cancel_linked_invoices(self):
for si_name in [self.consolidated_invoice, self.consolidated_credit_note]:
- if not si_name: continue
- si = frappe.get_doc('Sales Invoice', si_name)
+ if not si_name:
+ continue
+ si = frappe.get_doc("Sales Invoice", si_name)
si.flags.ignore_validate = True
si.cancel()
+
def update_item_wise_tax_detail(consolidate_tax_row, tax_row):
consolidated_tax_detail = json.loads(consolidate_tax_row.item_wise_tax_detail)
tax_row_detail = json.loads(tax_row.item_wise_tax_detail)
@@ -247,78 +265,150 @@ def update_item_wise_tax_detail(consolidate_tax_row, tax_row):
for item_code, tax_data in tax_row_detail.items():
if consolidated_tax_detail.get(item_code):
consolidated_tax_data = consolidated_tax_detail.get(item_code)
- consolidated_tax_detail.update({
- item_code: [consolidated_tax_data[0], consolidated_tax_data[1] + tax_data[1]]
- })
+ consolidated_tax_detail.update(
+ {item_code: [consolidated_tax_data[0], consolidated_tax_data[1] + tax_data[1]]}
+ )
else:
- consolidated_tax_detail.update({
- item_code: [tax_data[0], tax_data[1]]
- })
+ consolidated_tax_detail.update({item_code: [tax_data[0], tax_data[1]]})
+
+ consolidate_tax_row.item_wise_tax_detail = json.dumps(
+ consolidated_tax_detail, separators=(",", ":")
+ )
- consolidate_tax_row.item_wise_tax_detail = json.dumps(consolidated_tax_detail, separators=(',', ':'))
def get_all_unconsolidated_invoices():
filters = {
- 'consolidated_invoice': [ 'in', [ '', None ]],
- 'status': ['not in', ['Consolidated']],
- 'docstatus': 1
+ "consolidated_invoice": ["in", ["", None]],
+ "status": ["not in", ["Consolidated"]],
+ "docstatus": 1,
}
- pos_invoices = frappe.db.get_all('POS Invoice', filters=filters,
- fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer'])
+ pos_invoices = frappe.db.get_all(
+ "POS Invoice",
+ filters=filters,
+ fields=[
+ "name as pos_invoice",
+ "posting_date",
+ "grand_total",
+ "customer",
+ "is_return",
+ "return_against",
+ ],
+ )
return pos_invoices
+
def get_invoice_customer_map(pos_invoices):
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Customer 2' : [{}] }
pos_invoice_customer_map = {}
for invoice in pos_invoices:
- customer = invoice.get('customer')
+ customer = invoice.get("customer")
pos_invoice_customer_map.setdefault(customer, [])
pos_invoice_customer_map[customer].append(invoice)
return pos_invoice_customer_map
+
def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
- invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions'))
+ invoices = pos_invoices or (closing_entry and closing_entry.get("pos_transactions"))
if frappe.flags.in_test and not invoices:
invoices = get_all_unconsolidated_invoices()
invoice_by_customer = get_invoice_customer_map(invoices)
if len(invoices) >= 10 and closing_entry:
- closing_entry.set_status(update=True, status='Queued')
- enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry)
+ closing_entry.set_status(update=True, status="Queued")
+ enqueue_job(
+ create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry
+ )
else:
create_merge_logs(invoice_by_customer, closing_entry)
+
def unconsolidate_pos_invoices(closing_entry):
merge_logs = frappe.get_all(
- 'POS Invoice Merge Log',
- filters={ 'pos_closing_entry': closing_entry.name },
- pluck='name'
+ "POS Invoice Merge Log", filters={"pos_closing_entry": closing_entry.name}, pluck="name"
)
if len(merge_logs) >= 10:
- closing_entry.set_status(update=True, status='Queued')
+ closing_entry.set_status(update=True, status="Queued")
enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry)
else:
cancel_merge_logs(merge_logs, closing_entry)
+
+def split_invoices(invoices):
+ """
+ Splits invoices into multiple groups
+ Use-case:
+ If a serial no is sold and later it is returned
+ then split the invoices such that the selling entry is merged first and then the return entry
+ """
+ # Input
+ # invoices = [
+ # {'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0},
+ # {'pos_invoice': 'Invoice with SR#1', 'is_return': 1},
+ # {'pos_invoice': 'Invoice with SR#2', 'is_return': 0}
+ # ]
+ # Output
+ # _invoices = [
+ # [{'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0}],
+ # [{'pos_invoice': 'Invoice with SR#1', 'is_return': 1}, {'pos_invoice': 'Invoice with SR#2', 'is_return': 0}],
+ # ]
+
+ _invoices = []
+ special_invoices = []
+ pos_return_docs = [
+ frappe.get_cached_doc("POS Invoice", d.pos_invoice)
+ for d in invoices
+ if d.is_return and d.return_against
+ ]
+ for pos_invoice in pos_return_docs:
+ for item in pos_invoice.items:
+ if not item.serial_no:
+ continue
+
+ return_against_is_added = any(
+ d for d in _invoices if d.pos_invoice == pos_invoice.return_against
+ )
+ if return_against_is_added:
+ break
+
+ return_against_is_consolidated = (
+ frappe.db.get_value("POS Invoice", pos_invoice.return_against, "status", cache=True)
+ == "Consolidated"
+ )
+ if return_against_is_consolidated:
+ break
+
+ pos_invoice_row = [d for d in invoices if d.pos_invoice == pos_invoice.return_against]
+ _invoices.append(pos_invoice_row)
+ special_invoices.append(pos_invoice.return_against)
+ break
+
+ _invoices.append([d for d in invoices if d.pos_invoice not in special_invoices])
+
+ return _invoices
+
+
def create_merge_logs(invoice_by_customer, closing_entry=None):
try:
- for customer, invoices in six.iteritems(invoice_by_customer):
- merge_log = frappe.new_doc('POS Invoice Merge Log')
- merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate()
- merge_log.customer = customer
- merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None
+ for customer, invoices in invoice_by_customer.items():
+ for _invoices in split_invoices(invoices):
+ merge_log = frappe.new_doc("POS Invoice Merge Log")
+ merge_log.posting_date = (
+ getdate(closing_entry.get("posting_date")) if closing_entry else nowdate()
+ )
+ merge_log.customer = customer
+ merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
- merge_log.set('pos_invoices', invoices)
- merge_log.save(ignore_permissions=True)
- merge_log.submit()
+ merge_log.set("pos_invoices", _invoices)
+ merge_log.save(ignore_permissions=True)
+ merge_log.submit()
if closing_entry:
- closing_entry.set_status(update=True, status='Submitted')
- closing_entry.db_set('error_message', '')
+ closing_entry.set_status(update=True, status="Submitted")
+ closing_entry.db_set("error_message", "")
closing_entry.update_opening_entry()
except Exception as e:
@@ -327,24 +417,25 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
error_message = safe_load_json(message_log)
if closing_entry:
- closing_entry.set_status(update=True, status='Failed')
- closing_entry.db_set('error_message', error_message)
+ closing_entry.set_status(update=True, status="Failed")
+ closing_entry.db_set("error_message", error_message)
raise
finally:
frappe.db.commit()
- frappe.publish_realtime('closing_process_complete', {'user': frappe.session.user})
+ frappe.publish_realtime("closing_process_complete", {"user": frappe.session.user})
+
def cancel_merge_logs(merge_logs, closing_entry=None):
try:
for log in merge_logs:
- merge_log = frappe.get_doc('POS Invoice Merge Log', log)
+ merge_log = frappe.get_doc("POS Invoice Merge Log", log)
merge_log.flags.ignore_permissions = True
merge_log.cancel()
if closing_entry:
- closing_entry.set_status(update=True, status='Cancelled')
- closing_entry.db_set('error_message', '')
+ closing_entry.set_status(update=True, status="Cancelled")
+ closing_entry.db_set("error_message", "")
closing_entry.update_opening_entry(for_cancel=True)
except Exception as e:
@@ -353,18 +444,19 @@ def cancel_merge_logs(merge_logs, closing_entry=None):
error_message = safe_load_json(message_log)
if closing_entry:
- closing_entry.set_status(update=True, status='Submitted')
- closing_entry.db_set('error_message', error_message)
+ closing_entry.set_status(update=True, status="Submitted")
+ closing_entry.db_set("error_message", error_message)
raise
finally:
frappe.db.commit()
- frappe.publish_realtime('closing_process_complete', {'user': frappe.session.user})
+ frappe.publish_realtime("closing_process_complete", {"user": frappe.session.user})
+
def enqueue_job(job, **kwargs):
check_scheduler_status()
- closing_entry = kwargs.get('closing_entry') or {}
+ closing_entry = kwargs.get("closing_entry") or {}
job_name = closing_entry.get("name")
if not job_already_enqueued(job_name):
@@ -379,24 +471,27 @@ def enqueue_job(job, **kwargs):
)
if job == create_merge_logs:
- msg = _('POS Invoices will be consolidated in a background process')
+ msg = _("POS Invoices will be consolidated in a background process")
else:
- msg = _('POS Invoices will be unconsolidated in a background process')
+ msg = _("POS Invoices will be unconsolidated in a background process")
frappe.msgprint(msg, alert=1)
+
def check_scheduler_status():
if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
+
def job_already_enqueued(job_name):
enqueued_jobs = [d.get("job_name") for d in get_info()]
if job_name in enqueued_jobs:
return True
+
def safe_load_json(message):
try:
- json_message = json.loads(message).get('message')
+ json_message = json.loads(message).get("message")
except Exception:
json_message = message
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
index 5930aa097f7..9e696f18b6a 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
@@ -5,6 +5,7 @@ import json
import unittest
import frappe
+from frappe.tests.utils import change_settings
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
@@ -23,21 +24,19 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
- pos_inv.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
- })
+ pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
- pos_inv2.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
- })
+ pos_inv2.append(
+ "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}
+ )
pos_inv2.submit()
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
- pos_inv3.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300
- })
+ pos_inv3.append(
+ "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300}
+ )
pos_inv3.submit()
consolidate_pos_invoices()
@@ -62,28 +61,29 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
- pos_inv.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
- })
+ pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
- pos_inv2.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
- })
+ pos_inv2.append(
+ "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}
+ )
pos_inv2.submit()
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
- pos_inv3.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300
- })
+ pos_inv3.append(
+ "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300}
+ )
pos_inv3.submit()
pos_inv_cn = make_sales_return(pos_inv.name)
pos_inv_cn.set("payments", [])
- pos_inv_cn.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300
- })
+ pos_inv_cn.append(
+ "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -100}
+ )
+ pos_inv_cn.append(
+ "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": -200}
+ )
pos_inv_cn.paid_amount = -300
pos_inv_cn.submit()
@@ -97,7 +97,12 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
pos_inv_cn.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice))
- self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return"))
+ consolidated_credit_note = frappe.get_doc("Sales Invoice", pos_inv_cn.consolidated_invoice)
+ self.assertEqual(consolidated_credit_note.is_return, 1)
+ self.assertEqual(consolidated_credit_note.payments[0].mode_of_payment, "Cash")
+ self.assertEqual(consolidated_credit_note.payments[0].amount, -100)
+ self.assertEqual(consolidated_credit_note.payments[1].mode_of_payment, "Bank Draft")
+ self.assertEqual(consolidated_credit_note.payments[1].amount, -200)
finally:
frappe.set_user("Administrator")
@@ -110,41 +115,47 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
try:
inv = create_pos_invoice(qty=1, rate=100, do_not_save=True)
- inv.append("taxes", {
- "account_head": "_Test Account VAT - _TC",
- "charge_type": "On Net Total",
- "cost_center": "_Test Cost Center - _TC",
- "description": "VAT",
- "doctype": "Sales Taxes and Charges",
- "rate": 9
- })
+ inv.append(
+ "taxes",
+ {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 9,
+ },
+ )
inv.insert()
inv.submit()
inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True)
- inv2.get('items')[0].item_code = '_Test Item 2'
- inv2.append("taxes", {
- "account_head": "_Test Account VAT - _TC",
- "charge_type": "On Net Total",
- "cost_center": "_Test Cost Center - _TC",
- "description": "VAT",
- "doctype": "Sales Taxes and Charges",
- "rate": 5
- })
+ inv2.get("items")[0].item_code = "_Test Item 2"
+ inv2.append(
+ "taxes",
+ {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 5,
+ },
+ )
inv2.insert()
inv2.submit()
consolidate_pos_invoices()
inv.load_from_db()
- consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
- item_wise_tax_detail = json.loads(consolidated_invoice.get('taxes')[0].item_wise_tax_detail)
+ consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
+ item_wise_tax_detail = json.loads(consolidated_invoice.get("taxes")[0].item_wise_tax_detail)
- tax_rate, amount = item_wise_tax_detail.get('_Test Item')
+ tax_rate, amount = item_wise_tax_detail.get("_Test Item")
self.assertEqual(tax_rate, 9)
self.assertEqual(amount, 9)
- tax_rate2, amount2 = item_wise_tax_detail.get('_Test Item 2')
+ tax_rate2, amount2 = item_wise_tax_detail.get("_Test Item 2")
self.assertEqual(tax_rate2, 5)
self.assertEqual(amount2, 5)
finally:
@@ -152,11 +163,10 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
-
def test_consolidation_round_off_error_1(self):
- '''
+ """
Test round off error in consolidated invoice creation if POS Invoice has inclusive tax
- '''
+ """
frappe.db.sql("delete from `tabPOS Invoice`")
@@ -171,43 +181,45 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
init_user_and_profile()
inv = create_pos_invoice(qty=3, rate=10000, do_not_save=True)
- inv.append("taxes", {
- "account_head": "_Test Account VAT - _TC",
- "charge_type": "On Net Total",
- "cost_center": "_Test Cost Center - _TC",
- "description": "VAT",
- "doctype": "Sales Taxes and Charges",
- "rate": 7.5,
- "included_in_print_rate": 1
- })
- inv.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000
- })
+ inv.append(
+ "taxes",
+ {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 7.5,
+ "included_in_print_rate": 1,
+ },
+ )
+ inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 30000})
inv.insert()
inv.submit()
inv2 = create_pos_invoice(qty=3, rate=10000, do_not_save=True)
- inv2.append("taxes", {
- "account_head": "_Test Account VAT - _TC",
- "charge_type": "On Net Total",
- "cost_center": "_Test Cost Center - _TC",
- "description": "VAT",
- "doctype": "Sales Taxes and Charges",
- "rate": 7.5,
- "included_in_print_rate": 1
- })
- inv2.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000
- })
+ inv2.append(
+ "taxes",
+ {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 7.5,
+ "included_in_print_rate": 1,
+ },
+ )
+ inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 30000})
inv2.insert()
inv2.submit()
consolidate_pos_invoices()
inv.load_from_db()
- consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
+ consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
self.assertEqual(consolidated_invoice.outstanding_amount, 0)
- self.assertEqual(consolidated_invoice.status, 'Paid')
+ self.assertEqual(consolidated_invoice.status, "Paid")
finally:
frappe.set_user("Administrator")
@@ -215,9 +227,9 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Invoice`")
def test_consolidation_round_off_error_2(self):
- '''
+ """
Test the same case as above but with an Unpaid POS Invoice
- '''
+ """
frappe.db.sql("delete from `tabPOS Invoice`")
try:
@@ -231,50 +243,207 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
init_user_and_profile()
inv = create_pos_invoice(qty=6, rate=10000, do_not_save=True)
- inv.append("taxes", {
- "account_head": "_Test Account VAT - _TC",
- "charge_type": "On Net Total",
- "cost_center": "_Test Cost Center - _TC",
- "description": "VAT",
- "doctype": "Sales Taxes and Charges",
- "rate": 7.5,
- "included_in_print_rate": 1
- })
- inv.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000
- })
+ inv.append(
+ "taxes",
+ {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 7.5,
+ "included_in_print_rate": 1,
+ },
+ )
+ inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60000})
inv.insert()
inv.submit()
inv2 = create_pos_invoice(qty=6, rate=10000, do_not_save=True)
- inv2.append("taxes", {
- "account_head": "_Test Account VAT - _TC",
- "charge_type": "On Net Total",
- "cost_center": "_Test Cost Center - _TC",
- "description": "VAT",
- "doctype": "Sales Taxes and Charges",
- "rate": 7.5,
- "included_in_print_rate": 1
- })
- inv2.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000
- })
+ inv2.append(
+ "taxes",
+ {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 7.5,
+ "included_in_print_rate": 1,
+ },
+ )
+ inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60000})
inv2.insert()
inv2.submit()
inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True)
- inv3.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000
- })
+ inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
inv3.insert()
inv3.submit()
consolidate_pos_invoices()
inv.load_from_db()
- consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
+ consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
self.assertEqual(consolidated_invoice.outstanding_amount, 800)
- self.assertNotEqual(consolidated_invoice.status, 'Paid')
+ self.assertNotEqual(consolidated_invoice.status, "Paid")
+
+ finally:
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabPOS Profile`")
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+ @change_settings(
+ "System Settings", {"number_format": "#,###.###", "currency_precision": 3, "float_precision": 3}
+ )
+ def test_consolidation_round_off_error_3(self):
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+ try:
+ make_stock_entry(
+ to_warehouse="_Test Warehouse - _TC",
+ item_code="_Test Item",
+ rate=8000,
+ qty=10,
+ )
+ init_user_and_profile()
+
+ item_rates = [69, 59, 29]
+ for i in [1, 2]:
+ inv = create_pos_invoice(is_return=1, do_not_save=1)
+ inv.items = []
+ for rate in item_rates:
+ inv.append(
+ "items",
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": -1,
+ "rate": rate,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ )
+ inv.append(
+ "taxes",
+ {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 15,
+ "included_in_print_rate": 1,
+ },
+ )
+ inv.payments = []
+ inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -157})
+ inv.paid_amount = -157
+ inv.save()
+ inv.submit()
+
+ consolidate_pos_invoices()
+
+ inv.load_from_db()
+ consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
+ self.assertEqual(consolidated_invoice.status, "Return")
+ self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001)
+
+ finally:
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabPOS Profile`")
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+ def test_consolidation_rounding_adjustment(self):
+ """
+ Test if the rounding adjustment is calculated correctly
+ """
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+ try:
+ make_stock_entry(
+ to_warehouse="_Test Warehouse - _TC",
+ item_code="_Test Item",
+ rate=8000,
+ qty=10,
+ )
+
+ init_user_and_profile()
+
+ inv = create_pos_invoice(qty=1, rate=69.5, do_not_save=True)
+ inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 70})
+ inv.insert()
+ inv.submit()
+
+ inv2 = create_pos_invoice(qty=1, rate=59.5, do_not_save=True)
+ inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60})
+ inv2.insert()
+ inv2.submit()
+
+ consolidate_pos_invoices()
+
+ inv.load_from_db()
+ consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
+ self.assertEqual(consolidated_invoice.rounding_adjustment, 1)
+
+ finally:
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabPOS Profile`")
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+ def test_serial_no_case_1(self):
+ """
+ Create a POS Invoice with serial no
+ Create a Return Invoice with serial no
+ Create a POS Invoice with serial no again
+ Consolidate the invoices
+
+ The first POS Invoice should be consolidated with a separate single Merge Log
+ The second and third POS Invoice should be consolidated with a single Merge Log
+ """
+
+ from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+ try:
+ se = make_serialized_item()
+ serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
+
+ init_user_and_profile()
+
+ pos_inv = create_pos_invoice(
+ item_code="_Test Serialized Item With Series",
+ serial_no=serial_no,
+ qty=1,
+ rate=100,
+ do_not_submit=1,
+ )
+ pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
+ pos_inv.submit()
+
+ pos_inv_cn = make_sales_return(pos_inv.name)
+ pos_inv_cn.paid_amount = -100
+ pos_inv_cn.submit()
+
+ pos_inv2 = create_pos_invoice(
+ item_code="_Test Serialized Item With Series",
+ serial_no=serial_no,
+ qty=1,
+ rate=100,
+ do_not_submit=1,
+ )
+ pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
+ pos_inv2.submit()
+
+ consolidate_pos_invoices()
+
+ pos_inv.load_from_db()
+ pos_inv2.load_from_db()
+
+ self.assertNotEqual(pos_inv.consolidated_invoice, pos_inv2.consolidated_invoice)
finally:
frappe.set_user("Administrator")
diff --git a/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json b/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json
index 205c4ede901..387c4b0f360 100644
--- a/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json
+++ b/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json
@@ -9,7 +9,9 @@
"posting_date",
"column_break_3",
"customer",
- "grand_total"
+ "grand_total",
+ "is_return",
+ "return_against"
],
"fields": [
{
@@ -48,11 +50,27 @@
"in_list_view": 1,
"label": "Amount",
"reqd": 1
+ },
+ {
+ "default": "0",
+ "fetch_from": "pos_invoice.is_return",
+ "fieldname": "is_return",
+ "fieldtype": "Check",
+ "label": "Is Return",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "pos_invoice.return_against",
+ "fieldname": "return_against",
+ "fieldtype": "Link",
+ "label": "Return Against",
+ "options": "POS Invoice",
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2020-05-29 15:08:42.194979",
+ "modified": "2022-03-24 13:32:02.366257",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Reference",
@@ -61,5 +79,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
index 0b2e045e5a7..3cd14264bb8 100644
--- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
+++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
@@ -17,7 +17,9 @@ class POSOpeningEntry(StatusUpdater):
def validate_pos_profile_and_cashier(self):
if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
- frappe.throw(_("POS Profile {} does not belongs to company {}").format(self.pos_profile, self.company))
+ frappe.throw(
+ _("POS Profile {} does not belongs to company {}").format(self.pos_profile, self.company)
+ )
if not cint(frappe.db.get_value("User", self.user, "enabled")):
frappe.throw(_("User {} is disabled. Please select valid user/cashier").format(self.user))
@@ -26,8 +28,11 @@ class POSOpeningEntry(StatusUpdater):
invalid_modes = []
for d in self.balance_details:
if d.mode_of_payment:
- account = frappe.db.get_value("Mode of Payment Account",
- {"parent": d.mode_of_payment, "company": self.company}, "default_account")
+ account = frappe.db.get_value(
+ "Mode of Payment Account",
+ {"parent": d.mode_of_payment, "company": self.company},
+ "default_account",
+ )
if not account:
invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))
diff --git a/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py
index 105d53d00e8..64c658ab151 100644
--- a/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py
+++ b/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py
@@ -9,6 +9,7 @@ import frappe
class TestPOSOpeningEntry(unittest.TestCase):
pass
+
def create_opening_entry(pos_profile, user):
entry = frappe.new_doc("POS Opening Entry")
entry.pos_profile = pos_profile.name
@@ -16,11 +17,9 @@ def create_opening_entry(pos_profile, user):
entry.company = pos_profile.company
entry.period_start_date = frappe.utils.get_datetime()
- balance_details = [];
+ balance_details = []
for d in pos_profile.payments:
- balance_details.append(frappe._dict({
- 'mode_of_payment': d.mode_of_payment
- }))
+ balance_details.append(frappe._dict({"mode_of_payment": d.mode_of_payment}))
entry.set("balance_details", balance_details)
entry.submit()
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json
index 9c9f37bba27..11646a6517d 100644
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.json
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json
@@ -22,6 +22,7 @@
"hide_images",
"hide_unavailable_items",
"auto_add_item_to_cart",
+ "validate_stock_on_save",
"column_break_16",
"update_stock",
"ignore_pricing_rule",
@@ -351,6 +352,12 @@
{
"fieldname": "column_break_25",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "validate_stock_on_save",
+ "fieldtype": "Check",
+ "label": "Validate Stock on Save"
}
],
"icon": "icon-cog",
@@ -378,10 +385,11 @@
"link_fieldname": "pos_profile"
}
],
- "modified": "2021-10-14 14:17:00.469298",
+ "modified": "2022-03-21 13:29:28.480533",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",
+ "naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@@ -404,5 +412,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py
index d80c1b27cce..cb84d2bec6c 100644
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.py
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py
@@ -18,29 +18,42 @@ class POSProfile(Document):
def validate_default_profile(self):
for row in self.applicable_for_users:
- res = frappe.db.sql("""select pf.name
+ res = frappe.db.sql(
+ """select pf.name
from
`tabPOS Profile User` pfu, `tabPOS Profile` pf
where
pf.name = pfu.parent and pfu.user = %s and pf.name != %s and pf.company = %s
- and pfu.default=1 and pf.disabled = 0""", (row.user, self.name, self.company))
+ and pfu.default=1 and pf.disabled = 0""",
+ (row.user, self.name, self.company),
+ )
if row.default and res:
- msgprint(_("Already set default in pos profile {0} for user {1}, kindly disabled default")
- .format(res[0][0], row.user), raise_exception=1)
+ msgprint(
+ _("Already set default in pos profile {0} for user {1}, kindly disabled default").format(
+ res[0][0], row.user
+ ),
+ raise_exception=1,
+ )
elif not row.default and not res:
- msgprint(_("User {0} doesn't have any default POS Profile. Check Default at Row {1} for this User.")
- .format(row.user, row.idx))
+ msgprint(
+ _(
+ "User {0} doesn't have any default POS Profile. Check Default at Row {1} for this User."
+ ).format(row.user, row.idx)
+ )
def validate_all_link_fields(self):
- accounts = {"Account": [self.income_account,
- self.expense_account], "Cost Center": [self.cost_center],
- "Warehouse": [self.warehouse]}
+ accounts = {
+ "Account": [self.income_account, self.expense_account],
+ "Cost Center": [self.cost_center],
+ "Warehouse": [self.warehouse],
+ }
for link_dt, dn_list in iteritems(accounts):
for link_dn in dn_list:
- if link_dn and not frappe.db.exists({"doctype": link_dt,
- "company": self.company, "name": link_dn}):
+ if link_dn and not frappe.db.exists(
+ {"doctype": link_dt, "company": self.company, "name": link_dn}
+ ):
frappe.throw(_("{0} does not belong to Company {1}").format(link_dn, self.company))
def validate_duplicate_groups(self):
@@ -48,10 +61,15 @@ class POSProfile(Document):
customer_groups = [d.customer_group for d in self.customer_groups]
if len(item_groups) != len(set(item_groups)):
- frappe.throw(_("Duplicate item group found in the item group table"), title = "Duplicate Item Group")
+ frappe.throw(
+ _("Duplicate item group found in the item group table"), title="Duplicate Item Group"
+ )
if len(customer_groups) != len(set(customer_groups)):
- frappe.throw(_("Duplicate customer group found in the cutomer group table"), title = "Duplicate Customer Group")
+ frappe.throw(
+ _("Duplicate customer group found in the cutomer group table"),
+ title="Duplicate Customer Group",
+ )
def validate_payment_methods(self):
if not self.payments:
@@ -69,7 +87,7 @@ class POSProfile(Document):
account = frappe.db.get_value(
"Mode of Payment Account",
{"parent": d.mode_of_payment, "company": self.company},
- "default_account"
+ "default_account",
)
if not account:
@@ -92,12 +110,16 @@ class POSProfile(Document):
frappe.defaults.clear_default("is_pos")
if not include_current_pos:
- condition = " where pfu.name != '%s' and pfu.default = 1 " % self.name.replace("'", "\'")
+ condition = " where pfu.name != '%s' and pfu.default = 1 " % self.name.replace("'", "'")
else:
condition = " where pfu.default = 1 "
- pos_view_users = frappe.db.sql_list("""select pfu.user
- from `tabPOS Profile User` as pfu {0}""".format(condition))
+ pos_view_users = frappe.db.sql_list(
+ """select pfu.user
+ from `tabPOS Profile User` as pfu {0}""".format(
+ condition
+ )
+ )
for user in pos_view_users:
if user:
@@ -105,48 +127,62 @@ class POSProfile(Document):
else:
frappe.defaults.set_global_default("is_pos", 1)
+
def get_item_groups(pos_profile):
item_groups = []
- pos_profile = frappe.get_cached_doc('POS Profile', pos_profile)
+ pos_profile = frappe.get_cached_doc("POS Profile", pos_profile)
- if pos_profile.get('item_groups'):
+ if pos_profile.get("item_groups"):
# Get items based on the item groups defined in the POS profile
- for data in pos_profile.get('item_groups'):
- item_groups.extend(["%s" % frappe.db.escape(d.name) for d in get_child_nodes('Item Group', data.item_group)])
+ for data in pos_profile.get("item_groups"):
+ item_groups.extend(
+ ["%s" % frappe.db.escape(d.name) for d in get_child_nodes("Item Group", data.item_group)]
+ )
return list(set(item_groups))
+
def get_child_nodes(group_type, root):
lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"])
- return frappe.db.sql(""" Select name, lft, rgt from `tab{tab}` where
- lft >= {lft} and rgt <= {rgt} order by lft""".format(tab=group_type, lft=lft, rgt=rgt), as_dict=1)
+ return frappe.db.sql(
+ """ Select name, lft, rgt from `tab{tab}` where
+ lft >= {lft} and rgt <= {rgt} order by lft""".format(
+ tab=group_type, lft=lft, rgt=rgt
+ ),
+ as_dict=1,
+ )
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
- user = frappe.session['user']
- company = filters.get('company') or frappe.defaults.get_user_default('company')
+ user = frappe.session["user"]
+ company = filters.get("company") or frappe.defaults.get_user_default("company")
args = {
- 'user': user,
- 'start': start,
- 'company': company,
- 'page_len': page_len,
- 'txt': '%%%s%%' % txt
+ "user": user,
+ "start": start,
+ "company": company,
+ "page_len": page_len,
+ "txt": "%%%s%%" % txt,
}
- pos_profile = frappe.db.sql("""select pf.name
+ pos_profile = frappe.db.sql(
+ """select pf.name
from
`tabPOS Profile` pf, `tabPOS Profile User` pfu
where
pfu.parent = pf.name and pfu.user = %(user)s and pf.company = %(company)s
and (pf.name like %(txt)s)
- and pf.disabled = 0 limit %(start)s, %(page_len)s""", args)
+ and pf.disabled = 0 limit %(start)s, %(page_len)s""",
+ args,
+ )
if not pos_profile:
- del args['user']
+ del args["user"]
- pos_profile = frappe.db.sql("""select pf.name
+ pos_profile = frappe.db.sql(
+ """select pf.name
from
`tabPOS Profile` pf left join `tabPOS Profile User` pfu
on
@@ -155,26 +191,37 @@ def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
ifnull(pfu.user, '') = ''
and pf.company = %(company)s
and pf.name like %(txt)s
- and pf.disabled = 0""", args)
+ and pf.disabled = 0""",
+ args,
+ )
return pos_profile
+
@frappe.whitelist()
def set_default_profile(pos_profile, company):
modified = now()
user = frappe.session.user
if pos_profile and company:
- frappe.db.sql(""" update `tabPOS Profile User` pfu, `tabPOS Profile` pf
+ frappe.db.sql(
+ """ update `tabPOS Profile User` pfu, `tabPOS Profile` pf
set
pfu.default = 0, pf.modified = %s, pf.modified_by = %s
where
pfu.user = %s and pf.name = pfu.parent and pf.company = %s
- and pfu.default = 1""", (modified, user, user, company), auto_commit=1)
+ and pfu.default = 1""",
+ (modified, user, user, company),
+ auto_commit=1,
+ )
- frappe.db.sql(""" update `tabPOS Profile User` pfu, `tabPOS Profile` pf
+ frappe.db.sql(
+ """ update `tabPOS Profile User` pfu, `tabPOS Profile` pf
set
pfu.default = 1, pf.modified = %s, pf.modified_by = %s
where
pfu.user = %s and pf.name = pfu.parent and pf.company = %s and pf.name = %s
- """, (modified, user, user, company, pos_profile), auto_commit=1)
+ """,
+ (modified, user, user, company, pos_profile),
+ auto_commit=1,
+ )
diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
index c8cf0d2a0f1..788aa62701d 100644
--- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
+++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
@@ -8,7 +8,8 @@ import frappe
from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes
from erpnext.stock.get_item_details import get_pos_profile
-test_dependencies = ['Item']
+test_dependencies = ["Item"]
+
class TestPOSProfile(unittest.TestCase):
def test_pos_profile(self):
@@ -17,46 +18,64 @@ class TestPOSProfile(unittest.TestCase):
pos_profile = get_pos_profile("_Test Company") or {}
if pos_profile:
doc = frappe.get_doc("POS Profile", pos_profile.get("name"))
- doc.append('item_groups', {'item_group': '_Test Item Group'})
- doc.append('customer_groups', {'customer_group': '_Test Customer Group'})
+ doc.append("item_groups", {"item_group": "_Test Item Group"})
+ doc.append("customer_groups", {"customer_group": "_Test Customer Group"})
doc.save()
items = get_items_list(doc, doc.company)
customers = get_customers_list(doc)
- products_count = frappe.db.sql(""" select count(name) from tabItem where item_group = '_Test Item Group'""", as_list=1)
- customers_count = frappe.db.sql(""" select count(name) from tabCustomer where customer_group = '_Test Customer Group'""")
+ products_count = frappe.db.sql(
+ """ select count(name) from tabItem where item_group = '_Test Item Group'""", as_list=1
+ )
+ customers_count = frappe.db.sql(
+ """ select count(name) from tabCustomer where customer_group = '_Test Customer Group'"""
+ )
self.assertEqual(len(items), products_count[0][0])
self.assertEqual(len(customers), customers_count[0][0])
frappe.db.sql("delete from `tabPOS Profile`")
+
def get_customers_list(pos_profile=None):
if pos_profile is None:
pos_profile = {}
cond = "1=1"
customer_groups = []
- if pos_profile.get('customer_groups'):
+ if pos_profile.get("customer_groups"):
# Get customers based on the customer groups defined in the POS profile
- for d in pos_profile.get('customer_groups'):
- customer_groups.extend([d.get('name') for d in get_child_nodes('Customer Group', d.get('customer_group'))])
- cond = "customer_group in (%s)" % (', '.join(['%s'] * len(customer_groups)))
+ for d in pos_profile.get("customer_groups"):
+ customer_groups.extend(
+ [d.get("name") for d in get_child_nodes("Customer Group", d.get("customer_group"))]
+ )
+ cond = "customer_group in (%s)" % (", ".join(["%s"] * len(customer_groups)))
- return frappe.db.sql(""" select name, customer_name, customer_group,
+ return (
+ frappe.db.sql(
+ """ select name, customer_name, customer_group,
territory, customer_pos_id from tabCustomer where disabled = 0
- and {cond}""".format(cond=cond), tuple(customer_groups), as_dict=1) or {}
+ and {cond}""".format(
+ cond=cond
+ ),
+ tuple(customer_groups),
+ as_dict=1,
+ )
+ or {}
+ )
+
def get_items_list(pos_profile, company):
cond = ""
args_list = []
- if pos_profile.get('item_groups'):
+ if pos_profile.get("item_groups"):
# Get items based on the item groups defined in the POS profile
- for d in pos_profile.get('item_groups'):
- args_list.extend([d.name for d in get_child_nodes('Item Group', d.item_group)])
+ for d in pos_profile.get("item_groups"):
+ args_list.extend([d.name for d in get_child_nodes("Item Group", d.item_group)])
if args_list:
- cond = "and i.item_group in (%s)" % (', '.join(['%s'] * len(args_list)))
+ cond = "and i.item_group in (%s)" % (", ".join(["%s"] * len(args_list)))
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
select
i.name, i.item_code, i.item_name, i.description, i.item_group, i.has_batch_no,
i.has_serial_no, i.is_stock_item, i.brand, i.stock_uom, i.image,
@@ -69,7 +88,13 @@ def get_items_list(pos_profile, company):
where
i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1 and i.is_fixed_asset = 0
{cond}
- """.format(cond=cond), tuple([company] + args_list), as_dict=1)
+ """.format(
+ cond=cond
+ ),
+ tuple([company] + args_list),
+ as_dict=1,
+ )
+
def make_pos_profile(**args):
frappe.db.sql("delete from `tabPOS Payment Method`")
@@ -77,38 +102,34 @@ def make_pos_profile(**args):
args = frappe._dict(args)
- pos_profile = frappe.get_doc({
- "company": args.company or "_Test Company",
- "cost_center": args.cost_center or "_Test Cost Center - _TC",
- "currency": args.currency or "INR",
- "doctype": "POS Profile",
- "expense_account": args.expense_account or "_Test Account Cost for Goods Sold - _TC",
- "income_account": args.income_account or "Sales - _TC",
- "name": args.name or "_Test POS Profile",
- "naming_series": "_T-POS Profile-",
- "selling_price_list": args.selling_price_list or "_Test Price List",
- "territory": args.territory or "_Test Territory",
- "customer_group": frappe.db.get_value('Customer Group', {'is_group': 0}, 'name'),
- "warehouse": args.warehouse or "_Test Warehouse - _TC",
- "write_off_account": args.write_off_account or "_Test Write Off - _TC",
- "write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC"
- })
+ pos_profile = frappe.get_doc(
+ {
+ "company": args.company or "_Test Company",
+ "cost_center": args.cost_center or "_Test Cost Center - _TC",
+ "currency": args.currency or "INR",
+ "doctype": "POS Profile",
+ "expense_account": args.expense_account or "_Test Account Cost for Goods Sold - _TC",
+ "income_account": args.income_account or "Sales - _TC",
+ "name": args.name or "_Test POS Profile",
+ "naming_series": "_T-POS Profile-",
+ "selling_price_list": args.selling_price_list or "_Test Price List",
+ "territory": args.territory or "_Test Territory",
+ "customer_group": frappe.db.get_value("Customer Group", {"is_group": 0}, "name"),
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "write_off_account": args.write_off_account or "_Test Write Off - _TC",
+ "write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC",
+ }
+ )
mode_of_payment = frappe.get_doc("Mode of Payment", "Cash")
company = args.company or "_Test Company"
default_account = args.income_account or "Sales - _TC"
if not frappe.db.get_value("Mode of Payment Account", {"company": company, "parent": "Cash"}):
- mode_of_payment.append("accounts", {
- "company": company,
- "default_account": default_account
- })
+ mode_of_payment.append("accounts", {"company": company, "default_account": default_account})
mode_of_payment.save()
- pos_profile.append("payments", {
- 'mode_of_payment': 'Cash',
- 'default': 1
- })
+ pos_profile.append("payments", {"mode_of_payment": "Cash", "default": 1})
if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"):
pos_profile.insert()
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index ad60bbad950..db6ec82c83d 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -13,11 +13,11 @@ from frappe.model.document import Document
from frappe.utils import cint, flt, getdate
from six import string_types
-apply_on_dict = {"Item Code": "items",
- "Item Group": "item_groups", "Brand": "brands"}
+apply_on_dict = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}
other_fields = ["other_item_code", "other_item_group", "other_brand"]
+
class PricingRule(Document):
def validate(self):
self.validate_mandatory()
@@ -32,7 +32,8 @@ class PricingRule(Document):
self.validate_dates()
self.validate_condition()
- if not self.margin_type: self.margin_rate_or_amount = 0.0
+ if not self.margin_type:
+ self.margin_rate_or_amount = 0.0
def validate_duplicate_apply_on(self):
field = apply_on_dict.get(self.apply_on)
@@ -50,36 +51,51 @@ class PricingRule(Document):
throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError)
if self.apply_rule_on_other:
- o_field = 'other_' + frappe.scrub(self.apply_rule_on_other)
+ o_field = "other_" + frappe.scrub(self.apply_rule_on_other)
if not self.get(o_field) and o_field in other_fields:
- frappe.throw(_("For the 'Apply Rule On Other' condition the field {0} is mandatory")
- .format(frappe.bold(self.apply_rule_on_other)))
+ frappe.throw(
+ _("For the 'Apply Rule On Other' condition the field {0} is mandatory").format(
+ frappe.bold(self.apply_rule_on_other)
+ )
+ )
-
- if self.price_or_product_discount == 'Price' and not self.rate_or_discount:
+ if self.price_or_product_discount == "Price" and not self.rate_or_discount:
throw(_("Rate or Discount is required for the price discount."), frappe.MandatoryError)
if self.apply_discount_on_rate:
if not self.priority:
- throw(_("As the field {0} is enabled, the field {1} is mandatory.")
- .format(frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")))
+ throw(
+ _("As the field {0} is enabled, the field {1} is mandatory.").format(
+ frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")
+ )
+ )
if self.priority and cint(self.priority) == 1:
- throw(_("As the field {0} is enabled, the value of the field {1} should be more than 1.")
- .format(frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")))
+ throw(
+ _("As the field {0} is enabled, the value of the field {1} should be more than 1.").format(
+ frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")
+ )
+ )
def validate_applicable_for_selling_or_buying(self):
if not self.selling and not self.buying:
throw(_("Atleast one of the Selling or Buying must be selected"))
- if not self.selling and self.applicable_for in ["Customer", "Customer Group",
- "Territory", "Sales Partner", "Campaign"]:
- throw(_("Selling must be checked, if Applicable For is selected as {0}")
- .format(self.applicable_for))
+ if not self.selling and self.applicable_for in [
+ "Customer",
+ "Customer Group",
+ "Territory",
+ "Sales Partner",
+ "Campaign",
+ ]:
+ throw(
+ _("Selling must be checked, if Applicable For is selected as {0}").format(self.applicable_for)
+ )
if not self.buying and self.applicable_for in ["Supplier", "Supplier Group"]:
- throw(_("Buying must be checked, if Applicable For is selected as {0}")
- .format(self.applicable_for))
+ throw(
+ _("Buying must be checked, if Applicable For is selected as {0}").format(self.applicable_for)
+ )
def validate_min_max_qty(self):
if self.min_qty and self.max_qty and flt(self.min_qty) > flt(self.max_qty):
@@ -96,11 +112,12 @@ class PricingRule(Document):
# reset all values except for the logic field
options = (self.meta.get_options(logic_field) or "").split("\n")
for f in options:
- if not f: continue
+ if not f:
+ continue
scrubbed_f = frappe.scrub(f)
- if logic_field == 'apply_on':
+ if logic_field == "apply_on":
apply_on_f = apply_on_dict.get(f, f)
else:
apply_on_f = scrubbed_f
@@ -113,8 +130,11 @@ class PricingRule(Document):
apply_rule_on_other = frappe.scrub(self.apply_rule_on_other or "")
- cleanup_other_fields = (other_fields if not apply_rule_on_other
- else [o_field for o_field in other_fields if o_field != 'other_' + apply_rule_on_other])
+ cleanup_other_fields = (
+ other_fields
+ if not apply_rule_on_other
+ else [o_field for o_field in other_fields if o_field != "other_" + apply_rule_on_other]
+ )
for other_field in cleanup_other_fields:
self.set(other_field, None)
@@ -124,7 +144,7 @@ class PricingRule(Document):
if flt(self.get(frappe.scrub(field))) < 0:
throw(_("{0} can not be negative").format(field))
- if self.price_or_product_discount == 'Product' and not self.free_item:
+ if self.price_or_product_discount == "Product" and not self.free_item:
if self.mixed_conditions:
frappe.throw(_("Free item code is not selected"))
else:
@@ -151,31 +171,37 @@ class PricingRule(Document):
frappe.throw(_("Valid from date must be less than valid upto date"))
def validate_condition(self):
- if self.condition and ("=" in self.condition) and re.match(r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+', self.condition):
+ if (
+ self.condition
+ and ("=" in self.condition)
+ and re.match(r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+', self.condition)
+ ):
frappe.throw(_("Invalid condition expression"))
-#--------------------------------------------------------------------------------
+
+# --------------------------------------------------------------------------------
+
@frappe.whitelist()
def apply_pricing_rule(args, doc=None):
"""
- args = {
- "items": [{"doctype": "", "name": "", "item_code": "", "brand": "", "item_group": ""}, ...],
- "customer": "something",
- "customer_group": "something",
- "territory": "something",
- "supplier": "something",
- "supplier_group": "something",
- "currency": "something",
- "conversion_rate": "something",
- "price_list": "something",
- "plc_conversion_rate": "something",
- "company": "something",
- "transaction_date": "something",
- "campaign": "something",
- "sales_partner": "something",
- "ignore_pricing_rule": "something"
- }
+ args = {
+ "items": [{"doctype": "", "name": "", "item_code": "", "brand": "", "item_group": ""}, ...],
+ "customer": "something",
+ "customer_group": "something",
+ "territory": "something",
+ "supplier": "something",
+ "supplier_group": "something",
+ "currency": "something",
+ "conversion_rate": "something",
+ "price_list": "something",
+ "plc_conversion_rate": "something",
+ "company": "something",
+ "transaction_date": "something",
+ "campaign": "something",
+ "sales_partner": "something",
+ "ignore_pricing_rule": "something"
+ }
"""
if isinstance(args, string_types):
@@ -189,16 +215,23 @@ def apply_pricing_rule(args, doc=None):
# list of dictionaries
out = []
- if args.get("doctype") == "Material Request": return out
+ if args.get("doctype") == "Material Request":
+ return out
item_list = args.get("items")
args.pop("items")
- set_serial_nos_based_on_fifo = frappe.db.get_single_value("Stock Settings",
- "automatically_set_serial_nos_based_on_fifo")
+ set_serial_nos_based_on_fifo = frappe.db.get_single_value(
+ "Stock Settings", "automatically_set_serial_nos_based_on_fifo"
+ )
- item_code_list = tuple(item.get('item_code') for item in item_list)
- query_items = frappe.get_all('Item', fields=['item_code','has_serial_no'], filters=[['item_code','in',item_code_list]],as_list=1)
+ item_code_list = tuple(item.get("item_code") for item in item_list)
+ query_items = frappe.get_all(
+ "Item",
+ fields=["item_code", "has_serial_no"],
+ filters=[["item_code", "in", item_code_list]],
+ as_list=1,
+ )
serialized_items = dict()
for item_code, val in query_items:
serialized_items.setdefault(item_code, val)
@@ -206,26 +239,31 @@ def apply_pricing_rule(args, doc=None):
for item in item_list:
args_copy = copy.deepcopy(args)
args_copy.update(item)
- data = get_pricing_rule_for_item(args_copy, item.get('price_list_rate'), doc=doc)
+ data = get_pricing_rule_for_item(args_copy, item.get("price_list_rate"), doc=doc)
out.append(data)
- if serialized_items.get(item.get('item_code')) and not item.get("serial_no") and set_serial_nos_based_on_fifo and not args.get('is_return'):
+ if (
+ serialized_items.get(item.get("item_code"))
+ and not item.get("serial_no")
+ and set_serial_nos_based_on_fifo
+ and not args.get("is_return")
+ ):
out[0].update(get_serial_no_for_item(args_copy))
return out
+
def get_serial_no_for_item(args):
from erpnext.stock.get_item_details import get_serial_no
- item_details = frappe._dict({
- "doctype": args.doctype,
- "name": args.name,
- "serial_no": args.serial_no
- })
+ item_details = frappe._dict(
+ {"doctype": args.doctype, "name": args.name, "serial_no": args.serial_no}
+ )
if args.get("parenttype") in ("Sales Invoice", "Delivery Note") and flt(args.stock_qty) > 0:
item_details.serial_no = get_serial_no(args)
return item_details
+
def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False):
from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules,
@@ -240,18 +278,20 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
if doc:
doc = frappe.get_doc(doc)
- if (args.get('is_free_item') or
- args.get("parenttype") == "Material Request"): return {}
+ if args.get("is_free_item") or args.get("parenttype") == "Material Request":
+ return {}
- item_details = frappe._dict({
- "doctype": args.doctype,
- "has_margin": False,
- "name": args.name,
- "free_item_data": [],
- "parent": args.parent,
- "parenttype": args.parenttype,
- "child_docname": args.get('child_docname'),
- })
+ item_details = frappe._dict(
+ {
+ "doctype": args.doctype,
+ "has_margin": False,
+ "name": args.name,
+ "free_item_data": [],
+ "parent": args.parent,
+ "parenttype": args.parenttype,
+ "child_docname": args.get("child_docname"),
+ }
+ )
if args.ignore_pricing_rule or not args.item_code:
if frappe.db.exists(args.doctype, args.name) and args.get("pricing_rules"):
@@ -265,20 +305,25 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
update_args_for_pricing_rule(args)
- pricing_rules = (get_applied_pricing_rules(args.get('pricing_rules'))
- if for_validate and args.get("pricing_rules") else get_pricing_rules(args, doc))
+ pricing_rules = (
+ get_applied_pricing_rules(args.get("pricing_rules"))
+ if for_validate and args.get("pricing_rules")
+ else get_pricing_rules(args, doc)
+ )
if pricing_rules:
rules = []
for pricing_rule in pricing_rules:
- if not pricing_rule: continue
+ if not pricing_rule:
+ continue
if isinstance(pricing_rule, string_types):
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule)
- if pricing_rule.get('suggestion'): continue
+ if pricing_rule.get("suggestion"):
+ continue
item_details.validate_applied_rule = pricing_rule.get("validate_applied_rule", 0)
item_details.price_or_product_discount = pricing_rule.get("price_or_product_discount")
@@ -286,14 +331,19 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
rules.append(get_pricing_rule_details(args, pricing_rule))
if pricing_rule.mixed_conditions or pricing_rule.apply_rule_on_other:
- item_details.update({
- 'apply_rule_on_other_items': json.dumps(pricing_rule.apply_rule_on_other_items),
- 'price_or_product_discount': pricing_rule.price_or_product_discount,
- 'apply_rule_on': (frappe.scrub(pricing_rule.apply_rule_on_other)
- if pricing_rule.apply_rule_on_other else frappe.scrub(pricing_rule.get('apply_on')))
- })
+ item_details.update(
+ {
+ "apply_rule_on_other_items": json.dumps(pricing_rule.apply_rule_on_other_items),
+ "price_or_product_discount": pricing_rule.price_or_product_discount,
+ "apply_rule_on": (
+ frappe.scrub(pricing_rule.apply_rule_on_other)
+ if pricing_rule.apply_rule_on_other
+ else frappe.scrub(pricing_rule.get("apply_on"))
+ ),
+ }
+ )
- if pricing_rule.coupon_code_based==1 and args.coupon_code==None:
+ if pricing_rule.coupon_code_based == 1 and args.coupon_code == None:
return item_details
if not pricing_rule.validate_applied_rule:
@@ -310,7 +360,8 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
item_details.pricing_rules = frappe.as_json([d.pricing_rule for d in rules])
- if not doc: return item_details
+ if not doc:
+ return item_details
elif args.get("pricing_rules"):
item_details = remove_pricing_rule_for_item(
@@ -322,19 +373,22 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
return item_details
+
def update_args_for_pricing_rule(args):
if not (args.item_group and args.brand):
try:
- args.item_group, args.brand = frappe.get_cached_value("Item", args.item_code, ["item_group", "brand"])
+ args.item_group, args.brand = frappe.get_cached_value(
+ "Item", args.item_code, ["item_group", "brand"]
+ )
except frappe.DoesNotExistError:
return
if not args.item_group:
frappe.throw(_("Item Group not mentioned in item master for item {0}").format(args.item_code))
- if args.transaction_type=="selling":
+ if args.transaction_type == "selling":
if args.customer and not (args.customer_group and args.territory):
- if args.quotation_to and args.quotation_to != 'Customer':
+ if args.quotation_to and args.quotation_to != "Customer":
customer = frappe._dict()
else:
customer = frappe.get_cached_value("Customer", args.customer, ["customer_group", "territory"])
@@ -348,20 +402,25 @@ def update_args_for_pricing_rule(args):
args.supplier_group = frappe.get_cached_value("Supplier", args.supplier, "supplier_group")
args.customer = args.customer_group = args.territory = None
+
def get_pricing_rule_details(args, pricing_rule):
- return frappe._dict({
- 'pricing_rule': pricing_rule.name,
- 'rate_or_discount': pricing_rule.rate_or_discount,
- 'margin_type': pricing_rule.margin_type,
- 'item_code': args.get("item_code"),
- 'child_docname': args.get('child_docname')
- })
+ return frappe._dict(
+ {
+ "pricing_rule": pricing_rule.name,
+ "rate_or_discount": pricing_rule.rate_or_discount,
+ "margin_type": pricing_rule.margin_type,
+ "item_code": args.get("item_code"),
+ "child_docname": args.get("child_docname"),
+ }
+ )
+
def apply_price_discount_rule(pricing_rule, item_details, args):
item_details.pricing_rule_for = pricing_rule.rate_or_discount
- if ((pricing_rule.margin_type in ['Amount', 'Percentage'] and pricing_rule.currency == args.currency)
- or (pricing_rule.margin_type == 'Percentage')):
+ if (
+ pricing_rule.margin_type in ["Amount", "Percentage"] and pricing_rule.currency == args.currency
+ ) or (pricing_rule.margin_type == "Percentage"):
item_details.margin_type = pricing_rule.margin_type
item_details.has_margin = True
@@ -370,7 +429,7 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
else:
item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
- if pricing_rule.rate_or_discount == 'Rate':
+ if pricing_rule.rate_or_discount == "Rate":
pricing_rule_rate = 0.0
if pricing_rule.currency == args.currency:
pricing_rule_rate = pricing_rule.rate
@@ -378,63 +437,71 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
if pricing_rule_rate:
# Override already set price list rate (from item price)
# if pricing_rule_rate > 0
- item_details.update({
- "price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
- })
- item_details.update({
- "discount_percentage": 0.0
- })
+ item_details.update(
+ {
+ "price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
+ }
+ )
+ item_details.update({"discount_percentage": 0.0})
- for apply_on in ['Discount Amount', 'Discount Percentage']:
- if pricing_rule.rate_or_discount != apply_on: continue
+ for apply_on in ["Discount Amount", "Discount Percentage"]:
+ if pricing_rule.rate_or_discount != apply_on:
+ continue
field = frappe.scrub(apply_on)
if pricing_rule.apply_discount_on_rate and item_details.get("discount_percentage"):
# Apply discount on discounted rate
- item_details[field] += ((100 - item_details[field]) * (pricing_rule.get(field, 0) / 100))
+ item_details[field] += (100 - item_details[field]) * (pricing_rule.get(field, 0) / 100)
else:
if field not in item_details:
item_details.setdefault(field, 0)
- item_details[field] += (pricing_rule.get(field, 0)
- if pricing_rule else args.get(field, 0))
+ item_details[field] += pricing_rule.get(field, 0) if pricing_rule else args.get(field, 0)
+
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None):
from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules,
get_pricing_rule_items,
)
- for d in get_applied_pricing_rules(pricing_rules):
- if not d or not frappe.db.exists("Pricing Rule", d): continue
- pricing_rule = frappe.get_cached_doc('Pricing Rule', d)
- if pricing_rule.price_or_product_discount == 'Price':
- if pricing_rule.rate_or_discount == 'Discount Percentage':
+ for d in get_applied_pricing_rules(pricing_rules):
+ if not d or not frappe.db.exists("Pricing Rule", d):
+ continue
+ pricing_rule = frappe.get_cached_doc("Pricing Rule", d)
+
+ if pricing_rule.price_or_product_discount == "Price":
+ if pricing_rule.rate_or_discount == "Discount Percentage":
item_details.discount_percentage = 0.0
item_details.discount_amount = 0.0
item_details.rate = rate or 0.0
- if pricing_rule.rate_or_discount == 'Discount Amount':
+ if pricing_rule.rate_or_discount == "Discount Amount":
item_details.discount_amount = 0.0
- if pricing_rule.margin_type in ['Percentage', 'Amount']:
+ if pricing_rule.margin_type in ["Percentage", "Amount"]:
item_details.margin_rate_or_amount = 0.0
item_details.margin_type = None
- elif pricing_rule.get('free_item'):
- item_details.remove_free_item = (item_code if pricing_rule.get('same_item')
- else pricing_rule.get('free_item'))
+ elif pricing_rule.get("free_item"):
+ item_details.remove_free_item = (
+ item_code if pricing_rule.get("same_item") else pricing_rule.get("free_item")
+ )
if pricing_rule.get("mixed_conditions") or pricing_rule.get("apply_rule_on_other"):
items = get_pricing_rule_items(pricing_rule)
- item_details.apply_on = (frappe.scrub(pricing_rule.apply_rule_on_other)
- if pricing_rule.apply_rule_on_other else frappe.scrub(pricing_rule.get('apply_on')))
- item_details.applied_on_items = ','.join(items)
+ item_details.apply_on = (
+ frappe.scrub(pricing_rule.apply_rule_on_other)
+ if pricing_rule.apply_rule_on_other
+ else frappe.scrub(pricing_rule.get("apply_on"))
+ )
+ item_details.applied_on_items = ",".join(items)
- item_details.pricing_rules = ''
+ item_details.pricing_rules = ""
item_details.pricing_rule_removed = True
return item_details
+
@frappe.whitelist()
def remove_pricing_rules(item_list):
if isinstance(item_list, string_types):
@@ -452,19 +519,26 @@ def remove_pricing_rules(item_list):
return out
+
def set_transaction_type(args):
if args.transaction_type:
return
if args.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
args.transaction_type = "selling"
- elif args.doctype in ("Material Request", "Supplier Quotation", "Purchase Order",
- "Purchase Receipt", "Purchase Invoice"):
- args.transaction_type = "buying"
+ elif args.doctype in (
+ "Material Request",
+ "Supplier Quotation",
+ "Purchase Order",
+ "Purchase Receipt",
+ "Purchase Invoice",
+ ):
+ args.transaction_type = "buying"
elif args.customer:
args.transaction_type = "selling"
else:
args.transaction_type = "buying"
+
@frappe.whitelist()
def make_pricing_rule(doctype, docname):
doc = frappe.new_doc("Pricing Rule")
@@ -475,15 +549,18 @@ def make_pricing_rule(doctype, docname):
return doc
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_item_uoms(doctype, txt, searchfield, start, page_len, filters):
- items = [filters.get('value')]
- if filters.get('apply_on') != 'Item Code':
- field = frappe.scrub(filters.get('apply_on'))
- items = [d.name for d in frappe.db.get_all("Item", filters={field: filters.get('value')})]
+ items = [filters.get("value")]
+ if filters.get("apply_on") != "Item Code":
+ field = frappe.scrub(filters.get("apply_on"))
+ items = [d.name for d in frappe.db.get_all("Item", filters={field: filters.get("value")})]
- return frappe.get_all('UOM Conversion Detail', filters={
- 'parent': ('in', items),
- 'uom': ("like", "{0}%".format(txt))
- }, fields = ["distinct uom"], as_list=1)
+ return frappe.get_all(
+ "UOM Conversion Detail",
+ filters={"parent": ("in", items), "uom": ("like", "{0}%".format(txt))},
+ fields=["distinct uom"],
+ as_list=1,
+ )
diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
index f3b3cd4df77..4b81a7d6a23 100644
--- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
@@ -2,7 +2,6 @@
# License: GNU General Public License v3. See license.txt
-
import unittest
import frappe
@@ -32,31 +31,31 @@ class TestPricingRule(unittest.TestCase):
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
- "items": [{
- "item_code": "_Test Item"
- }],
+ "items": [{"item_code": "_Test Item"}],
"currency": "USD",
"selling": 1,
"rate_or_discount": "Discount Percentage",
"rate": 0,
"discount_percentage": 10,
- "company": "_Test Company"
+ "company": "_Test Company",
}
frappe.get_doc(test_record.copy()).insert()
- args = frappe._dict({
- "item_code": "_Test Item",
- "company": "_Test Company",
- "price_list": "_Test Price List",
- "currency": "_Test Currency",
- "doctype": "Sales Order",
- "conversion_rate": 1,
- "price_list_currency": "_Test Currency",
- "plc_conversion_rate": 1,
- "order_type": "Sales",
- "customer": "_Test Customer",
- "name": None
- })
+ args = frappe._dict(
+ {
+ "item_code": "_Test Item",
+ "company": "_Test Company",
+ "price_list": "_Test Price List",
+ "currency": "_Test Currency",
+ "doctype": "Sales Order",
+ "conversion_rate": 1,
+ "price_list_currency": "_Test Currency",
+ "plc_conversion_rate": 1,
+ "order_type": "Sales",
+ "customer": "_Test Customer",
+ "name": None,
+ }
+ )
details = get_item_details(args)
self.assertEqual(details.get("discount_percentage"), 10)
@@ -75,9 +74,7 @@ class TestPricingRule(unittest.TestCase):
prule = frappe.get_doc(test_record.copy())
prule.apply_on = "Item Group"
prule.items = []
- prule.append('item_groups', {
- 'item_group': "All Item Groups"
- })
+ prule.append("item_groups", {"item_group": "All Item Groups"})
prule.title = "_Test Pricing Rule for Item Group"
prule.discount_percentage = 15
prule.insert()
@@ -100,6 +97,7 @@ class TestPricingRule(unittest.TestCase):
frappe.db.sql("update `tabPricing Rule` set priority=NULL where campaign='_Test Campaign'")
from erpnext.accounts.doctype.pricing_rule.utils import MultiplePricingRuleConflict
+
self.assertRaises(MultiplePricingRuleConflict, get_item_details, args)
args.item_code = "_Test Item 2"
@@ -115,41 +113,47 @@ class TestPricingRule(unittest.TestCase):
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
- "items": [{
- "item_code": "_Test FG Item 2",
- }],
+ "items": [
+ {
+ "item_code": "_Test FG Item 2",
+ }
+ ],
"selling": 1,
"currency": "USD",
"rate_or_discount": "Discount Percentage",
"rate": 0,
"margin_type": "Percentage",
"margin_rate_or_amount": 10,
- "company": "_Test Company"
+ "company": "_Test Company",
}
frappe.get_doc(test_record.copy()).insert()
- item_price = frappe.get_doc({
- "doctype": "Item Price",
- "price_list": "_Test Price List 2",
- "item_code": "_Test FG Item 2",
- "price_list_rate": 100
- })
+ item_price = frappe.get_doc(
+ {
+ "doctype": "Item Price",
+ "price_list": "_Test Price List 2",
+ "item_code": "_Test FG Item 2",
+ "price_list_rate": 100,
+ }
+ )
item_price.insert(ignore_permissions=True)
- args = frappe._dict({
- "item_code": "_Test FG Item 2",
- "company": "_Test Company",
- "price_list": "_Test Price List",
- "currency": "_Test Currency",
- "doctype": "Sales Order",
- "conversion_rate": 1,
- "price_list_currency": "_Test Currency",
- "plc_conversion_rate": 1,
- "order_type": "Sales",
- "customer": "_Test Customer",
- "name": None
- })
+ args = frappe._dict(
+ {
+ "item_code": "_Test FG Item 2",
+ "company": "_Test Company",
+ "price_list": "_Test Price List",
+ "currency": "_Test Currency",
+ "doctype": "Sales Order",
+ "conversion_rate": 1,
+ "price_list_currency": "_Test Currency",
+ "plc_conversion_rate": 1,
+ "order_type": "Sales",
+ "customer": "_Test Customer",
+ "name": None,
+ }
+ )
details = get_item_details(args)
self.assertEqual(details.get("margin_type"), "Percentage")
self.assertEqual(details.get("margin_rate_or_amount"), 10)
@@ -178,25 +182,27 @@ class TestPricingRule(unittest.TestCase):
"discount_percentage": 10,
"applicable_for": "Customer Group",
"customer_group": "All Customer Groups",
- "company": "_Test Company"
+ "company": "_Test Company",
}
frappe.get_doc(test_record.copy()).insert()
- args = frappe._dict({
- "item_code": "Mixed Cond Item 1",
- "item_group": "Products",
- "company": "_Test Company",
- "price_list": "_Test Price List",
- "currency": "_Test Currency",
- "doctype": "Sales Order",
- "conversion_rate": 1,
- "price_list_currency": "_Test Currency",
- "plc_conversion_rate": 1,
- "order_type": "Sales",
- "customer": "_Test Customer",
- "customer_group": "_Test Customer Group",
- "name": None
- })
+ args = frappe._dict(
+ {
+ "item_code": "Mixed Cond Item 1",
+ "item_group": "Products",
+ "company": "_Test Company",
+ "price_list": "_Test Price List",
+ "currency": "_Test Currency",
+ "doctype": "Sales Order",
+ "conversion_rate": 1,
+ "price_list_currency": "_Test Currency",
+ "plc_conversion_rate": 1,
+ "order_type": "Sales",
+ "customer": "_Test Customer",
+ "customer_group": "_Test Customer Group",
+ "name": None,
+ }
+ )
details = get_item_details(args)
self.assertEqual(details.get("discount_percentage"), 10)
@@ -206,72 +212,79 @@ class TestPricingRule(unittest.TestCase):
from erpnext.stock.get_item_details import get_item_details
if not frappe.db.exists("Item", "Test Variant PRT"):
- frappe.get_doc({
- "doctype": "Item",
- "item_code": "Test Variant PRT",
- "item_name": "Test Variant PRT",
- "description": "Test Variant PRT",
- "item_group": "_Test Item Group",
- "is_stock_item": 1,
- "variant_of": "_Test Variant Item",
- "default_warehouse": "_Test Warehouse - _TC",
- "stock_uom": "_Test UOM",
- "attributes": [
+ frappe.get_doc(
+ {
+ "doctype": "Item",
+ "item_code": "Test Variant PRT",
+ "item_name": "Test Variant PRT",
+ "description": "Test Variant PRT",
+ "item_group": "_Test Item Group",
+ "is_stock_item": 1,
+ "variant_of": "_Test Variant Item",
+ "default_warehouse": "_Test Warehouse - _TC",
+ "stock_uom": "_Test UOM",
+ "attributes": [{"attribute": "Test Size", "attribute_value": "Medium"}],
+ }
+ ).insert()
+
+ frappe.get_doc(
+ {
+ "doctype": "Pricing Rule",
+ "title": "_Test Pricing Rule 1",
+ "apply_on": "Item Code",
+ "currency": "USD",
+ "items": [
{
- "attribute": "Test Size",
- "attribute_value": "Medium"
+ "item_code": "_Test Variant Item",
}
],
- }).insert()
+ "selling": 1,
+ "rate_or_discount": "Discount Percentage",
+ "rate": 0,
+ "discount_percentage": 7.5,
+ "company": "_Test Company",
+ }
+ ).insert()
- frappe.get_doc({
- "doctype": "Pricing Rule",
- "title": "_Test Pricing Rule 1",
- "apply_on": "Item Code",
- "currency": "USD",
- "items": [{
- "item_code": "_Test Variant Item",
- }],
- "selling": 1,
- "rate_or_discount": "Discount Percentage",
- "rate": 0,
- "discount_percentage": 7.5,
- "company": "_Test Company"
- }).insert()
-
- args = frappe._dict({
- "item_code": "Test Variant PRT",
- "company": "_Test Company",
- "price_list": "_Test Price List",
- "currency": "_Test Currency",
- "doctype": "Sales Order",
- "conversion_rate": 1,
- "price_list_currency": "_Test Currency",
- "plc_conversion_rate": 1,
- "order_type": "Sales",
- "customer": "_Test Customer",
- "name": None
- })
+ args = frappe._dict(
+ {
+ "item_code": "Test Variant PRT",
+ "company": "_Test Company",
+ "price_list": "_Test Price List",
+ "currency": "_Test Currency",
+ "doctype": "Sales Order",
+ "conversion_rate": 1,
+ "price_list_currency": "_Test Currency",
+ "plc_conversion_rate": 1,
+ "order_type": "Sales",
+ "customer": "_Test Customer",
+ "name": None,
+ }
+ )
details = get_item_details(args)
self.assertEqual(details.get("discount_percentage"), 7.5)
# add a new pricing rule for that item code, it should take priority
- frappe.get_doc({
- "doctype": "Pricing Rule",
- "title": "_Test Pricing Rule 2",
- "apply_on": "Item Code",
- "items": [{
- "item_code": "Test Variant PRT",
- }],
- "currency": "USD",
- "selling": 1,
- "rate_or_discount": "Discount Percentage",
- "rate": 0,
- "discount_percentage": 17.5,
- "priority": 1,
- "company": "_Test Company"
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Pricing Rule",
+ "title": "_Test Pricing Rule 2",
+ "apply_on": "Item Code",
+ "items": [
+ {
+ "item_code": "Test Variant PRT",
+ }
+ ],
+ "currency": "USD",
+ "selling": 1,
+ "rate_or_discount": "Discount Percentage",
+ "rate": 0,
+ "discount_percentage": 17.5,
+ "priority": 1,
+ "company": "_Test Company",
+ }
+ ).insert()
details = get_item_details(args)
self.assertEqual(details.get("discount_percentage"), 17.5)
@@ -282,33 +295,31 @@ class TestPricingRule(unittest.TestCase):
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"currency": "USD",
- "items": [{
- "item_code": "_Test Item",
- }],
+ "items": [
+ {
+ "item_code": "_Test Item",
+ }
+ ],
"selling": 1,
"rate_or_discount": "Discount Percentage",
"rate": 0,
"min_qty": 5,
"max_qty": 7,
"discount_percentage": 17.5,
- "company": "_Test Company"
+ "company": "_Test Company",
}
frappe.get_doc(test_record.copy()).insert()
- if not frappe.db.get_value('UOM Conversion Detail',
- {'parent': '_Test Item', 'uom': 'box'}):
- item = frappe.get_doc('Item', '_Test Item')
- item.append('uoms', {
- 'uom': 'Box',
- 'conversion_factor': 5
- })
+ if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Test Item", "uom": "box"}):
+ item = frappe.get_doc("Item", "_Test Item")
+ item.append("uoms", {"uom": "Box", "conversion_factor": 5})
item.save(ignore_permissions=True)
# With pricing rule
so = make_sales_order(item_code="_Test Item", qty=1, uom="Box", do_not_submit=True)
so.items[0].price_list_rate = 100
so.submit()
- so = frappe.get_doc('Sales Order', so.name)
+ so = frappe.get_doc("Sales Order", so.name)
self.assertEqual(so.items[0].discount_percentage, 17.5)
self.assertEqual(so.items[0].rate, 82.5)
@@ -316,13 +327,15 @@ class TestPricingRule(unittest.TestCase):
so = make_sales_order(item_code="_Test Item", qty=2, uom="Box", do_not_submit=True)
so.items[0].price_list_rate = 100
so.submit()
- so = frappe.get_doc('Sales Order', so.name)
+ so = frappe.get_doc("Sales Order", so.name)
self.assertEqual(so.items[0].discount_percentage, 0)
self.assertEqual(so.items[0].rate, 100)
def test_pricing_rule_with_margin_and_discount(self):
- frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
- make_pricing_rule(selling=1, margin_type="Percentage", margin_rate_or_amount=10, discount_percentage=10)
+ frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
+ make_pricing_rule(
+ selling=1, margin_type="Percentage", margin_rate_or_amount=10, discount_percentage=10
+ )
si = create_sales_invoice(do_not_save=True)
si.items[0].price_list_rate = 1000
si.payment_schedule = []
@@ -336,9 +349,14 @@ class TestPricingRule(unittest.TestCase):
self.assertEqual(item.rate, 990)
def test_pricing_rule_with_margin_and_discount_amount(self):
- frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
- make_pricing_rule(selling=1, margin_type="Percentage", margin_rate_or_amount=10,
- rate_or_discount="Discount Amount", discount_amount=110)
+ frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
+ make_pricing_rule(
+ selling=1,
+ margin_type="Percentage",
+ margin_rate_or_amount=10,
+ rate_or_discount="Discount Amount",
+ discount_amount=110,
+ )
si = create_sales_invoice(do_not_save=True)
si.items[0].price_list_rate = 1000
si.payment_schedule = []
@@ -351,15 +369,17 @@ class TestPricingRule(unittest.TestCase):
self.assertEqual(item.rate, 990)
def test_pricing_rule_for_product_discount_on_same_item(self):
- frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
+ frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"currency": "USD",
- "items": [{
- "item_code": "_Test Item",
- }],
+ "items": [
+ {
+ "item_code": "_Test Item",
+ }
+ ],
"selling": 1,
"rate_or_discount": "Discount Percentage",
"rate": 0,
@@ -369,7 +389,7 @@ class TestPricingRule(unittest.TestCase):
"price_or_product_discount": "Product",
"same_item": 1,
"free_qty": 1,
- "company": "_Test Company"
+ "company": "_Test Company",
}
frappe.get_doc(test_record.copy()).insert()
@@ -379,17 +399,18 @@ class TestPricingRule(unittest.TestCase):
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item")
-
def test_pricing_rule_for_product_discount_on_different_item(self):
- frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
+ frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"currency": "USD",
- "items": [{
- "item_code": "_Test Item",
- }],
+ "items": [
+ {
+ "item_code": "_Test Item",
+ }
+ ],
"selling": 1,
"rate_or_discount": "Discount Percentage",
"rate": 0,
@@ -400,7 +421,7 @@ class TestPricingRule(unittest.TestCase):
"same_item": 0,
"free_item": "_Test Item 2",
"free_qty": 1,
- "company": "_Test Company"
+ "company": "_Test Company",
}
frappe.get_doc(test_record.copy()).insert()
@@ -411,15 +432,17 @@ class TestPricingRule(unittest.TestCase):
self.assertEqual(so.items[1].item_code, "_Test Item 2")
def test_cumulative_pricing_rule(self):
- frappe.delete_doc_if_exists('Pricing Rule', '_Test Cumulative Pricing Rule')
+ frappe.delete_doc_if_exists("Pricing Rule", "_Test Cumulative Pricing Rule")
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Cumulative Pricing Rule",
"apply_on": "Item Code",
"currency": "USD",
- "items": [{
- "item_code": "_Test Item",
- }],
+ "items": [
+ {
+ "item_code": "_Test Item",
+ }
+ ],
"is_cumulative": 1,
"selling": 1,
"applicable_for": "Customer",
@@ -432,24 +455,26 @@ class TestPricingRule(unittest.TestCase):
"price_or_product_discount": "Price",
"company": "_Test Company",
"valid_from": frappe.utils.nowdate(),
- "valid_upto": frappe.utils.nowdate()
+ "valid_upto": frappe.utils.nowdate(),
}
frappe.get_doc(test_record.copy()).insert()
- args = frappe._dict({
- "item_code": "_Test Item",
- "company": "_Test Company",
- "price_list": "_Test Price List",
- "currency": "_Test Currency",
- "doctype": "Sales Invoice",
- "conversion_rate": 1,
- "price_list_currency": "_Test Currency",
- "plc_conversion_rate": 1,
- "order_type": "Sales",
- "customer": "_Test Customer",
- "name": None,
- "transaction_date": frappe.utils.nowdate()
- })
+ args = frappe._dict(
+ {
+ "item_code": "_Test Item",
+ "company": "_Test Company",
+ "price_list": "_Test Price List",
+ "currency": "_Test Currency",
+ "doctype": "Sales Invoice",
+ "conversion_rate": 1,
+ "price_list_currency": "_Test Currency",
+ "plc_conversion_rate": 1,
+ "order_type": "Sales",
+ "customer": "_Test Customer",
+ "name": None,
+ "transaction_date": frappe.utils.nowdate(),
+ }
+ )
details = get_item_details(args)
self.assertTrue(details)
@@ -457,8 +482,12 @@ class TestPricingRule(unittest.TestCase):
def test_pricing_rule_for_condition(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
- make_pricing_rule(selling=1, margin_type="Percentage", \
- condition="customer=='_Test Customer 1' and is_return==0", discount_percentage=10)
+ make_pricing_rule(
+ selling=1,
+ margin_type="Percentage",
+ condition="customer=='_Test Customer 1' and is_return==0",
+ discount_percentage=10,
+ )
# Incorrect Customer and Correct is_return value
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 2", is_return=0)
@@ -482,10 +511,20 @@ class TestPricingRule(unittest.TestCase):
self.assertEqual(item.rate, 900)
def test_multiple_pricing_rules(self):
- make_pricing_rule(discount_percentage=20, selling=1, priority=1, apply_multiple_pricing_rules=1,
- title="_Test Pricing Rule 1")
- make_pricing_rule(discount_percentage=10, selling=1, title="_Test Pricing Rule 2", priority=2,
- apply_multiple_pricing_rules=1)
+ make_pricing_rule(
+ discount_percentage=20,
+ selling=1,
+ priority=1,
+ apply_multiple_pricing_rules=1,
+ title="_Test Pricing Rule 1",
+ )
+ make_pricing_rule(
+ discount_percentage=10,
+ selling=1,
+ title="_Test Pricing Rule 2",
+ priority=2,
+ apply_multiple_pricing_rules=1,
+ )
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
self.assertEqual(si.items[0].discount_percentage, 30)
si.delete()
@@ -496,10 +535,21 @@ class TestPricingRule(unittest.TestCase):
def test_multiple_pricing_rules_with_apply_discount_on_discounted_rate(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
- make_pricing_rule(discount_percentage=20, selling=1, priority=1, apply_multiple_pricing_rules=1,
- title="_Test Pricing Rule 1")
- make_pricing_rule(discount_percentage=10, selling=1, priority=2,
- apply_discount_on_rate=1, title="_Test Pricing Rule 2", apply_multiple_pricing_rules=1)
+ make_pricing_rule(
+ discount_percentage=20,
+ selling=1,
+ priority=1,
+ apply_multiple_pricing_rules=1,
+ title="_Test Pricing Rule 1",
+ )
+ make_pricing_rule(
+ discount_percentage=10,
+ selling=1,
+ priority=2,
+ apply_discount_on_rate=1,
+ title="_Test Pricing Rule 2",
+ apply_multiple_pricing_rules=1,
+ )
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
self.assertEqual(si.items[0].discount_percentage, 28)
@@ -516,16 +566,18 @@ class TestPricingRule(unittest.TestCase):
"doctype": "Pricing Rule",
"title": "_Test Water Flask Rule",
"apply_on": "Item Code",
- "items": [{
- "item_code": "Water Flask",
- }],
+ "items": [
+ {
+ "item_code": "Water Flask",
+ }
+ ],
"selling": 1,
"currency": "INR",
"rate_or_discount": "Rate",
"rate": 0,
"margin_type": "Percentage",
"margin_rate_or_amount": 2,
- "company": "_Test Company"
+ "company": "_Test Company",
}
rule = frappe.get_doc(pricing_rule_record)
rule.insert()
@@ -552,9 +604,11 @@ class TestPricingRule(unittest.TestCase):
"doctype": "Pricing Rule",
"title": "_Test Sanitizer Rule",
"apply_on": "Item Code",
- "items": [{
- "item_code": "Test Sanitizer Item",
- }],
+ "items": [
+ {
+ "item_code": "Test Sanitizer Item",
+ }
+ ],
"selling": 1,
"currency": "INR",
"rate_or_discount": "Rate",
@@ -562,63 +616,73 @@ class TestPricingRule(unittest.TestCase):
"priority": 2,
"margin_type": "Percentage",
"margin_rate_or_amount": 0.0,
- "company": "_Test Company"
+ "company": "_Test Company",
}
rule = frappe.get_doc(pricing_rule_record)
- rule.rate_or_discount = 'Rate'
+ rule.rate_or_discount = "Rate"
rule.rate = 100.0
rule.insert()
rule1 = frappe.get_doc(pricing_rule_record)
- rule1.currency = 'USD'
- rule1.rate_or_discount = 'Rate'
+ rule1.currency = "USD"
+ rule1.rate_or_discount = "Rate"
rule1.rate = 2.0
rule1.priority = 1
rule1.insert()
- args = frappe._dict({
- "item_code": "Test Sanitizer Item",
- "company": "_Test Company",
- "price_list": "_Test Price List",
- "currency": "USD",
- "doctype": "Sales Invoice",
- "conversion_rate": 1,
- "price_list_currency": "_Test Currency",
- "plc_conversion_rate": 1,
- "order_type": "Sales",
- "customer": "_Test Customer",
- "name": None,
- "transaction_date": frappe.utils.nowdate()
- })
+ args = frappe._dict(
+ {
+ "item_code": "Test Sanitizer Item",
+ "company": "_Test Company",
+ "price_list": "_Test Price List",
+ "currency": "USD",
+ "doctype": "Sales Invoice",
+ "conversion_rate": 1,
+ "price_list_currency": "_Test Currency",
+ "plc_conversion_rate": 1,
+ "order_type": "Sales",
+ "customer": "_Test Customer",
+ "name": None,
+ "transaction_date": frappe.utils.nowdate(),
+ }
+ )
details = get_item_details(args)
self.assertEqual(details.price_list_rate, 2.0)
-
- args = frappe._dict({
- "item_code": "Test Sanitizer Item",
- "company": "_Test Company",
- "price_list": "_Test Price List",
- "currency": "INR",
- "doctype": "Sales Invoice",
- "conversion_rate": 1,
- "price_list_currency": "_Test Currency",
- "plc_conversion_rate": 1,
- "order_type": "Sales",
- "customer": "_Test Customer",
- "name": None,
- "transaction_date": frappe.utils.nowdate()
- })
+ args = frappe._dict(
+ {
+ "item_code": "Test Sanitizer Item",
+ "company": "_Test Company",
+ "price_list": "_Test Price List",
+ "currency": "INR",
+ "doctype": "Sales Invoice",
+ "conversion_rate": 1,
+ "price_list_currency": "_Test Currency",
+ "plc_conversion_rate": 1,
+ "order_type": "Sales",
+ "customer": "_Test Customer",
+ "name": None,
+ "transaction_date": frappe.utils.nowdate(),
+ }
+ )
details = get_item_details(args)
self.assertEqual(details.price_list_rate, 100.0)
def test_pricing_rule_for_transaction(self):
make_item("Water Flask 1")
- frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
- make_pricing_rule(selling=1, min_qty=5, price_or_product_discount="Product",
- apply_on="Transaction", free_item="Water Flask 1", free_qty=1, free_item_rate=10)
+ frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
+ make_pricing_rule(
+ selling=1,
+ min_qty=5,
+ price_or_product_discount="Product",
+ apply_on="Transaction",
+ free_item="Water Flask 1",
+ free_qty=1,
+ free_item_rate=10,
+ )
si = create_sales_invoice(qty=5, do_not_submit=True)
self.assertEqual(len(si.items), 2)
@@ -631,10 +695,22 @@ class TestPricingRule(unittest.TestCase):
doc.delete()
def test_multiple_pricing_rules_with_min_qty(self):
- make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4,
- apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1")
- make_pricing_rule(discount_percentage=10, selling=1, priority=2, min_qty=4,
- apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 2")
+ make_pricing_rule(
+ discount_percentage=20,
+ selling=1,
+ priority=1,
+ min_qty=4,
+ apply_multiple_pricing_rules=1,
+ title="_Test Pricing Rule with Min Qty - 1",
+ )
+ make_pricing_rule(
+ discount_percentage=10,
+ selling=1,
+ priority=2,
+ min_qty=4,
+ apply_multiple_pricing_rules=1,
+ title="_Test Pricing Rule with Min Qty - 2",
+ )
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD")
item = si.items[0]
@@ -659,14 +735,16 @@ class TestPricingRule(unittest.TestCase):
"title": "_Test Water Flask Rule",
"apply_on": "Item Code",
"price_or_product_discount": "Price",
- "items": [{
- "item_code": "Water Flask",
- }],
+ "items": [
+ {
+ "item_code": "Water Flask",
+ }
+ ],
"selling": 1,
"currency": "INR",
"rate_or_discount": "Discount Percentage",
"discount_percentage": 20,
- "company": "_Test Company"
+ "company": "_Test Company",
}
rule = frappe.get_doc(pricing_rule_record)
rule.insert()
@@ -693,64 +771,75 @@ class TestPricingRule(unittest.TestCase):
test_dependencies = ["Campaign"]
+
def make_pricing_rule(**args):
args = frappe._dict(args)
- doc = frappe.get_doc({
- "doctype": "Pricing Rule",
- "title": args.title or "_Test Pricing Rule",
- "company": args.company or "_Test Company",
- "apply_on": args.apply_on or "Item Code",
- "applicable_for": args.applicable_for,
- "selling": args.selling or 0,
- "currency": "USD",
- "apply_discount_on_rate": args.apply_discount_on_rate or 0,
- "buying": args.buying or 0,
- "min_qty": args.min_qty or 0.0,
- "max_qty": args.max_qty or 0.0,
- "rate_or_discount": args.rate_or_discount or "Discount Percentage",
- "discount_percentage": args.discount_percentage or 0.0,
- "rate": args.rate or 0.0,
- "margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
- "condition": args.condition or '',
- "priority": args.priority or 1,
- "discount_amount": args.discount_amount or 0.0,
- "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
- })
+ doc = frappe.get_doc(
+ {
+ "doctype": "Pricing Rule",
+ "title": args.title or "_Test Pricing Rule",
+ "company": args.company or "_Test Company",
+ "apply_on": args.apply_on or "Item Code",
+ "applicable_for": args.applicable_for,
+ "selling": args.selling or 0,
+ "currency": "USD",
+ "apply_discount_on_rate": args.apply_discount_on_rate or 0,
+ "buying": args.buying or 0,
+ "min_qty": args.min_qty or 0.0,
+ "max_qty": args.max_qty or 0.0,
+ "rate_or_discount": args.rate_or_discount or "Discount Percentage",
+ "discount_percentage": args.discount_percentage or 0.0,
+ "rate": args.rate or 0.0,
+ "margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
+ "condition": args.condition or "",
+ "priority": args.priority or 1,
+ "discount_amount": args.discount_amount or 0.0,
+ "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0,
+ }
+ )
- for field in ["free_item", "free_qty", "free_item_rate", "priority",
- "margin_type", "price_or_product_discount"]:
+ for field in [
+ "free_item",
+ "free_qty",
+ "free_item_rate",
+ "priority",
+ "margin_type",
+ "price_or_product_discount",
+ ]:
if args.get(field):
doc.set(field, args.get(field))
- apply_on = doc.apply_on.replace(' ', '_').lower()
- child_table = {'Item Code': 'items', 'Item Group': 'item_groups', 'Brand': 'brands'}
+ apply_on = doc.apply_on.replace(" ", "_").lower()
+ child_table = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}
if doc.apply_on != "Transaction":
- doc.append(child_table.get(doc.apply_on), {
- apply_on: args.get(apply_on) or "_Test Item"
- })
+ doc.append(child_table.get(doc.apply_on), {apply_on: args.get(apply_on) or "_Test Item"})
doc.insert(ignore_permissions=True)
if args.get(apply_on) and apply_on != "item_code":
doc.db_set(apply_on, args.get(apply_on))
- applicable_for = doc.applicable_for.replace(' ', '_').lower()
+ applicable_for = doc.applicable_for.replace(" ", "_").lower()
if args.get(applicable_for):
doc.db_set(applicable_for, args.get(applicable_for))
return doc
+
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()
+ 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():
- for doctype in ["Pricing Rule", "Pricing Rule Item Code",
- "Pricing Rule Item Group", "Pricing Rule Brand"]:
+ for doctype in [
+ "Pricing Rule",
+ "Pricing Rule Item Code",
+ "Pricing Rule Item Group",
+ "Pricing Rule Brand",
+ ]:
frappe.db.sql("delete from `tab{0}`".format(doctype))
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index 7792590c9c7..70926cfbd72 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -16,22 +16,21 @@ from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
from erpnext.stock.get_item_details import get_conversion_factor
-class MultiplePricingRuleConflict(frappe.ValidationError): pass
+class MultiplePricingRuleConflict(frappe.ValidationError):
+ pass
+
+
+apply_on_table = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}
-apply_on_table = {
- 'Item Code': 'items',
- 'Item Group': 'item_groups',
- 'Brand': 'brands'
-}
def get_pricing_rules(args, doc=None):
pricing_rules = []
- values = {}
+ values = {}
- if not frappe.db.exists('Pricing Rule', {'disable': 0, args.transaction_type: 1}):
+ if not frappe.db.exists("Pricing Rule", {"disable": 0, args.transaction_type: 1}):
return
- for apply_on in ['Item Code', 'Item Group', 'Brand']:
+ for apply_on in ["Item Code", "Item Group", "Brand"]:
pricing_rules.extend(_get_pricing_rules(apply_on, args, values))
if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):
break
@@ -40,7 +39,8 @@ def get_pricing_rules(args, doc=None):
pricing_rules = filter_pricing_rule_based_on_condition(pricing_rules, doc)
- if not pricing_rules: return []
+ if not pricing_rules:
+ return []
if apply_multiple_pricing_rules(pricing_rules):
pricing_rules = sorted_by_priority(pricing_rules, args, doc)
@@ -56,6 +56,7 @@ def get_pricing_rules(args, doc=None):
return rules
+
def sorted_by_priority(pricing_rules, args, doc=None):
# If more than one pricing rules, then sort by priority
pricing_rules_list = []
@@ -64,10 +65,10 @@ def sorted_by_priority(pricing_rules, args, doc=None):
for pricing_rule in pricing_rules:
pricing_rule = filter_pricing_rules(args, pricing_rule, doc)
if pricing_rule:
- if not pricing_rule.get('priority'):
- pricing_rule['priority'] = 1
+ if not pricing_rule.get("priority"):
+ pricing_rule["priority"] = 1
- if pricing_rule.get('apply_multiple_pricing_rules'):
+ if pricing_rule.get("apply_multiple_pricing_rules"):
pricing_rule_dict.setdefault(cint(pricing_rule.get("priority")), []).append(pricing_rule)
for key in sorted(pricing_rule_dict):
@@ -75,6 +76,7 @@ def sorted_by_priority(pricing_rules, args, doc=None):
return pricing_rules_list
+
def filter_pricing_rule_based_on_condition(pricing_rules, doc=None):
filtered_pricing_rules = []
if doc:
@@ -92,40 +94,48 @@ def filter_pricing_rule_based_on_condition(pricing_rules, doc=None):
return filtered_pricing_rules
+
def _get_pricing_rules(apply_on, args, values):
apply_on_field = frappe.scrub(apply_on)
- if not args.get(apply_on_field): return []
+ if not args.get(apply_on_field):
+ return []
- child_doc = '`tabPricing Rule {0}`'.format(apply_on)
+ child_doc = "`tabPricing Rule {0}`".format(apply_on)
conditions = item_variant_condition = item_conditions = ""
values[apply_on_field] = args.get(apply_on_field)
- if apply_on_field in ['item_code', 'brand']:
- item_conditions = "{child_doc}.{apply_on_field}= %({apply_on_field})s".format(child_doc=child_doc,
- apply_on_field = apply_on_field)
+ if apply_on_field in ["item_code", "brand"]:
+ item_conditions = "{child_doc}.{apply_on_field}= %({apply_on_field})s".format(
+ child_doc=child_doc, apply_on_field=apply_on_field
+ )
- if apply_on_field == 'item_code':
+ if apply_on_field == "item_code":
if "variant_of" not in args:
args.variant_of = frappe.get_cached_value("Item", args.item_code, "variant_of")
if args.variant_of:
- item_variant_condition = ' or {child_doc}.item_code=%(variant_of)s '.format(child_doc=child_doc)
- values['variant_of'] = args.variant_of
- elif apply_on_field == 'item_group':
+ item_variant_condition = " or {child_doc}.item_code=%(variant_of)s ".format(
+ child_doc=child_doc
+ )
+ values["variant_of"] = args.variant_of
+ elif apply_on_field == "item_group":
item_conditions = _get_tree_conditions(args, "Item Group", child_doc, False)
conditions += get_other_conditions(conditions, values, args)
- warehouse_conditions = _get_tree_conditions(args, "Warehouse", '`tabPricing Rule`')
+ warehouse_conditions = _get_tree_conditions(args, "Warehouse", "`tabPricing Rule`")
if warehouse_conditions:
warehouse_conditions = " and {0}".format(warehouse_conditions)
- if not args.price_list: args.price_list = None
+ if not args.price_list:
+ args.price_list = None
conditions += " and ifnull(`tabPricing Rule`.for_price_list, '') in (%(price_list)s, '')"
values["price_list"] = args.get("price_list")
- pricing_rules = frappe.db.sql("""select `tabPricing Rule`.*,
+ pricing_rules = (
+ frappe.db.sql(
+ """select `tabPricing Rule`.*,
{child_doc}.{apply_on_field}, {child_doc}.uom
from `tabPricing Rule`, {child_doc}
where ({item_conditions} or (`tabPricing Rule`.apply_rule_on_other is not null
@@ -135,25 +145,35 @@ def _get_pricing_rules(apply_on, args, values):
`tabPricing Rule`.{transaction_type} = 1 {warehouse_cond} {conditions}
order by `tabPricing Rule`.priority desc,
`tabPricing Rule`.name desc""".format(
- child_doc = child_doc,
- apply_on_field = apply_on_field,
- item_conditions = item_conditions,
- item_variant_condition = item_variant_condition,
- transaction_type = args.transaction_type,
- warehouse_cond = warehouse_conditions,
- apply_on_other_field = "other_{0}".format(apply_on_field),
- conditions = conditions), values, as_dict=1) or []
+ child_doc=child_doc,
+ apply_on_field=apply_on_field,
+ item_conditions=item_conditions,
+ item_variant_condition=item_variant_condition,
+ transaction_type=args.transaction_type,
+ warehouse_cond=warehouse_conditions,
+ apply_on_other_field="other_{0}".format(apply_on_field),
+ conditions=conditions,
+ ),
+ values,
+ as_dict=1,
+ )
+ or []
+ )
return pricing_rules
-def apply_multiple_pricing_rules(pricing_rules):
- apply_multiple_rule = [d.apply_multiple_pricing_rules
- for d in pricing_rules if d.apply_multiple_pricing_rules]
- if not apply_multiple_rule: return False
+def apply_multiple_pricing_rules(pricing_rules):
+ apply_multiple_rule = [
+ d.apply_multiple_pricing_rules for d in pricing_rules if d.apply_multiple_pricing_rules
+ ]
+
+ if not apply_multiple_rule:
+ return False
return True
+
def _get_tree_conditions(args, parenttype, table, allow_blank=True):
field = frappe.scrub(parenttype)
condition = ""
@@ -169,28 +189,37 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
except TypeError:
frappe.throw(_("Invalid {0}").format(args.get(field)))
- parent_groups = frappe.db.sql_list("""select name from `tab%s`
- where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
+ parent_groups = frappe.db.sql_list(
+ """select name from `tab%s`
+ where lft<=%s and rgt>=%s"""
+ % (parenttype, "%s", "%s"),
+ (lft, rgt),
+ )
if parenttype in ["Customer Group", "Item Group", "Territory"]:
parent_field = "parent_{0}".format(frappe.scrub(parenttype))
- root_name = frappe.db.get_list(parenttype,
- {"is_group": 1, parent_field: ("is", "not set")}, "name", as_list=1, ignore_permissions=True)
+ root_name = frappe.db.get_list(
+ parenttype,
+ {"is_group": 1, parent_field: ("is", "not set")},
+ "name",
+ as_list=1,
+ ignore_permissions=True,
+ )
if root_name and root_name[0][0]:
parent_groups.append(root_name[0][0])
if parent_groups:
- if allow_blank: parent_groups.append('')
+ if allow_blank:
+ parent_groups.append("")
condition = "ifnull({table}.{field}, '') in ({parent_groups})".format(
- table=table,
- field=field,
- parent_groups=", ".join(frappe.db.escape(d) for d in parent_groups)
+ table=table, field=field, parent_groups=", ".join(frappe.db.escape(d) for d in parent_groups)
)
frappe.flags.tree_conditions[key] = condition
return condition
+
def get_other_conditions(conditions, values, args):
for field in ["company", "customer", "supplier", "campaign", "sales_partner"]:
if args.get(field):
@@ -200,17 +229,18 @@ def get_other_conditions(conditions, values, args):
conditions += " and ifnull(`tabPricing Rule`.{0}, '') = ''".format(field)
for parenttype in ["Customer Group", "Territory", "Supplier Group"]:
- group_condition = _get_tree_conditions(args, parenttype, '`tabPricing Rule`')
+ group_condition = _get_tree_conditions(args, parenttype, "`tabPricing Rule`")
if group_condition:
conditions += " and " + group_condition
if args.get("transaction_date"):
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')
and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')"""
- values['transaction_date'] = args.get('transaction_date')
+ values["transaction_date"] = args.get("transaction_date")
return conditions
+
def filter_pricing_rules(args, pricing_rules, doc=None):
if not isinstance(pricing_rules, list):
pricing_rules = [pricing_rules]
@@ -219,15 +249,16 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
# filter for qty
if pricing_rules:
- stock_qty = flt(args.get('stock_qty'))
- amount = flt(args.get('price_list_rate')) * flt(args.get('qty'))
+ stock_qty = flt(args.get("stock_qty"))
+ amount = flt(args.get("price_list_rate")) * flt(args.get("qty"))
if pricing_rules[0].apply_rule_on_other:
field = frappe.scrub(pricing_rules[0].apply_rule_on_other)
- if (field and pricing_rules[0].get('other_' + field) != args.get(field)): return
+ if field and pricing_rules[0].get("other_" + field) != args.get(field):
+ return
- pr_doc = frappe.get_cached_doc('Pricing Rule', pricing_rules[0].name)
+ pr_doc = frappe.get_cached_doc("Pricing Rule", pricing_rules[0].name)
if pricing_rules[0].mixed_conditions and doc:
stock_qty, amount, items = get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args)
@@ -235,7 +266,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
pricing_rule_args.apply_rule_on_other_items = items
elif pricing_rules[0].is_cumulative:
- items = [args.get(frappe.scrub(pr_doc.get('apply_on')))]
+ items = [args.get(frappe.scrub(pr_doc.get("apply_on")))]
data = get_qty_amount_data_for_cumulative(pr_doc, args, items)
if data:
@@ -249,13 +280,15 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
if not pricing_rules:
for d in original_pricing_rule:
- if not d.threshold_percentage: continue
+ if not d.threshold_percentage:
+ continue
- msg = validate_quantity_and_amount_for_suggestion(d, stock_qty,
- amount, args.get('item_code'), args.get('transaction_type'))
+ msg = validate_quantity_and_amount_for_suggestion(
+ d, stock_qty, amount, args.get("item_code"), args.get("transaction_type")
+ )
if msg:
- return {'suggestion': msg, 'item_code': args.get('item_code')}
+ return {"suggestion": msg, "item_code": args.get("item_code")}
# add variant_of property in pricing rule
for p in pricing_rules:
@@ -265,7 +298,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
p.variant_of = None
if len(pricing_rules) > 1:
- filtered_rules = list(filter(lambda x: x.currency==args.get('currency'), pricing_rules))
+ filtered_rules = list(filter(lambda x: x.currency == args.get("currency"), pricing_rules))
if filtered_rules:
pricing_rules = filtered_rules
@@ -273,7 +306,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
if pricing_rules:
max_priority = max(cint(p.priority) for p in pricing_rules)
if max_priority:
- pricing_rules = list(filter(lambda x: cint(x.priority)==max_priority, pricing_rules))
+ pricing_rules = list(filter(lambda x: cint(x.priority) == max_priority, pricing_rules))
if pricing_rules and not isinstance(pricing_rules, list):
pricing_rules = list(pricing_rules)
@@ -281,42 +314,61 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
if len(pricing_rules) > 1:
rate_or_discount = list(set(d.rate_or_discount for d in pricing_rules))
if len(rate_or_discount) == 1 and rate_or_discount[0] == "Discount Percentage":
- pricing_rules = list(filter(lambda x: x.for_price_list==args.price_list, pricing_rules)) \
- or pricing_rules
+ pricing_rules = (
+ list(filter(lambda x: x.for_price_list == args.price_list, pricing_rules)) or pricing_rules
+ )
if len(pricing_rules) > 1 and not args.for_shopping_cart:
- frappe.throw(_("Multiple Price Rules exists with same criteria, please resolve conflict by assigning priority. Price Rules: {0}")
- .format("\n".join(d.name for d in pricing_rules)), MultiplePricingRuleConflict)
+ frappe.throw(
+ _(
+ "Multiple Price Rules exists with same criteria, please resolve conflict by assigning priority. Price Rules: {0}"
+ ).format("\n".join(d.name for d in pricing_rules)),
+ MultiplePricingRuleConflict,
+ )
elif pricing_rules:
return pricing_rules[0]
-def validate_quantity_and_amount_for_suggestion(args, qty, amount, item_code, transaction_type):
- fieldname, msg = '', ''
- type_of_transaction = 'purchase' if transaction_type == 'buying' else 'sale'
- for field, value in {'min_qty': qty, 'min_amt': amount}.items():
- if (args.get(field) and value < args.get(field)
- and (args.get(field) - cint(args.get(field) * args.threshold_percentage * 0.01)) <= value):
+def validate_quantity_and_amount_for_suggestion(args, qty, amount, item_code, transaction_type):
+ fieldname, msg = "", ""
+ type_of_transaction = "purchase" if transaction_type == "buying" else "sale"
+
+ for field, value in {"min_qty": qty, "min_amt": amount}.items():
+ if (
+ args.get(field)
+ and value < args.get(field)
+ and (args.get(field) - cint(args.get(field) * args.threshold_percentage * 0.01)) <= value
+ ):
fieldname = field
- for field, value in {'max_qty': qty, 'max_amt': amount}.items():
- if (args.get(field) and value > args.get(field)
- and (args.get(field) + cint(args.get(field) * args.threshold_percentage * 0.01)) >= value):
+ for field, value in {"max_qty": qty, "max_amt": amount}.items():
+ if (
+ args.get(field)
+ and value > args.get(field)
+ and (args.get(field) + cint(args.get(field) * args.threshold_percentage * 0.01)) >= value
+ ):
fieldname = field
if fieldname:
- msg = (_("If you {0} {1} quantities of the item {2}, the scheme {3} will be applied on the item.")
- .format(type_of_transaction, args.get(fieldname), bold(item_code), bold(args.rule_description)))
+ msg = _(
+ "If you {0} {1} quantities of the item {2}, the scheme {3} will be applied on the item."
+ ).format(
+ type_of_transaction, args.get(fieldname), bold(item_code), bold(args.rule_description)
+ )
- if fieldname in ['min_amt', 'max_amt']:
- msg = (_("If you {0} {1} worth item {2}, the scheme {3} will be applied on the item.")
- .format(type_of_transaction, fmt_money(args.get(fieldname), currency=args.get("currency")),
- bold(item_code), bold(args.rule_description)))
+ if fieldname in ["min_amt", "max_amt"]:
+ msg = _("If you {0} {1} worth item {2}, the scheme {3} will be applied on the item.").format(
+ type_of_transaction,
+ fmt_money(args.get(fieldname), currency=args.get("currency")),
+ bold(item_code),
+ bold(args.rule_description),
+ )
frappe.msgprint(msg)
return msg
+
def filter_pricing_rules_for_qty_amount(qty, rate, pricing_rules, args=None):
rules = []
@@ -327,16 +379,19 @@ def filter_pricing_rules_for_qty_amount(qty, rate, pricing_rules, args=None):
if rule.get("uom"):
conversion_factor = get_conversion_factor(rule.item_code, rule.uom).get("conversion_factor", 1)
- if (flt(qty) >= (flt(rule.min_qty) * conversion_factor)
- and (flt(qty)<= (rule.max_qty * conversion_factor) if rule.max_qty else True)):
+ if flt(qty) >= (flt(rule.min_qty) * conversion_factor) and (
+ flt(qty) <= (rule.max_qty * conversion_factor) if rule.max_qty else True
+ ):
status = True
# if user has created item price against the transaction UOM
if args and rule.get("uom") == args.get("uom"):
conversion_factor = 1.0
- if status and (flt(rate) >= (flt(rule.min_amt) * conversion_factor)
- and (flt(rate)<= (rule.max_amt * conversion_factor) if rule.max_amt else True)):
+ if status and (
+ flt(rate) >= (flt(rule.min_amt) * conversion_factor)
+ and (flt(rate) <= (rule.max_amt * conversion_factor) if rule.max_amt else True)
+ ):
status = True
else:
status = False
@@ -346,6 +401,7 @@ def filter_pricing_rules_for_qty_amount(qty, rate, pricing_rules, args=None):
return rules
+
def if_all_rules_same(pricing_rules, fields):
all_rules_same = True
val = [pricing_rules[0].get(k) for k in fields]
@@ -356,30 +412,34 @@ def if_all_rules_same(pricing_rules, fields):
return all_rules_same
+
def apply_internal_priority(pricing_rules, field_set, args):
filtered_rules = []
for field in field_set:
if args.get(field):
# filter function always returns a filter object even if empty
# list conversion is necessary to check for an empty result
- filtered_rules = list(filter(lambda x: x.get(field)==args.get(field), pricing_rules))
- if filtered_rules: break
+ filtered_rules = list(filter(lambda x: x.get(field) == args.get(field), pricing_rules))
+ if filtered_rules:
+ break
return filtered_rules or pricing_rules
+
def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args):
sum_qty, sum_amt = [0, 0]
items = get_pricing_rule_items(pr_doc) or []
- apply_on = frappe.scrub(pr_doc.get('apply_on'))
+ apply_on = frappe.scrub(pr_doc.get("apply_on"))
if items and doc.get("items"):
- for row in doc.get('items'):
- if (row.get(apply_on) or args.get(apply_on)) not in items: continue
+ for row in doc.get("items"):
+ if (row.get(apply_on) or args.get(apply_on)) not in items:
+ continue
if pr_doc.mixed_conditions:
- amt = args.get('qty') * args.get("price_list_rate")
+ amt = args.get("qty") * args.get("price_list_rate")
if args.get("item_code") != row.get("item_code"):
- amt = flt(row.get('qty')) * flt(row.get("price_list_rate") or args.get("rate"))
+ amt = flt(row.get("qty")) * flt(row.get("price_list_rate") or args.get("rate"))
sum_qty += flt(row.get("stock_qty")) or flt(args.get("stock_qty")) or flt(args.get("qty"))
sum_amt += amt
@@ -393,28 +453,33 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args):
return sum_qty, sum_amt, items
+
def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules):
items = get_pricing_rule_items(pr_doc)
for row in doc.items:
if row.get(frappe.scrub(pr_doc.apply_rule_on_other)) in items:
- pricing_rules = filter_pricing_rules_for_qty_amount(row.get("stock_qty"),
- row.get("amount"), pricing_rules, row)
+ pricing_rules = filter_pricing_rules_for_qty_amount(
+ row.get("stock_qty"), row.get("amount"), pricing_rules, row
+ )
if pricing_rules and pricing_rules[0]:
pricing_rules[0].apply_rule_on_other_items = items
return pricing_rules
+
def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None):
if items is None:
items = []
sum_qty, sum_amt = [0, 0]
- doctype = doc.get('parenttype') or doc.doctype
+ doctype = doc.get("parenttype") or doc.doctype
- date_field = 'transaction_date' if frappe.get_meta(doctype).has_field('transaction_date') else 'posting_date'
+ date_field = (
+ "transaction_date" if frappe.get_meta(doctype).has_field("transaction_date") else "posting_date"
+ )
- child_doctype = '{0} Item'.format(doctype)
- apply_on = frappe.scrub(pr_doc.get('apply_on'))
+ child_doctype = "{0} Item".format(doctype)
+ apply_on = frappe.scrub(pr_doc.get("apply_on"))
values = [pr_doc.valid_from, pr_doc.valid_upto]
condition = ""
@@ -423,72 +488,86 @@ def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None):
warehouses = get_child_warehouses(pr_doc.warehouse)
condition += """ and `tab{child_doc}`.warehouse in ({warehouses})
- """.format(child_doc=child_doctype, warehouses = ','.join(['%s'] * len(warehouses)))
+ """.format(
+ child_doc=child_doctype, warehouses=",".join(["%s"] * len(warehouses))
+ )
values.extend(warehouses)
if items:
- condition = " and `tab{child_doc}`.{apply_on} in ({items})".format(child_doc = child_doctype,
- apply_on = apply_on, items = ','.join(['%s'] * len(items)))
+ condition = " and `tab{child_doc}`.{apply_on} in ({items})".format(
+ child_doc=child_doctype, apply_on=apply_on, items=",".join(["%s"] * len(items))
+ )
values.extend(items)
- data_set = frappe.db.sql(""" SELECT `tab{child_doc}`.stock_qty,
+ data_set = frappe.db.sql(
+ """ SELECT `tab{child_doc}`.stock_qty,
`tab{child_doc}`.amount
FROM `tab{child_doc}`, `tab{parent_doc}`
WHERE
`tab{child_doc}`.parent = `tab{parent_doc}`.name and `tab{parent_doc}`.{date_field}
between %s and %s and `tab{parent_doc}`.docstatus = 1
{condition} group by `tab{child_doc}`.name
- """.format(parent_doc = doctype,
- child_doc = child_doctype,
- condition = condition,
- date_field = date_field
- ), tuple(values), as_dict=1)
+ """.format(
+ parent_doc=doctype, child_doc=child_doctype, condition=condition, date_field=date_field
+ ),
+ tuple(values),
+ as_dict=1,
+ )
for data in data_set:
- sum_qty += data.get('stock_qty')
- sum_amt += data.get('amount')
+ sum_qty += data.get("stock_qty")
+ sum_amt += data.get("amount")
return [sum_qty, sum_amt]
+
def apply_pricing_rule_on_transaction(doc):
conditions = "apply_on = 'Transaction'"
values = {}
conditions = get_other_conditions(conditions, values, doc)
- pricing_rules = frappe.db.sql(""" Select `tabPricing Rule`.* from `tabPricing Rule`
+ pricing_rules = frappe.db.sql(
+ """ Select `tabPricing Rule`.* from `tabPricing Rule`
where {conditions} and `tabPricing Rule`.disable = 0
- """.format(conditions = conditions), values, as_dict=1)
+ """.format(
+ conditions=conditions
+ ),
+ values,
+ as_dict=1,
+ )
if pricing_rules:
- pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty,
- doc.total, pricing_rules)
+ pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, doc.total, pricing_rules)
if not pricing_rules:
remove_free_item(doc)
for d in pricing_rules:
- if d.price_or_product_discount == 'Price':
+ if d.price_or_product_discount == "Price":
if d.apply_discount_on:
- doc.set('apply_discount_on', d.apply_discount_on)
+ doc.set("apply_discount_on", d.apply_discount_on)
- for field in ['additional_discount_percentage', 'discount_amount']:
- pr_field = ('discount_percentage'
- if field == 'additional_discount_percentage' else field)
+ for field in ["additional_discount_percentage", "discount_amount"]:
+ pr_field = "discount_percentage" if field == "additional_discount_percentage" else field
- if not d.get(pr_field): continue
+ if not d.get(pr_field):
+ continue
- if d.validate_applied_rule and doc.get(field) is not None and doc.get(field) < d.get(pr_field):
- frappe.msgprint(_("User has not applied rule on the invoice {0}")
- .format(doc.name))
+ if (
+ d.validate_applied_rule and doc.get(field) is not None and doc.get(field) < d.get(pr_field)
+ ):
+ frappe.msgprint(_("User has not applied rule on the invoice {0}").format(doc.name))
else:
if not d.coupon_code_based:
doc.set(field, d.get(pr_field))
- elif doc.get('coupon_code'):
+ elif doc.get("coupon_code"):
# coupon code based pricing rule
- coupon_code_pricing_rule = frappe.db.get_value('Coupon Code', doc.get('coupon_code'), 'pricing_rule')
+ coupon_code_pricing_rule = frappe.db.get_value(
+ "Coupon Code", doc.get("coupon_code"), "pricing_rule"
+ )
if coupon_code_pricing_rule == d.name:
# if selected coupon code is linked with pricing rule
doc.set(field, d.get(pr_field))
@@ -500,83 +579,93 @@ def apply_pricing_rule_on_transaction(doc):
doc.set(field, 0)
doc.calculate_taxes_and_totals()
- elif d.price_or_product_discount == 'Product':
- item_details = frappe._dict({'parenttype': doc.doctype, 'free_item_data': []})
+ elif d.price_or_product_discount == "Product":
+ item_details = frappe._dict({"parenttype": doc.doctype, "free_item_data": []})
get_product_discount_rule(d, item_details, doc=doc)
apply_pricing_rule_for_free_items(doc, item_details.free_item_data)
doc.set_missing_values()
doc.calculate_taxes_and_totals()
+
def remove_free_item(doc):
for d in doc.items:
if d.is_free_item:
doc.remove(d)
+
def get_applied_pricing_rules(pricing_rules):
if pricing_rules:
- if pricing_rules.startswith('['):
+ if pricing_rules.startswith("["):
return json.loads(pricing_rules)
else:
- return pricing_rules.split(',')
+ return pricing_rules.split(",")
return []
+
def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
free_item = pricing_rule.free_item
- if pricing_rule.same_item and pricing_rule.get("apply_on") != 'Transaction':
+ if pricing_rule.same_item and pricing_rule.get("apply_on") != "Transaction":
free_item = item_details.item_code or args.item_code
if not free_item:
- frappe.throw(_("Free item not set in the pricing rule {0}")
- .format(get_link_to_form("Pricing Rule", pricing_rule.name)))
+ frappe.throw(
+ _("Free item not set in the pricing rule {0}").format(
+ get_link_to_form("Pricing Rule", pricing_rule.name)
+ )
+ )
qty = pricing_rule.free_qty or 1
if pricing_rule.is_recursive:
- transaction_qty = args.get('qty') if args else doc.total_qty
+ transaction_qty = args.get("qty") if args else doc.total_qty
if transaction_qty:
qty = flt(transaction_qty) * qty
free_item_data_args = {
- 'item_code': free_item,
- 'qty': qty,
- 'pricing_rules': pricing_rule.name,
- 'rate': pricing_rule.free_item_rate or 0,
- 'price_list_rate': pricing_rule.free_item_rate or 0,
- 'is_free_item': 1
+ "item_code": free_item,
+ "qty": qty,
+ "pricing_rules": pricing_rule.name,
+ "rate": pricing_rule.free_item_rate or 0,
+ "price_list_rate": pricing_rule.free_item_rate or 0,
+ "is_free_item": 1,
}
- item_data = frappe.get_cached_value('Item', free_item, ['item_name',
- 'description', 'stock_uom'], as_dict=1)
+ item_data = frappe.get_cached_value(
+ "Item", free_item, ["item_name", "description", "stock_uom"], as_dict=1
+ )
free_item_data_args.update(item_data)
- free_item_data_args['uom'] = pricing_rule.free_item_uom or item_data.stock_uom
- free_item_data_args['conversion_factor'] = get_conversion_factor(free_item,
- free_item_data_args['uom']).get("conversion_factor", 1)
+ free_item_data_args["uom"] = pricing_rule.free_item_uom or item_data.stock_uom
+ free_item_data_args["conversion_factor"] = get_conversion_factor(
+ free_item, free_item_data_args["uom"]
+ ).get("conversion_factor", 1)
- if item_details.get("parenttype") == 'Purchase Order':
- free_item_data_args['schedule_date'] = doc.schedule_date if doc else today()
+ if item_details.get("parenttype") == "Purchase Order":
+ free_item_data_args["schedule_date"] = doc.schedule_date if doc else today()
- if item_details.get("parenttype") == 'Sales Order':
- free_item_data_args['delivery_date'] = doc.delivery_date if doc else today()
+ if item_details.get("parenttype") == "Sales Order":
+ free_item_data_args["delivery_date"] = doc.delivery_date if doc else today()
item_details.free_item_data.append(free_item_data_args)
+
def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False):
if pricing_rule_args:
items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item)
for args in pricing_rule_args:
- if not items or (args.get('item_code'), args.get('pricing_rules')) not in items:
- doc.append('items', args)
+ if not items or (args.get("item_code"), args.get("pricing_rules")) not in items:
+ doc.append("items", args)
+
def get_pricing_rule_items(pr_doc):
apply_on_data = []
- apply_on = frappe.scrub(pr_doc.get('apply_on'))
+ apply_on = frappe.scrub(pr_doc.get("apply_on"))
- pricing_rule_apply_on = apply_on_table.get(pr_doc.get('apply_on'))
+ pricing_rule_apply_on = apply_on_table.get(pr_doc.get("apply_on"))
for d in pr_doc.get(pricing_rule_apply_on):
- if apply_on == 'item_group':
+ if apply_on == "item_group":
apply_on_data.extend(get_child_item_groups(d.get(apply_on)))
else:
apply_on_data.append(d.get(apply_on))
@@ -587,6 +676,7 @@ def get_pricing_rule_items(pr_doc):
return list(set(apply_on_data))
+
def validate_coupon_code(coupon_name):
coupon = frappe.get_doc("Coupon Code", coupon_name)
@@ -599,16 +689,21 @@ def validate_coupon_code(coupon_name):
elif coupon.used >= coupon.maximum_use:
frappe.throw(_("Sorry, this coupon code is no longer valid"))
-def update_coupon_code_count(coupon_name,transaction_type):
- coupon=frappe.get_doc("Coupon Code",coupon_name)
+
+def update_coupon_code_count(coupon_name, transaction_type):
+ coupon = frappe.get_doc("Coupon Code", coupon_name)
if coupon:
- if transaction_type=='used':
- if coupon.used0:
- coupon.used=coupon.used-1
+ frappe.throw(
+ _("{0} Coupon used are {1}. Allowed quantity is exhausted").format(
+ coupon.coupon_code, coupon.used
+ )
+ )
+ elif transaction_type == "cancelled":
+ if coupon.used > 0:
+ coupon.used = coupon.used - 1
coupon.save(ignore_permissions=True)
diff --git a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py
index d544f976d2a..8ec726b36cd 100644
--- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py
+++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py
@@ -11,7 +11,7 @@ from erpnext.accounts.deferred_revenue import (
convert_deferred_expense_to_expense,
convert_deferred_revenue_to_income,
)
-from erpnext.accounts.general_ledger import make_reverse_gl_entries
+from erpnext.accounts.general_ledger import make_gl_entries
class ProcessDeferredAccounting(Document):
@@ -21,17 +21,17 @@ class ProcessDeferredAccounting(Document):
def on_submit(self):
conditions = build_conditions(self.type, self.account, self.company)
- if self.type == 'Income':
+ if self.type == "Income":
convert_deferred_revenue_to_income(self.name, self.start_date, self.end_date, conditions)
else:
convert_deferred_expense_to_expense(self.name, self.start_date, self.end_date, conditions)
def on_cancel(self):
- self.ignore_linked_doctypes = ['GL Entry']
- gl_entries = frappe.get_all('GL Entry', fields = ['*'],
- filters={
- 'against_voucher_type': self.doctype,
- 'against_voucher': self.name
- })
+ self.ignore_linked_doctypes = ["GL Entry"]
+ gl_entries = frappe.get_all(
+ "GL Entry",
+ fields=["*"],
+ filters={"against_voucher_type": self.doctype, "against_voucher": self.name},
+ )
- make_reverse_gl_entries(gl_entries=gl_entries)
+ make_gl_entries(gl_entries=gl_entries, cancel=1)
diff --git a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py
index 757d0fa3d01..164ba6aa348 100644
--- a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py
+++ b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py
@@ -15,9 +15,12 @@ from erpnext.stock.doctype.item.test_item import create_item
class TestProcessDeferredAccounting(unittest.TestCase):
def test_creation_of_ledger_entry_on_submit(self):
- ''' test creation of gl entries on submission of document '''
- deferred_account = create_account(account_name="Deferred Revenue",
- parent_account="Current Liabilities - _TC", company="_Test Company")
+ """test creation of gl entries on submission of document"""
+ deferred_account = create_account(
+ account_name="Deferred Revenue",
+ parent_account="Current Liabilities - _TC",
+ company="_Test Company",
+ )
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1
@@ -25,7 +28,9 @@ class TestProcessDeferredAccounting(unittest.TestCase):
item.no_of_months = 12
item.save()
- si = create_sales_invoice(item=item.name, update_stock=0, posting_date="2019-01-10", do_not_submit=True)
+ si = create_sales_invoice(
+ item=item.name, update_stock=0, posting_date="2019-01-10", do_not_submit=True
+ )
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-10"
si.items[0].service_end_date = "2019-03-15"
@@ -33,20 +38,22 @@ class TestProcessDeferredAccounting(unittest.TestCase):
si.save()
si.submit()
- process_deferred_accounting = doc = frappe.get_doc(dict(
- doctype='Process Deferred Accounting',
- posting_date="2019-01-01",
- start_date="2019-01-01",
- end_date="2019-01-31",
- type="Income"
- ))
+ process_deferred_accounting = doc = frappe.get_doc(
+ dict(
+ doctype="Process Deferred Accounting",
+ posting_date="2019-01-01",
+ start_date="2019-01-01",
+ end_date="2019-01-31",
+ type="Income",
+ )
+ )
process_deferred_accounting.insert()
process_deferred_accounting.submit()
expected_gle = [
[deferred_account, 33.85, 0.0, "2019-01-31"],
- ["Sales - _TC", 0.0, 33.85, "2019-01-31"]
+ ["Sales - _TC", 0.0, 33.85, "2019-01-31"],
]
check_gl_entries(self, si.name, expected_gle, "2019-01-10")
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
index f8d191cc3f8..82705a9cea4 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
@@ -64,10 +64,10 @@
{{ frappe.format(row.account, {fieldtype: "Link"}) or " " }}
- {{ row.account and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}
+ {{ row.get('account', '') and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}
- {{ row.account and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}
+ {{ row.get('account', '') and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}
{% endif %}
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
index 088c190f451..29f2e98e779 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
@@ -51,6 +51,13 @@ frappe.ui.form.on('Process Statement Of Accounts', {
}
}
});
+ frm.set_query("account", function() {
+ return {
+ filters: {
+ 'company': frm.doc.company
+ }
+ };
+ });
if(frm.doc.__islocal){
frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1));
frm.set_value('to_date', frappe.datetime.get_today());
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index 1b34d6d1f2f..01f716daa21 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -23,130 +23,166 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get
class ProcessStatementOfAccounts(Document):
def validate(self):
if not self.subject:
- self.subject = 'Statement Of Accounts for {{ customer.name }}'
+ self.subject = "Statement Of Accounts for {{ customer.name }}"
if not self.body:
- self.body = 'Hello {{ customer.name }}, PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.'
+ self.body = "Hello {{ customer.name }}, PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}."
validate_template(self.subject)
validate_template(self.body)
if not self.customers:
- frappe.throw(_('Customers not selected.'))
+ frappe.throw(_("Customers not selected."))
if self.enable_auto_email:
- self.to_date = self.start_date
- self.from_date = add_months(self.to_date, -1 * self.filter_duration)
+ if self.start_date and getdate(self.start_date) >= getdate(today()):
+ self.to_date = self.start_date
+ self.from_date = add_months(self.to_date, -1 * self.filter_duration)
def get_report_pdf(doc, consolidated=True):
statement_dict = {}
- ageing = ''
+ ageing = ""
base_template_path = "frappe/www/printview.html"
- template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
+ template_path = (
+ "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
+ )
for entry in doc.customers:
if doc.include_ageing:
- ageing_filters = frappe._dict({
- 'company': doc.company,
- 'report_date': doc.to_date,
- 'ageing_based_on': doc.ageing_based_on,
- 'range1': 30,
- 'range2': 60,
- 'range3': 90,
- 'range4': 120,
- 'customer': entry.customer
- })
+ ageing_filters = frappe._dict(
+ {
+ "company": doc.company,
+ "report_date": doc.to_date,
+ "ageing_based_on": doc.ageing_based_on,
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ "customer": entry.customer,
+ }
+ )
col1, ageing = get_ageing(ageing_filters)
if ageing:
- ageing[0]['ageing_based_on'] = doc.ageing_based_on
+ ageing[0]["ageing_based_on"] = doc.ageing_based_on
- tax_id = frappe.get_doc('Customer', entry.customer).tax_id
- presentation_currency = get_party_account_currency('Customer', entry.customer, doc.company) \
- or doc.currency or get_company_currency(doc.company)
+ tax_id = frappe.get_doc("Customer", entry.customer).tax_id
+ presentation_currency = (
+ get_party_account_currency("Customer", entry.customer, doc.company)
+ or doc.currency
+ or get_company_currency(doc.company)
+ )
if doc.letter_head:
from frappe.www.printview import get_letter_head
+
letter_head = get_letter_head(doc, 0)
- filters= frappe._dict({
- 'from_date': doc.from_date,
- 'to_date': doc.to_date,
- 'company': doc.company,
- 'finance_book': doc.finance_book if doc.finance_book else None,
- 'account': [doc.account] if doc.account else None,
- 'party_type': 'Customer',
- 'party': [entry.customer],
- 'presentation_currency': presentation_currency,
- 'group_by': doc.group_by,
- 'currency': doc.currency,
- 'cost_center': [cc.cost_center_name for cc in doc.cost_center],
- 'project': [p.project_name for p in doc.project],
- 'show_opening_entries': 0,
- 'include_default_book_entries': 0,
- 'tax_id': tax_id if tax_id else None
- })
+ filters = frappe._dict(
+ {
+ "from_date": doc.from_date,
+ "to_date": doc.to_date,
+ "company": doc.company,
+ "finance_book": doc.finance_book if doc.finance_book else None,
+ "account": [doc.account] if doc.account else None,
+ "party_type": "Customer",
+ "party": [entry.customer],
+ "presentation_currency": presentation_currency,
+ "group_by": doc.group_by,
+ "currency": doc.currency,
+ "cost_center": [cc.cost_center_name for cc in doc.cost_center],
+ "project": [p.project_name for p in doc.project],
+ "show_opening_entries": 0,
+ "include_default_book_entries": 0,
+ "tax_id": tax_id if tax_id else None,
+ }
+ )
col, res = get_soa(filters)
for x in [0, -2, -1]:
- res[x]['account'] = res[x]['account'].replace("'","")
+ res[x]["account"] = res[x]["account"].replace("'", "")
if len(res) == 3:
continue
- html = frappe.render_template(template_path, \
- {"filters": filters, "data": res, "ageing": ageing[0] if (doc.include_ageing and ageing) else None,
+ html = frappe.render_template(
+ template_path,
+ {
+ "filters": filters,
+ "data": res,
+ "ageing": ageing[0] if (doc.include_ageing and ageing) else None,
"letter_head": letter_head if doc.letter_head else None,
- "terms_and_conditions": frappe.db.get_value('Terms and Conditions', doc.terms_and_conditions, 'terms')
- if doc.terms_and_conditions else None})
+ "terms_and_conditions": frappe.db.get_value(
+ "Terms and Conditions", doc.terms_and_conditions, "terms"
+ )
+ if doc.terms_and_conditions
+ else None,
+ },
+ )
- html = frappe.render_template(base_template_path, {"body": html, \
- "css": get_print_style(), "title": "Statement For " + entry.customer})
+ html = frappe.render_template(
+ base_template_path,
+ {"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer},
+ )
statement_dict[entry.customer] = html
if not bool(statement_dict):
return False
elif consolidated:
- result = ''.join(list(statement_dict.values()))
- return get_pdf(result, {'orientation': doc.orientation})
+ result = "".join(list(statement_dict.values()))
+ return get_pdf(result, {"orientation": doc.orientation})
else:
for customer, statement_html in statement_dict.items():
- statement_dict[customer]=get_pdf(statement_html, {'orientation': doc.orientation})
+ statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation})
return statement_dict
+
def get_customers_based_on_territory_or_customer_group(customer_collection, collection_name):
fields_dict = {
- 'Customer Group': 'customer_group',
- 'Territory': 'territory',
+ "Customer Group": "customer_group",
+ "Territory": "territory",
}
collection = frappe.get_doc(customer_collection, collection_name)
- selected = [customer.name for customer in frappe.get_list(customer_collection, filters=[
- ['lft', '>=', collection.lft],
- ['rgt', '<=', collection.rgt]
- ],
- fields=['name'],
- order_by='lft asc, rgt desc'
- )]
- return frappe.get_list('Customer', fields=['name', 'email_id'], \
- filters=[[fields_dict[customer_collection], 'IN', selected]])
+ selected = [
+ customer.name
+ for customer in frappe.get_list(
+ customer_collection,
+ filters=[["lft", ">=", collection.lft], ["rgt", "<=", collection.rgt]],
+ fields=["name"],
+ order_by="lft asc, rgt desc",
+ )
+ ]
+ return frappe.get_list(
+ "Customer",
+ fields=["name", "email_id"],
+ filters=[[fields_dict[customer_collection], "IN", selected]],
+ )
+
def get_customers_based_on_sales_person(sales_person):
- lft, rgt = frappe.db.get_value("Sales Person",
- sales_person, ["lft", "rgt"])
- records = frappe.db.sql("""
+ lft, rgt = frappe.db.get_value("Sales Person", sales_person, ["lft", "rgt"])
+ records = frappe.db.sql(
+ """
select distinct parent, parenttype
from `tabSales Team` steam
where parenttype = 'Customer'
and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person)
- """, (lft, rgt), as_dict=1)
+ """,
+ (lft, rgt),
+ as_dict=1,
+ )
sales_person_records = frappe._dict()
for d in records:
sales_person_records.setdefault(d.parenttype, set()).add(d.parent)
- if sales_person_records.get('Customer'):
- return frappe.get_list('Customer', fields=['name', 'email_id'], \
- filters=[['name', 'in', list(sales_person_records['Customer'])]])
+ if sales_person_records.get("Customer"):
+ return frappe.get_list(
+ "Customer",
+ fields=["name", "email_id"],
+ filters=[["name", "in", list(sales_person_records["Customer"])]],
+ )
else:
return []
+
def get_recipients_and_cc(customer, doc):
recipients = []
for clist in doc.customers:
@@ -155,65 +191,72 @@ def get_recipients_and_cc(customer, doc):
if doc.primary_mandatory and clist.primary_email:
recipients.append(clist.primary_email)
cc = []
- if doc.cc_to != '':
+ if doc.cc_to != "":
try:
- cc=[frappe.get_value('User', doc.cc_to, 'email')]
+ cc = [frappe.get_value("User", doc.cc_to, "email")]
except Exception:
pass
return recipients, cc
+
def get_context(customer, doc):
template_doc = copy.deepcopy(doc)
del template_doc.customers
template_doc.from_date = format_date(template_doc.from_date)
template_doc.to_date = format_date(template_doc.to_date)
return {
- 'doc': template_doc,
- 'customer': frappe.get_doc('Customer', customer),
- 'frappe': frappe.utils
+ "doc": template_doc,
+ "customer": frappe.get_doc("Customer", customer),
+ "frappe": frappe.utils,
}
+
@frappe.whitelist()
def fetch_customers(customer_collection, collection_name, primary_mandatory):
customer_list = []
customers = []
- if customer_collection == 'Sales Person':
+ if customer_collection == "Sales Person":
customers = get_customers_based_on_sales_person(collection_name)
if not bool(customers):
- frappe.throw(_('No Customers found with selected options.'))
+ frappe.throw(_("No Customers found with selected options."))
else:
- if customer_collection == 'Sales Partner':
- customers = frappe.get_list('Customer', fields=['name', 'email_id'], \
- filters=[['default_sales_partner', '=', collection_name]])
+ if customer_collection == "Sales Partner":
+ customers = frappe.get_list(
+ "Customer",
+ fields=["name", "email_id"],
+ filters=[["default_sales_partner", "=", collection_name]],
+ )
else:
- customers = get_customers_based_on_territory_or_customer_group(customer_collection, collection_name)
+ customers = get_customers_based_on_territory_or_customer_group(
+ customer_collection, collection_name
+ )
for customer in customers:
- primary_email = customer.get('email_id') or ''
+ primary_email = customer.get("email_id") or ""
billing_email = get_customer_emails(customer.name, 1, billing_and_primary=False)
if int(primary_mandatory):
- if (primary_email == ''):
+ if primary_email == "":
continue
- elif (billing_email == '') and (primary_email == ''):
+ elif (billing_email == "") and (primary_email == ""):
continue
- customer_list.append({
- 'name': customer.name,
- 'primary_email': primary_email,
- 'billing_email': billing_email
- })
+ customer_list.append(
+ {"name": customer.name, "primary_email": primary_email, "billing_email": billing_email}
+ )
return customer_list
+
@frappe.whitelist()
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
- """ Returns first email from Contact Email table as a Billing email
- when Is Billing Contact checked
- and Primary email- email with Is Primary checked """
+ """Returns first email from Contact Email table as a Billing email
+ when Is Billing Contact checked
+ and Primary email- email with Is Primary checked"""
- billing_email = frappe.db.sql("""
+ billing_email = frappe.db.sql(
+ """
SELECT
email.email_id
FROM
@@ -231,42 +274,43 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr
and link.link_name=%s
and contact.is_billing_contact=1
ORDER BY
- contact.creation desc""", customer_name)
+ contact.creation desc""",
+ customer_name,
+ )
if len(billing_email) == 0 or (billing_email[0][0] is None):
if billing_and_primary:
frappe.throw(_("No billing email found for customer: {0}").format(customer_name))
else:
- return ''
+ return ""
if billing_and_primary:
- primary_email = frappe.get_value('Customer', customer_name, 'email_id')
+ primary_email = frappe.get_value("Customer", customer_name, "email_id")
if primary_email is None and int(primary_mandatory):
frappe.throw(_("No primary email found for customer: {0}").format(customer_name))
- return [primary_email or '', billing_email[0][0]]
+ return [primary_email or "", billing_email[0][0]]
else:
- return billing_email[0][0] or ''
+ return billing_email[0][0] or ""
+
@frappe.whitelist()
def download_statements(document_name):
- doc = frappe.get_doc('Process Statement Of Accounts', document_name)
+ doc = frappe.get_doc("Process Statement Of Accounts", document_name)
report = get_report_pdf(doc)
if report:
- frappe.local.response.filename = doc.name + '.pdf'
+ frappe.local.response.filename = doc.name + ".pdf"
frappe.local.response.filecontent = report
frappe.local.response.type = "download"
+
@frappe.whitelist()
def send_emails(document_name, from_scheduler=False):
- doc = frappe.get_doc('Process Statement Of Accounts', document_name)
+ doc = frappe.get_doc("Process Statement Of Accounts", document_name)
report = get_report_pdf(doc, consolidated=False)
if report:
for customer, report_pdf in report.items():
- attachments = [{
- 'fname': customer + '.pdf',
- 'fcontent': report_pdf
- }]
+ attachments = [{"fname": customer + ".pdf", "fcontent": report_pdf}]
recipients, cc = get_recipients_and_cc(customer, doc)
context = get_context(customer, doc)
@@ -274,7 +318,7 @@ def send_emails(document_name, from_scheduler=False):
message = frappe.render_template(doc.body, context)
frappe.enqueue(
- queue='short',
+ queue="short",
method=frappe.sendmail,
recipients=recipients,
sender=frappe.session.user,
@@ -282,28 +326,34 @@ def send_emails(document_name, from_scheduler=False):
subject=subject,
message=message,
now=True,
- reference_doctype='Process Statement Of Accounts',
+ reference_doctype="Process Statement Of Accounts",
reference_name=document_name,
- attachments=attachments
+ attachments=attachments,
)
if doc.enable_auto_email and from_scheduler:
new_to_date = getdate(today())
- if doc.frequency == 'Weekly':
+ if doc.frequency == "Weekly":
new_to_date = add_days(new_to_date, 7)
else:
- new_to_date = add_months(new_to_date, 1 if doc.frequency == 'Monthly' else 3)
+ new_to_date = add_months(new_to_date, 1 if doc.frequency == "Monthly" else 3)
new_from_date = add_months(new_to_date, -1 * doc.filter_duration)
- doc.add_comment('Comment', 'Emails sent on: ' + frappe.utils.format_datetime(frappe.utils.now()))
- doc.db_set('to_date', new_to_date, commit=True)
- doc.db_set('from_date', new_from_date, commit=True)
+ doc.add_comment(
+ "Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now())
+ )
+ doc.db_set("to_date", new_to_date, commit=True)
+ doc.db_set("from_date", new_from_date, commit=True)
return True
else:
return False
+
@frappe.whitelist()
def send_auto_email():
- selected = frappe.get_list('Process Statement Of Accounts', filters={'to_date': format_date(today()), 'enable_auto_email': 1})
+ selected = frappe.get_list(
+ "Process Statement Of Accounts",
+ filters={"to_date": format_date(today()), "enable_auto_email": 1},
+ )
for entry in selected:
send_emails(entry.name, from_scheduler=True)
return True
diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py
index 5fbe93ee68d..fac9be7bdb1 100644
--- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py
+++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py
@@ -6,29 +6,73 @@ import frappe
from frappe import _
from frappe.model.document import Document
-pricing_rule_fields = ['apply_on', 'mixed_conditions', 'is_cumulative', 'other_item_code', 'other_item_group',
- 'apply_rule_on_other', 'other_brand', 'selling', 'buying', 'applicable_for', 'valid_from',
- 'valid_upto', 'customer', 'customer_group', 'territory', 'sales_partner', 'campaign', 'supplier',
- 'supplier_group', 'company', 'currency', 'apply_multiple_pricing_rules']
+pricing_rule_fields = [
+ "apply_on",
+ "mixed_conditions",
+ "is_cumulative",
+ "other_item_code",
+ "other_item_group",
+ "apply_rule_on_other",
+ "other_brand",
+ "selling",
+ "buying",
+ "applicable_for",
+ "valid_from",
+ "valid_upto",
+ "customer",
+ "customer_group",
+ "territory",
+ "sales_partner",
+ "campaign",
+ "supplier",
+ "supplier_group",
+ "company",
+ "currency",
+ "apply_multiple_pricing_rules",
+]
-other_fields = ['min_qty', 'max_qty', 'min_amt',
- 'max_amt', 'priority','warehouse', 'threshold_percentage', 'rule_description']
+other_fields = [
+ "min_qty",
+ "max_qty",
+ "min_amt",
+ "max_amt",
+ "priority",
+ "warehouse",
+ "threshold_percentage",
+ "rule_description",
+]
-price_discount_fields = ['rate_or_discount', 'apply_discount_on', 'apply_discount_on_rate',
- 'rate', 'discount_amount', 'discount_percentage', 'validate_applied_rule', 'apply_multiple_pricing_rules']
+price_discount_fields = [
+ "rate_or_discount",
+ "apply_discount_on",
+ "apply_discount_on_rate",
+ "rate",
+ "discount_amount",
+ "discount_percentage",
+ "validate_applied_rule",
+ "apply_multiple_pricing_rules",
+]
+
+product_discount_fields = [
+ "free_item",
+ "free_qty",
+ "free_item_uom",
+ "free_item_rate",
+ "same_item",
+ "is_recursive",
+ "apply_multiple_pricing_rules",
+]
-product_discount_fields = ['free_item', 'free_qty', 'free_item_uom',
- 'free_item_rate', 'same_item', 'is_recursive', 'apply_multiple_pricing_rules']
class TransactionExists(frappe.ValidationError):
pass
+
class PromotionalScheme(Document):
def validate(self):
if not self.selling and not self.buying:
frappe.throw(_("Either 'Selling' or 'Buying' must be selected"), title=_("Mandatory"))
- if not (self.price_discount_slabs
- or self.product_discount_slabs):
+ if not (self.price_discount_slabs or self.product_discount_slabs):
frappe.throw(_("Price or product discount slabs are required"))
self.validate_applicable_for()
@@ -39,7 +83,7 @@ class PromotionalScheme(Document):
applicable_for = frappe.scrub(self.applicable_for)
if not self.get(applicable_for):
- msg = (f'The field {frappe.bold(self.applicable_for)} is required')
+ msg = f"The field {frappe.bold(self.applicable_for)} is required"
frappe.throw(_(msg))
def validate_pricing_rules(self):
@@ -53,28 +97,28 @@ class PromotionalScheme(Document):
if self._doc_before_save.applicable_for == self.applicable_for:
return
- docnames = frappe.get_all('Pricing Rule',
- filters= {'promotional_scheme': self.name})
+ docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name})
for docname in docnames:
- if frappe.db.exists('Pricing Rule Detail',
- {'pricing_rule': docname.name, 'docstatus': ('<', 2)}):
+ if frappe.db.exists(
+ "Pricing Rule Detail", {"pricing_rule": docname.name, "docstatus": ("<", 2)}
+ ):
raise_for_transaction_exists(self.name)
if docnames and not transaction_exists:
for docname in docnames:
- frappe.delete_doc('Pricing Rule', docname.name)
+ frappe.delete_doc("Pricing Rule", docname.name)
def on_update(self):
- pricing_rules = frappe.get_all(
- 'Pricing Rule',
- fields = ["promotional_scheme_id", "name", "creation"],
- filters = {
- 'promotional_scheme': self.name,
- 'applicable_for': self.applicable_for
- },
- order_by = 'creation asc',
- ) or {}
+ pricing_rules = (
+ frappe.get_all(
+ "Pricing Rule",
+ fields=["promotional_scheme_id", "name", "creation"],
+ filters={"promotional_scheme": self.name, "applicable_for": self.applicable_for},
+ order_by="creation asc",
+ )
+ or {}
+ )
self.update_pricing_rules(pricing_rules)
def update_pricing_rules(self, pricing_rules):
@@ -83,7 +127,7 @@ class PromotionalScheme(Document):
names = []
for rule in pricing_rules:
names.append(rule.name)
- rules[rule.get('promotional_scheme_id')] = names
+ rules[rule.get("promotional_scheme_id")] = names
docs = get_pricing_rules(self, rules)
@@ -100,34 +144,38 @@ class PromotionalScheme(Document):
frappe.msgprint(_("New {0} pricing rules are created").format(count))
def on_trash(self):
- for rule in frappe.get_all('Pricing Rule',
- {'promotional_scheme': self.name}):
- frappe.delete_doc('Pricing Rule', rule.name)
+ for rule in frappe.get_all("Pricing Rule", {"promotional_scheme": self.name}):
+ frappe.delete_doc("Pricing Rule", rule.name)
+
def raise_for_transaction_exists(name):
- msg = (f"""You can't change the {frappe.bold(_('Applicable For'))}
- because transactions are present against the Promotional Scheme {frappe.bold(name)}. """)
- msg += 'Kindly disable this Promotional Scheme and create new for new Applicable For.'
+ msg = f"""You can't change the {frappe.bold(_('Applicable For'))}
+ because transactions are present against the Promotional Scheme {frappe.bold(name)}. """
+ msg += "Kindly disable this Promotional Scheme and create new for new Applicable For."
frappe.throw(_(msg), TransactionExists)
+
def get_pricing_rules(doc, rules=None):
if rules is None:
rules = {}
new_doc = []
- for child_doc, fields in {'price_discount_slabs': price_discount_fields,
- 'product_discount_slabs': product_discount_fields}.items():
+ for child_doc, fields in {
+ "price_discount_slabs": price_discount_fields,
+ "product_discount_slabs": product_discount_fields,
+ }.items():
if doc.get(child_doc):
new_doc.extend(_get_pricing_rules(doc, child_doc, fields, rules))
return new_doc
+
def _get_pricing_rules(doc, child_doc, discount_fields, rules=None):
if rules is None:
rules = {}
new_doc = []
args = get_args_for_pricing_rule(doc)
- applicable_for = frappe.scrub(doc.get('applicable_for'))
+ applicable_for = frappe.scrub(doc.get("applicable_for"))
for idx, d in enumerate(doc.get(child_doc)):
if d.name in rules:
@@ -138,15 +186,23 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules=None):
else:
for applicable_for_value in args.get(applicable_for):
docname = get_pricing_rule_docname(d, applicable_for, applicable_for_value)
- pr = prepare_pricing_rule(args, doc, child_doc, discount_fields,
- d, docname, applicable_for, applicable_for_value)
+ pr = prepare_pricing_rule(
+ args, doc, child_doc, discount_fields, d, docname, applicable_for, applicable_for_value
+ )
new_doc.append(pr)
elif args.get(applicable_for):
applicable_for_values = args.get(applicable_for) or []
for applicable_for_value in applicable_for_values:
- pr = prepare_pricing_rule(args, doc, child_doc, discount_fields,
- d, applicable_for=applicable_for, value= applicable_for_value)
+ pr = prepare_pricing_rule(
+ args,
+ doc,
+ child_doc,
+ discount_fields,
+ d,
+ applicable_for=applicable_for,
+ value=applicable_for_value,
+ )
new_doc.append(pr)
else:
@@ -155,20 +211,24 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules=None):
return new_doc
-def get_pricing_rule_docname(row: dict, applicable_for: str = None, applicable_for_value: str = None) -> str:
- fields = ['promotional_scheme_id', 'name']
- filters = {
- 'promotional_scheme_id': row.name
- }
+
+def get_pricing_rule_docname(
+ row: dict, applicable_for: str = None, applicable_for_value: str = None
+) -> str:
+ fields = ["promotional_scheme_id", "name"]
+ filters = {"promotional_scheme_id": row.name}
if applicable_for:
fields.append(applicable_for)
filters[applicable_for] = applicable_for_value
- docname = frappe.get_all('Pricing Rule', fields = fields, filters = filters)
- return docname[0].name if docname else ''
+ docname = frappe.get_all("Pricing Rule", fields=fields, filters=filters)
+ return docname[0].name if docname else ""
-def prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname=None, applicable_for=None, value=None):
+
+def prepare_pricing_rule(
+ args, doc, child_doc, discount_fields, d, docname=None, applicable_for=None, value=None
+):
if docname:
pr = frappe.get_doc("Pricing Rule", docname)
else:
@@ -182,33 +242,31 @@ def prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname=None,
return set_args(temp_args, pr, doc, child_doc, discount_fields, d)
+
def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields):
pr.update(args)
- for field in (other_fields + discount_fields):
+ for field in other_fields + discount_fields:
pr.set(field, child_doc_fields.get(field))
pr.promotional_scheme_id = child_doc_fields.name
pr.promotional_scheme = doc.name
pr.disable = child_doc_fields.disable if child_doc_fields.disable else doc.disable
- pr.price_or_product_discount = ('Price'
- if child_doc == 'price_discount_slabs' else 'Product')
+ pr.price_or_product_discount = "Price" if child_doc == "price_discount_slabs" else "Product"
- for field in ['items', 'item_groups', 'brands']:
+ for field in ["items", "item_groups", "brands"]:
if doc.get(field):
pr.set(field, [])
- apply_on = frappe.scrub(doc.get('apply_on'))
+ apply_on = frappe.scrub(doc.get("apply_on"))
for d in doc.get(field):
- pr.append(field, {
- apply_on: d.get(apply_on),
- 'uom': d.uom
- })
+ pr.append(field, {apply_on: d.get(apply_on), "uom": d.uom})
return pr
+
def get_args_for_pricing_rule(doc):
- args = { 'promotional_scheme': doc.name }
- applicable_for = frappe.scrub(doc.get('applicable_for'))
+ args = {"promotional_scheme": doc.name}
+ applicable_for = frappe.scrub(doc.get("applicable_for"))
for d in pricing_rule_fields:
if d == applicable_for:
diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme_dashboard.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme_dashboard.py
index 6d079242682..70e87404a35 100644
--- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme_dashboard.py
+++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme_dashboard.py
@@ -3,11 +3,6 @@ from frappe import _
def get_data():
return {
- 'fieldname': 'promotional_scheme',
- 'transactions': [
- {
- 'label': _('Reference'),
- 'items': ['Pricing Rule']
- }
- ]
+ "fieldname": "promotional_scheme",
+ "transactions": [{"label": _("Reference"), "items": ["Pricing Rule"]}],
}
diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py
index 49192a45f87..b3b9d7b2086 100644
--- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py
+++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py
@@ -11,96 +11,111 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
class TestPromotionalScheme(unittest.TestCase):
def setUp(self):
- if frappe.db.exists('Promotional Scheme', '_Test Scheme'):
- frappe.delete_doc('Promotional Scheme', '_Test Scheme')
+ if frappe.db.exists("Promotional Scheme", "_Test Scheme"):
+ frappe.delete_doc("Promotional Scheme", "_Test Scheme")
def test_promotional_scheme(self):
- ps = make_promotional_scheme(applicable_for='Customer', customer='_Test Customer')
- price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"],
- filters = {'promotional_scheme': ps.name})
- self.assertTrue(len(price_rules),1)
- price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[0].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1)
- self.assertTrue(price_doc_details.customer, '_Test Customer')
+ ps = make_promotional_scheme(applicable_for="Customer", customer="_Test Customer")
+ price_rules = frappe.get_all(
+ "Pricing Rule",
+ fields=["promotional_scheme_id", "name", "creation"],
+ filters={"promotional_scheme": ps.name},
+ )
+ self.assertTrue(len(price_rules), 1)
+ price_doc_details = frappe.db.get_value(
+ "Pricing Rule", price_rules[0].name, ["customer", "min_qty", "discount_percentage"], as_dict=1
+ )
+ self.assertTrue(price_doc_details.customer, "_Test Customer")
self.assertTrue(price_doc_details.min_qty, 4)
self.assertTrue(price_doc_details.discount_percentage, 20)
ps.price_discount_slabs[0].min_qty = 6
- ps.append('customer', {
- 'customer': "_Test Customer 2"})
+ ps.append("customer", {"customer": "_Test Customer 2"})
ps.save()
- price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"],
- filters = {'promotional_scheme': ps.name})
+ price_rules = frappe.get_all(
+ "Pricing Rule",
+ fields=["promotional_scheme_id", "name"],
+ filters={"promotional_scheme": ps.name},
+ )
self.assertTrue(len(price_rules), 2)
- price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[1].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1)
- self.assertTrue(price_doc_details.customer, '_Test Customer 2')
+ price_doc_details = frappe.db.get_value(
+ "Pricing Rule", price_rules[1].name, ["customer", "min_qty", "discount_percentage"], as_dict=1
+ )
+ self.assertTrue(price_doc_details.customer, "_Test Customer 2")
self.assertTrue(price_doc_details.min_qty, 6)
self.assertTrue(price_doc_details.discount_percentage, 20)
- price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[0].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1)
- self.assertTrue(price_doc_details.customer, '_Test Customer')
+ price_doc_details = frappe.db.get_value(
+ "Pricing Rule", price_rules[0].name, ["customer", "min_qty", "discount_percentage"], as_dict=1
+ )
+ self.assertTrue(price_doc_details.customer, "_Test Customer")
self.assertTrue(price_doc_details.min_qty, 6)
- frappe.delete_doc('Promotional Scheme', ps.name)
- price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"],
- filters = {'promotional_scheme': ps.name})
+ frappe.delete_doc("Promotional Scheme", ps.name)
+ price_rules = frappe.get_all(
+ "Pricing Rule",
+ fields=["promotional_scheme_id", "name"],
+ filters={"promotional_scheme": ps.name},
+ )
self.assertEqual(price_rules, [])
def test_promotional_scheme_without_applicable_for(self):
ps = make_promotional_scheme()
- price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
+ price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
self.assertTrue(len(price_rules), 1)
- frappe.delete_doc('Promotional Scheme', ps.name)
+ frappe.delete_doc("Promotional Scheme", ps.name)
- price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
+ price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
self.assertEqual(price_rules, [])
def test_change_applicable_for_in_promotional_scheme(self):
ps = make_promotional_scheme()
- price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
+ price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
self.assertTrue(len(price_rules), 1)
- so = make_sales_order(qty=5, currency='USD', do_not_save=True)
+ so = make_sales_order(qty=5, currency="USD", do_not_save=True)
so.set_missing_values()
so.save()
self.assertEqual(price_rules[0].name, so.pricing_rules[0].pricing_rule)
- ps.applicable_for = 'Customer'
- ps.append('customer', {
- 'customer': '_Test Customer'
- })
+ ps.applicable_for = "Customer"
+ ps.append("customer", {"customer": "_Test Customer"})
self.assertRaises(TransactionExists, ps.save)
- frappe.delete_doc('Sales Order', so.name)
- frappe.delete_doc('Promotional Scheme', ps.name)
- price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
+ frappe.delete_doc("Sales Order", so.name)
+ frappe.delete_doc("Promotional Scheme", ps.name)
+ price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
self.assertEqual(price_rules, [])
+
def make_promotional_scheme(**args):
args = frappe._dict(args)
- ps = frappe.new_doc('Promotional Scheme')
- ps.name = '_Test Scheme'
- ps.append('items',{
- 'item_code': '_Test Item'
- })
+ ps = frappe.new_doc("Promotional Scheme")
+ ps.name = "_Test Scheme"
+ ps.append("items", {"item_code": "_Test Item"})
ps.selling = 1
- ps.append('price_discount_slabs',{
- 'min_qty': 4,
- 'validate_applied_rule': 0,
- 'discount_percentage': 20,
- 'rule_description': 'Test'
- })
+ ps.append(
+ "price_discount_slabs",
+ {
+ "min_qty": 4,
+ "validate_applied_rule": 0,
+ "discount_percentage": 20,
+ "rule_description": "Test",
+ },
+ )
- ps.company = '_Test Company'
+ ps.company = "_Test Company"
if args.applicable_for:
ps.applicable_for = args.applicable_for
- ps.append(frappe.scrub(args.applicable_for), {
- frappe.scrub(args.applicable_for): args.get(frappe.scrub(args.applicable_for))
- })
+ ps.append(
+ frappe.scrub(args.applicable_for),
+ {frappe.scrub(args.applicable_for): args.get(frappe.scrub(args.applicable_for))},
+ )
ps.save()
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 19c0d8aaf3d..4f5640f9cb9 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -30,6 +30,9 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
onload: function() {
this._super();
+ // Ignore linked advances
+ this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry'];
+
if(!this.frm.doc.__islocal) {
// show credit_to in print format
if(!this.frm.doc.supplier && this.frm.doc.credit_to) {
@@ -276,6 +279,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
if(this.frm.updating_party_details || this.frm.doc.inter_company_invoice_reference)
return;
+ if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return;
+
erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details",
{
posting_date: this.frm.doc.posting_date,
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index f3452e1cf81..e4719d6b40c 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -33,6 +33,7 @@ from erpnext.accounts.utils import get_account_currency, get_fiscal_year
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.buying.utils import check_on_hold_or_closed_status
+from erpnext.controllers.accounts_controller import validate_account_head
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
@@ -40,25 +41,26 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
update_billed_amount_based_on_po,
)
-form_grid_templates = {
- "items": "templates/form_grid/item_grid.html"
-}
+form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
+
class PurchaseInvoice(BuyingController):
def __init__(self, *args, **kwargs):
super(PurchaseInvoice, self).__init__(*args, **kwargs)
- self.status_updater = [{
- 'source_dt': 'Purchase Invoice Item',
- 'target_dt': 'Purchase Order Item',
- 'join_field': 'po_detail',
- 'target_field': 'billed_amt',
- 'target_parent_dt': 'Purchase Order',
- 'target_parent_field': 'per_billed',
- 'target_ref_field': 'amount',
- 'source_field': 'amount',
- 'percent_join_field': 'purchase_order',
- 'overflow_type': 'billing'
- }]
+ self.status_updater = [
+ {
+ "source_dt": "Purchase Invoice Item",
+ "target_dt": "Purchase Order Item",
+ "join_field": "po_detail",
+ "target_field": "billed_amt",
+ "target_parent_dt": "Purchase Order",
+ "target_parent_field": "per_billed",
+ "target_ref_field": "amount",
+ "source_field": "amount",
+ "percent_join_field": "purchase_order",
+ "overflow_type": "billing",
+ }
+ ]
def onload(self):
super(PurchaseInvoice, self).onload()
@@ -67,15 +69,14 @@ class PurchaseInvoice(BuyingController):
def before_save(self):
if not self.on_hold:
- self.release_date = ''
-
+ self.release_date = ""
def invoice_is_blocked(self):
return self.on_hold and (not self.release_date or self.release_date > getdate(nowdate()))
def validate(self):
if not self.is_opening:
- self.is_opening = 'No'
+ self.is_opening = "No"
self.validate_posting_time()
@@ -87,14 +88,14 @@ class PurchaseInvoice(BuyingController):
self.validate_supplier_invoice()
# validate cash purchase
- if (self.is_paid == 1):
+ if self.is_paid == 1:
self.validate_cash()
# validate service stop date to lie in between start and end date
validate_service_stop_date(self)
- if self._action=="submit" and self.update_stock:
- self.make_batches('warehouse')
+ if self._action == "submit" and self.update_stock:
+ self.make_batches("warehouse")
self.validate_release_date()
self.check_conversion_rate()
@@ -105,45 +106,53 @@ class PurchaseInvoice(BuyingController):
self.validate_uom_is_integer("uom", "qty")
self.validate_uom_is_integer("stock_uom", "stock_qty")
self.set_expense_account(for_validate=True)
+ self.validate_expense_account()
self.set_against_expense_account()
self.validate_write_off_account()
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items")
self.create_remarks()
self.set_status()
self.validate_purchase_receipt_if_update_stock()
- validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference)
+ validate_inter_company_party(
+ self.doctype, self.supplier, self.company, self.inter_company_invoice_reference
+ )
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
def validate_release_date(self):
if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
- frappe.throw(_('Release date must be in the future'))
+ frappe.throw(_("Release date must be in the future"))
def validate_cash(self):
if not self.cash_bank_account and flt(self.paid_amount):
frappe.throw(_("Cash or Bank Account is mandatory for making payment entry"))
- if (flt(self.paid_amount) + flt(self.write_off_amount)
- - flt(self.get("rounded_total") or self.grand_total)
- > 1/(10**(self.precision("base_grand_total") + 1))):
+ if flt(self.paid_amount) + flt(self.write_off_amount) - flt(
+ self.get("rounded_total") or self.grand_total
+ ) > 1 / (10 ** (self.precision("base_grand_total") + 1)):
frappe.throw(_("""Paid amount + Write Off Amount can not be greater than Grand Total"""))
def create_remarks(self):
if not self.remarks:
if self.bill_no and self.bill_date:
- self.remarks = _("Against Supplier Invoice {0} dated {1}").format(self.bill_no,
- formatdate(self.bill_date))
+ self.remarks = _("Against Supplier Invoice {0} dated {1}").format(
+ self.bill_no, formatdate(self.bill_date)
+ )
else:
self.remarks = _("No Remarks")
def set_missing_values(self, for_validate=False):
if not self.credit_to:
self.credit_to = get_party_account("Supplier", self.supplier, self.company)
- self.party_account_currency = frappe.db.get_value("Account", self.credit_to, "account_currency", cache=True)
+ self.party_account_currency = frappe.db.get_value(
+ "Account", self.credit_to, "account_currency", cache=True
+ )
if not self.due_date:
- self.due_date = get_due_date(self.posting_date, "Supplier", self.supplier, self.company, self.bill_date)
+ self.due_date = get_due_date(
+ self.posting_date, "Supplier", self.supplier, self.company, self.bill_date
+ )
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
if tds_category and not for_validate:
@@ -155,8 +164,12 @@ class PurchaseInvoice(BuyingController):
def check_conversion_rate(self):
default_currency = erpnext.get_company_currency(self.company)
if not default_currency:
- throw(_('Please enter default currency in Company Master'))
- if (self.currency == default_currency and flt(self.conversion_rate) != 1.00) or not self.conversion_rate or (self.currency != default_currency and flt(self.conversion_rate) == 1.00):
+ throw(_("Please enter default currency in Company Master"))
+ if (
+ (self.currency == default_currency and flt(self.conversion_rate) != 1.00)
+ or not self.conversion_rate
+ or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
+ ):
throw(_("Conversion rate cannot be 0 or 1"))
def validate_credit_to_acc(self):
@@ -165,19 +178,24 @@ class PurchaseInvoice(BuyingController):
if not self.credit_to:
self.raise_missing_debit_credit_account_error("Supplier", self.supplier)
- account = frappe.db.get_value("Account", self.credit_to,
- ["account_type", "report_type", "account_currency"], as_dict=True)
+ account = frappe.db.get_value(
+ "Account", self.credit_to, ["account_type", "report_type", "account_currency"], as_dict=True
+ )
if account.report_type != "Balance Sheet":
frappe.throw(
- _("Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account.")
- .format(frappe.bold("Credit To")), title=_("Invalid Account")
+ _(
+ "Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account."
+ ).format(frappe.bold("Credit To")),
+ title=_("Invalid Account"),
)
if self.supplier and account.account_type != "Payable":
frappe.throw(
- _("Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account.")
- .format(frappe.bold("Credit To"), frappe.bold(self.credit_to)), title=_("Invalid Account")
+ _(
+ "Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account."
+ ).format(frappe.bold("Credit To"), frappe.bold(self.credit_to)),
+ title=_("Invalid Account"),
)
self.party_account_currency = account.account_currency
@@ -185,51 +203,61 @@ class PurchaseInvoice(BuyingController):
def check_on_hold_or_closed_status(self):
check_list = []
- for d in self.get('items'):
+ for d in self.get("items"):
if d.purchase_order and not d.purchase_order in check_list and not d.purchase_receipt:
check_list.append(d.purchase_order)
- check_on_hold_or_closed_status('Purchase Order', d.purchase_order)
+ check_on_hold_or_closed_status("Purchase Order", d.purchase_order)
def validate_with_previous_doc(self):
- super(PurchaseInvoice, self).validate_with_previous_doc({
- "Purchase Order": {
- "ref_dn_field": "purchase_order",
- "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]],
- },
- "Purchase Order Item": {
- "ref_dn_field": "po_detail",
- "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="]],
- "is_child_table": True,
- "allow_duplicate_prev_row_id": True
- },
- "Purchase Receipt": {
- "ref_dn_field": "purchase_receipt",
- "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]],
- },
- "Purchase Receipt Item": {
- "ref_dn_field": "pr_detail",
- "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="]],
- "is_child_table": True
+ super(PurchaseInvoice, self).validate_with_previous_doc(
+ {
+ "Purchase Order": {
+ "ref_dn_field": "purchase_order",
+ "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]],
+ },
+ "Purchase Order Item": {
+ "ref_dn_field": "po_detail",
+ "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="]],
+ "is_child_table": True,
+ "allow_duplicate_prev_row_id": True,
+ },
+ "Purchase Receipt": {
+ "ref_dn_field": "purchase_receipt",
+ "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]],
+ },
+ "Purchase Receipt Item": {
+ "ref_dn_field": "pr_detail",
+ "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="]],
+ "is_child_table": True,
+ },
}
- })
+ )
- if cint(frappe.db.get_single_value('Buying Settings', 'maintain_same_rate')) and not self.is_return:
- self.validate_rate_with_reference_doc([
- ["Purchase Order", "purchase_order", "po_detail"],
- ["Purchase Receipt", "purchase_receipt", "pr_detail"]
- ])
+ if (
+ cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) and not self.is_return
+ ):
+ self.validate_rate_with_reference_doc(
+ [
+ ["Purchase Order", "purchase_order", "po_detail"],
+ ["Purchase Receipt", "purchase_receipt", "pr_detail"],
+ ]
+ )
def validate_warehouse(self, for_validate=True):
if self.update_stock and for_validate:
- for d in self.get('items'):
- if not d.warehouse:
- frappe.throw(_("Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}").
- format(d.idx, d.item_code, self.company))
+ stock_items = self.get_stock_items()
+ for d in self.get("items"):
+ if not d.warehouse and d.item_code in stock_items:
+ frappe.throw(
+ _(
+ "Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}"
+ ).format(d.idx, d.item_code, self.company)
+ )
super(PurchaseInvoice, self).validate_warehouse()
def validate_item_code(self):
- for d in self.get('items'):
+ for d in self.get("items"):
if not d.item_code:
frappe.msgprint(_("Item Code required at Row No {0}").format(d.idx), raise_exception=True)
@@ -257,51 +285,82 @@ class PurchaseInvoice(BuyingController):
if item.item_code:
asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category")
- if auto_accounting_for_stock and item.item_code in stock_items \
- and self.is_opening == 'No' and not item.is_fixed_asset \
- and (not item.po_detail or
- not frappe.db.get_value("Purchase Order Item", item.po_detail, "delivered_by_supplier")):
+ if (
+ auto_accounting_for_stock
+ and item.item_code in stock_items
+ and self.is_opening == "No"
+ and not item.is_fixed_asset
+ and (
+ not item.po_detail
+ or not frappe.db.get_value("Purchase Order Item", item.po_detail, "delivered_by_supplier")
+ )
+ ):
if self.update_stock and (not item.from_warehouse):
- 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(
- item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]), frappe.bold(item.expense_account), frappe.bold(item.warehouse))
+ 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(
+ item.idx,
+ frappe.bold(warehouse_account[item.warehouse]["account"]),
+ frappe.bold(item.expense_account),
+ frappe.bold(item.warehouse),
+ )
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = warehouse_account[item.warehouse]["account"]
else:
# check if 'Stock Received But Not Billed' account is credited in Purchase receipt or not
if item.purchase_receipt:
- negative_expense_booked_in_pr = frappe.db.sql("""select name from `tabGL Entry`
+ negative_expense_booked_in_pr = frappe.db.sql(
+ """select name from `tabGL Entry`
where voucher_type='Purchase Receipt' and voucher_no=%s and account = %s""",
- (item.purchase_receipt, stock_not_billed_account))
+ (item.purchase_receipt, stock_not_billed_account),
+ )
if negative_expense_booked_in_pr:
- if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
- msg = _("Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}").format(
- item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt))
+ if (
+ for_validate and item.expense_account and item.expense_account != stock_not_billed_account
+ ):
+ msg = _(
+ "Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}"
+ ).format(
+ item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt)
+ )
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
else:
# If no purchase receipt present then book expense in 'Stock Received But Not Billed'
# This is done in cases when Purchase Invoice is created before Purchase Receipt
- if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
- msg = _("Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}.").format(
- item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code))
+ if (
+ for_validate and item.expense_account and item.expense_account != stock_not_billed_account
+ ):
+ msg = _(
+ "Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}."
+ ).format(
+ item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code)
+ )
msg += " "
- msg += _("This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice")
+ msg += _(
+ "This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice"
+ )
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
- asset_category_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
- company = self.company)
+ asset_category_account = get_asset_category_account(
+ "fixed_asset_account", item=item.item_code, company=self.company
+ )
if not asset_category_account:
- form_link = get_link_to_form('Asset Category', asset_category)
+ form_link = get_link_to_form("Asset Category", asset_category)
throw(
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
- title=_("Missing Account")
+ title=_("Missing Account"),
)
item.expense_account = asset_category_account
elif item.is_fixed_asset and item.pr_detail:
@@ -309,6 +368,10 @@ class PurchaseInvoice(BuyingController):
elif not item.expense_account and for_validate:
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
+ def validate_expense_account(self):
+ for item in self.get("items"):
+ validate_account_head(item.idx, item.expense_account, self.company, "Expense")
+
def set_against_expense_account(self):
against_accounts = []
for item in self.get("items"):
@@ -318,32 +381,44 @@ class PurchaseInvoice(BuyingController):
self.against_expense_account = ",".join(against_accounts)
def po_required(self):
- if frappe.db.get_value("Buying Settings", None, "po_required") == 'Yes':
+ if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes":
- if frappe.get_value('Supplier', self.supplier, 'allow_purchase_invoice_creation_without_purchase_order'):
+ if frappe.get_value(
+ "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order"
+ ):
return
- for d in self.get('items'):
+ for d in self.get("items"):
if not d.purchase_order:
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
msg += "
"
msg += _("To submit the invoice without purchase order please set {0} as {1} in {2}").format(
- frappe.bold(_('Purchase Order Required')), frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
+ frappe.bold(_("Purchase Order Required")),
+ frappe.bold("No"),
+ get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"),
+ )
throw(msg, title=_("Mandatory Purchase Order"))
def pr_required(self):
stock_items = self.get_stock_items()
- if frappe.db.get_value("Buying Settings", None, "pr_required") == 'Yes':
+ if frappe.db.get_value("Buying Settings", None, "pr_required") == "Yes":
- if frappe.get_value('Supplier', self.supplier, 'allow_purchase_invoice_creation_without_purchase_receipt'):
+ if frappe.get_value(
+ "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt"
+ ):
return
- for d in self.get('items'):
+ for d in self.get("items"):
if not d.purchase_receipt and d.item_code in stock_items:
msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code))
msg += "
"
- msg += _("To submit the invoice without purchase receipt please set {0} as {1} in {2}").format(
- frappe.bold(_('Purchase Receipt Required')), frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
+ msg += _(
+ "To submit the invoice without purchase receipt please set {0} as {1} in {2}"
+ ).format(
+ frappe.bold(_("Purchase Receipt Required")),
+ frappe.bold("No"),
+ get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"),
+ )
throw(msg, title=_("Mandatory Purchase Receipt"))
def validate_write_off_account(self):
@@ -351,56 +426,65 @@ class PurchaseInvoice(BuyingController):
throw(_("Please enter Write Off Account"))
def check_prev_docstatus(self):
- for d in self.get('items'):
+ for d in self.get("items"):
if d.purchase_order:
- submitted = frappe.db.sql("select name from `tabPurchase Order` where docstatus = 1 and name = %s", d.purchase_order)
+ submitted = frappe.db.sql(
+ "select name from `tabPurchase Order` where docstatus = 1 and name = %s", d.purchase_order
+ )
if not submitted:
frappe.throw(_("Purchase Order {0} is not submitted").format(d.purchase_order))
if d.purchase_receipt:
- submitted = frappe.db.sql("select name from `tabPurchase Receipt` where docstatus = 1 and name = %s", d.purchase_receipt)
+ submitted = frappe.db.sql(
+ "select name from `tabPurchase Receipt` where docstatus = 1 and name = %s", d.purchase_receipt
+ )
if not submitted:
frappe.throw(_("Purchase Receipt {0} is not submitted").format(d.purchase_receipt))
def update_status_updater_args(self):
if cint(self.update_stock):
- self.status_updater.append({
- 'source_dt': 'Purchase Invoice Item',
- 'target_dt': 'Purchase Order Item',
- 'join_field': 'po_detail',
- 'target_field': 'received_qty',
- 'target_parent_dt': 'Purchase Order',
- 'target_parent_field': 'per_received',
- 'target_ref_field': 'qty',
- 'source_field': 'received_qty',
- 'second_source_dt': 'Purchase Receipt Item',
- 'second_source_field': 'received_qty',
- 'second_join_field': 'purchase_order_item',
- 'percent_join_field':'purchase_order',
- 'overflow_type': 'receipt',
- 'extra_cond': """ and exists(select name from `tabPurchase Invoice`
- where name=`tabPurchase Invoice Item`.parent and update_stock = 1)"""
- })
+ self.status_updater.append(
+ {
+ "source_dt": "Purchase Invoice Item",
+ "target_dt": "Purchase Order Item",
+ "join_field": "po_detail",
+ "target_field": "received_qty",
+ "target_parent_dt": "Purchase Order",
+ "target_parent_field": "per_received",
+ "target_ref_field": "qty",
+ "source_field": "received_qty",
+ "second_source_dt": "Purchase Receipt Item",
+ "second_source_field": "received_qty",
+ "second_join_field": "purchase_order_item",
+ "percent_join_field": "purchase_order",
+ "overflow_type": "receipt",
+ "extra_cond": """ and exists(select name from `tabPurchase Invoice`
+ where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""",
+ }
+ )
if cint(self.is_return):
- self.status_updater.append({
- 'source_dt': 'Purchase Invoice Item',
- 'target_dt': 'Purchase Order Item',
- 'join_field': 'po_detail',
- 'target_field': 'returned_qty',
- 'source_field': '-1 * qty',
- 'second_source_dt': 'Purchase Receipt Item',
- 'second_source_field': '-1 * qty',
- 'second_join_field': 'purchase_order_item',
- 'overflow_type': 'receipt',
- 'extra_cond': """ and exists (select name from `tabPurchase Invoice`
- where name=`tabPurchase Invoice Item`.parent and update_stock=1 and is_return=1)"""
- })
+ self.status_updater.append(
+ {
+ "source_dt": "Purchase Invoice Item",
+ "target_dt": "Purchase Order Item",
+ "join_field": "po_detail",
+ "target_field": "returned_qty",
+ "source_field": "-1 * qty",
+ "second_source_dt": "Purchase Receipt Item",
+ "second_source_field": "-1 * qty",
+ "second_join_field": "purchase_order_item",
+ "overflow_type": "receipt",
+ "extra_cond": """ and exists (select name from `tabPurchase Invoice`
+ where name=`tabPurchase Invoice Item`.parent and update_stock=1 and is_return=1)""",
+ }
+ )
def validate_purchase_receipt_if_update_stock(self):
if self.update_stock:
for item in self.get("items"):
if item.purchase_receipt:
- frappe.throw(_("Stock cannot be updated against Purchase Receipt {0}")
- .format(item.purchase_receipt))
+ frappe.throw(
+ _("Stock cannot be updated against Purchase Receipt {0}").format(item.purchase_receipt)
+ )
def on_submit(self):
super(PurchaseInvoice, self).on_submit()
@@ -409,8 +493,9 @@ class PurchaseInvoice(BuyingController):
self.update_status_updater_args()
self.update_prevdoc_status()
- frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype,
- self.company, self.base_grand_total)
+ frappe.get_doc("Authorization Control").validate_approving_authority(
+ self.doctype, self.company, self.base_grand_total
+ )
if not self.is_return:
self.update_against_document_in_jv()
@@ -425,6 +510,7 @@ class PurchaseInvoice(BuyingController):
self.update_stock_ledger()
self.set_consumed_qty_in_po()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
+
update_serial_nos_after_submit(self, "items")
# this sequence because outstanding may get -negative
@@ -447,13 +533,23 @@ class PurchaseInvoice(BuyingController):
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
if self.docstatus == 1:
- make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost)
+ make_gl_entries(
+ gl_entries,
+ update_outstanding=update_outstanding,
+ merge_entries=False,
+ from_repost=from_repost,
+ )
elif self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
if update_outstanding == "No":
- update_outstanding_amt(self.credit_to, "Supplier", self.supplier,
- self.doctype, self.return_against if cint(self.is_return) and self.return_against else self.name)
+ update_outstanding_amt(
+ self.credit_to,
+ "Supplier",
+ self.supplier,
+ self.doctype,
+ self.return_against if cint(self.is_return) and self.return_against else self.name,
+ )
elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@@ -502,28 +598,41 @@ class PurchaseInvoice(BuyingController):
def make_supplier_gl_entry(self, gl_entries):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
- grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
- base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
- else self.base_grand_total, self.precision("base_grand_total"))
+ grand_total = (
+ self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
+ )
+ base_grand_total = flt(
+ self.base_rounded_total
+ if (self.base_rounding_adjustment and self.base_rounded_total)
+ else self.base_grand_total,
+ self.precision("base_grand_total"),
+ )
if grand_total and not self.is_internal_transfer():
- # Did not use base_grand_total to book rounding loss gle
- gl_entries.append(
- self.get_gl_dict({
+ # Did not use base_grand_total to book rounding loss gle
+ gl_entries.append(
+ self.get_gl_dict(
+ {
"account": self.credit_to,
"party_type": "Supplier",
"party": self.supplier,
"due_date": self.due_date,
"against": self.against_expense_account,
"credit": base_grand_total,
- "credit_in_account_currency": base_grand_total \
- if self.party_account_currency==self.company_currency else grand_total,
- "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
+ "credit_in_account_currency": base_grand_total
+ if self.party_account_currency == self.company_currency
+ else grand_total,
+ "against_voucher": self.return_against
+ if cint(self.is_return) and self.return_against
+ else self.name,
"against_voucher_type": self.doctype,
"project": self.project,
- "cost_center": self.cost_center
- }, self.party_account_currency, item=self)
+ "cost_center": self.cost_center,
+ },
+ self.party_account_currency,
+ item=self,
)
+ )
def make_item_gl_entries(self, gl_entries):
# item gl entries
@@ -535,19 +644,28 @@ class PurchaseInvoice(BuyingController):
voucher_wise_stock_value = {}
if self.update_stock:
- stock_ledger_entries = frappe.get_all("Stock Ledger Entry",
- fields = ["voucher_detail_no", "stock_value_difference", "warehouse"],
- filters={"voucher_no": self.name, "voucher_type": self.doctype, "is_cancelled": 0}
+ stock_ledger_entries = frappe.get_all(
+ "Stock Ledger Entry",
+ fields=["voucher_detail_no", "stock_value_difference", "warehouse"],
+ filters={"voucher_no": self.name, "voucher_type": self.doctype, "is_cancelled": 0},
)
for d in stock_ledger_entries:
- voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference)
+ voucher_wise_stock_value.setdefault(
+ (d.voucher_detail_no, d.warehouse), d.stock_value_difference
+ )
- valuation_tax_accounts = [d.account_head for d in self.get("taxes")
- if d.category in ('Valuation', 'Total and Valuation')
- and flt(d.base_tax_amount_after_discount_amount)]
+ valuation_tax_accounts = [
+ d.account_head
+ for d in self.get("taxes")
+ if d.category in ("Valuation", "Total and Valuation")
+ and flt(d.base_tax_amount_after_discount_amount)
+ ]
- provisional_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company,
- 'enable_provisional_accounting_for_non_stock_items'))
+ provisional_accounting_for_non_stock_items = cint(
+ frappe.db.get_value(
+ "Company", self.company, "enable_provisional_accounting_for_non_stock_items"
+ )
+ )
purchase_receipt_doc_map = {}
@@ -559,86 +677,122 @@ class PurchaseInvoice(BuyingController):
if self.update_stock and self.auto_accounting_for_stock and item.item_code in stock_items:
# warehouse account
- warehouse_debit_amount = self.make_stock_adjustment_entry(gl_entries,
- item, voucher_wise_stock_value, account_currency)
+ warehouse_debit_amount = self.make_stock_adjustment_entry(
+ gl_entries, item, voucher_wise_stock_value, account_currency
+ )
if item.from_warehouse:
- gl_entries.append(self.get_gl_dict({
- "account": warehouse_account[item.warehouse]['account'],
- "against": warehouse_account[item.from_warehouse]["account"],
- "cost_center": item.cost_center,
- "project": item.project or self.project,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "debit": warehouse_debit_amount,
- }, warehouse_account[item.warehouse]["account_currency"], item=item))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": warehouse_account[item.warehouse]["account"],
+ "against": warehouse_account[item.from_warehouse]["account"],
+ "cost_center": item.cost_center,
+ "project": item.project or self.project,
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
+ "debit": warehouse_debit_amount,
+ },
+ warehouse_account[item.warehouse]["account_currency"],
+ item=item,
+ )
+ )
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
- gl_entries.append(self.get_gl_dict({
- "account": warehouse_account[item.from_warehouse]['account'],
- "against": warehouse_account[item.warehouse]["account"],
- "cost_center": item.cost_center,
- "project": item.project or self.project,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")),
- }, warehouse_account[item.from_warehouse]["account_currency"], item=item))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": warehouse_account[item.from_warehouse]["account"],
+ "against": warehouse_account[item.warehouse]["account"],
+ "cost_center": item.cost_center,
+ "project": item.project or self.project,
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
+ "debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")),
+ },
+ warehouse_account[item.from_warehouse]["account_currency"],
+ item=item,
+ )
+ )
# Do not book expense for transfer within same company transfer
if not self.is_internal_transfer():
gl_entries.append(
- self.get_gl_dict({
- "account": item.expense_account,
- "against": self.supplier,
- "debit": flt(item.base_net_amount, item.precision("base_net_amount")),
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "cost_center": item.cost_center,
- "project": item.project
- }, account_currency, item=item)
+ self.get_gl_dict(
+ {
+ "account": item.expense_account,
+ "against": self.supplier,
+ "debit": flt(item.base_net_amount, item.precision("base_net_amount")),
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
+ "cost_center": item.cost_center,
+ "project": item.project,
+ },
+ account_currency,
+ item=item,
+ )
)
else:
if not self.is_internal_transfer():
gl_entries.append(
- self.get_gl_dict({
- "account": item.expense_account,
- "against": self.supplier,
- "debit": warehouse_debit_amount,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "cost_center": item.cost_center,
- "project": item.project or self.project
- }, account_currency, item=item)
+ self.get_gl_dict(
+ {
+ "account": item.expense_account,
+ "against": self.supplier,
+ "debit": warehouse_debit_amount,
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
+ "cost_center": item.cost_center,
+ "project": item.project or self.project,
+ },
+ account_currency,
+ item=item,
+ )
)
# Amount added through landed-cost-voucher
if landed_cost_entries:
for account, amount in iteritems(landed_cost_entries[(item.item_code, item.name)]):
- gl_entries.append(self.get_gl_dict({
- "account": account,
- "against": item.expense_account,
- "cost_center": item.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": flt(amount["base_amount"]),
- "credit_in_account_currency": flt(amount["amount"]),
- "project": item.project or self.project
- }, item=item))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": account,
+ "against": item.expense_account,
+ "cost_center": item.cost_center,
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
+ "credit": flt(amount["base_amount"]),
+ "credit_in_account_currency": flt(amount["amount"]),
+ "project": item.project or self.project,
+ },
+ item=item,
+ )
+ )
# sub-contracting warehouse
if flt(item.rm_supp_cost):
supplier_warehouse_account = warehouse_account[self.supplier_warehouse]["account"]
if not supplier_warehouse_account:
- frappe.throw(_("Please set account in Warehouse {0}")
- .format(self.supplier_warehouse))
- gl_entries.append(self.get_gl_dict({
- "account": supplier_warehouse_account,
- "against": item.expense_account,
- "cost_center": item.cost_center,
- "project": item.project or self.project,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": flt(item.rm_supp_cost)
- }, warehouse_account[self.supplier_warehouse]["account_currency"], item=item))
+ frappe.throw(_("Please set account in Warehouse {0}").format(self.supplier_warehouse))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": supplier_warehouse_account,
+ "against": item.expense_account,
+ "cost_center": item.cost_center,
+ "project": item.project or self.project,
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
+ "credit": flt(item.rm_supp_cost),
+ },
+ warehouse_account[self.supplier_warehouse]["account_currency"],
+ item=item,
+ )
+ )
- elif not item.is_fixed_asset or (item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category)):
- expense_account = (item.expense_account
- if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account)
+ elif not item.is_fixed_asset or (
+ item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category)
+ ):
+ expense_account = (
+ item.expense_account
+ if (not item.enable_deferred_expense or self.is_return)
+ else item.deferred_expense_account
+ )
if not item.is_fixed_asset:
dummy, amount = self.get_amount_and_base_amount(item, self.enable_discount_accounting)
@@ -647,7 +801,9 @@ class PurchaseInvoice(BuyingController):
if provisional_accounting_for_non_stock_items:
if item.purchase_receipt:
- provisional_account = self.get_company_default("default_provisional_account")
+ provisional_account = frappe.db.get_value(
+ "Purchase Receipt Item", item.pr_detail, "provisional_expense_account"
+ ) or self.get_company_default("default_provisional_account")
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
if not purchase_receipt_doc:
@@ -655,75 +811,115 @@ class PurchaseInvoice(BuyingController):
purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
- expense_booked_in_pr = frappe.db.get_value('GL Entry', {'is_cancelled': 0,
- 'voucher_type': 'Purchase Receipt', 'voucher_no': item.purchase_receipt, 'voucher_detail_no': item.pr_detail,
- 'account':provisional_account}, ['name'])
+ expense_booked_in_pr = frappe.db.get_value(
+ "GL Entry",
+ {
+ "is_cancelled": 0,
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": item.purchase_receipt,
+ "voucher_detail_no": item.pr_detail,
+ "account": provisional_account,
+ },
+ ["name"],
+ )
if expense_booked_in_pr:
# Intentionally passing purchase invoice item to handle partial billing
- purchase_receipt_doc.add_provisional_gl_entry(item, gl_entries, self.posting_date, reverse=1)
+ purchase_receipt_doc.add_provisional_gl_entry(
+ item, gl_entries, self.posting_date, provisional_account, reverse=1
+ )
if not self.is_internal_transfer():
- gl_entries.append(self.get_gl_dict({
- "account": expense_account,
- "against": self.supplier,
- "debit": amount,
- "cost_center": item.cost_center,
- "project": item.project or self.project
- }, account_currency, item=item))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": expense_account,
+ "against": self.supplier,
+ "debit": amount,
+ "cost_center": item.cost_center,
+ "project": item.project or self.project,
+ },
+ account_currency,
+ item=item,
+ )
+ )
# If asset is bought through this document and not linked to PR
if self.update_stock and item.landed_cost_voucher_amount:
- expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation")
+ expenses_included_in_asset_valuation = self.get_company_default(
+ "expenses_included_in_asset_valuation"
+ )
# Amount added through landed-cost-voucher
- gl_entries.append(self.get_gl_dict({
- "account": expenses_included_in_asset_valuation,
- "against": expense_account,
- "cost_center": item.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": flt(item.landed_cost_voucher_amount),
- "project": item.project or self.project
- }, item=item))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": expenses_included_in_asset_valuation,
+ "against": expense_account,
+ "cost_center": item.cost_center,
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
+ "credit": flt(item.landed_cost_voucher_amount),
+ "project": item.project or self.project,
+ },
+ item=item,
+ )
+ )
- gl_entries.append(self.get_gl_dict({
- "account": expense_account,
- "against": expenses_included_in_asset_valuation,
- "cost_center": item.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "debit": flt(item.landed_cost_voucher_amount),
- "project": item.project or self.project
- }, item=item))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": expense_account,
+ "against": expenses_included_in_asset_valuation,
+ "cost_center": item.cost_center,
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
+ "debit": flt(item.landed_cost_voucher_amount),
+ "project": item.project or self.project,
+ },
+ item=item,
+ )
+ )
# update gross amount of asset bought through this document
- assets = frappe.db.get_all('Asset',
- filters={ 'purchase_invoice': self.name, 'item_code': item.item_code }
+ assets = frappe.db.get_all(
+ "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
)
for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
- frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
+ frappe.db.set_value(
+ "Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)
+ )
- if self.auto_accounting_for_stock and self.is_opening == "No" and \
- item.item_code in stock_items and item.item_tax_amount:
- # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
- if item.purchase_receipt and valuation_tax_accounts:
- negative_expense_booked_in_pr = frappe.db.sql("""select name from `tabGL Entry`
+ if (
+ self.auto_accounting_for_stock
+ and self.is_opening == "No"
+ and item.item_code in stock_items
+ and item.item_tax_amount
+ ):
+ # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
+ if item.purchase_receipt and valuation_tax_accounts:
+ negative_expense_booked_in_pr = frappe.db.sql(
+ """select name from `tabGL Entry`
where voucher_type='Purchase Receipt' and voucher_no=%s and account in %s""",
- (item.purchase_receipt, valuation_tax_accounts))
+ (item.purchase_receipt, valuation_tax_accounts),
+ )
- if not negative_expense_booked_in_pr:
- gl_entries.append(
- self.get_gl_dict({
+ if not negative_expense_booked_in_pr:
+ gl_entries.append(
+ self.get_gl_dict(
+ {
"account": self.stock_received_but_not_billed,
"against": self.supplier,
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
"remarks": self.remarks or _("Accounting Entry for Stock"),
"cost_center": self.cost_center,
- "project": item.project or self.project
- }, item=item)
+ "project": item.project or self.project,
+ },
+ item=item,
)
+ )
- self.negative_expense_to_be_booked += flt(item.item_tax_amount, \
- item.precision("item_tax_amount"))
+ self.negative_expense_to_be_booked += flt(
+ item.item_tax_amount, item.precision("item_tax_amount")
+ )
def get_asset_gl_entry(self, gl_entries):
arbnb_account = self.get_company_default("asset_received_but_not_billed")
@@ -731,125 +927,179 @@ class PurchaseInvoice(BuyingController):
for item in self.get("items"):
if item.is_fixed_asset:
- asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate)
+ asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate)
base_asset_amount = flt(item.base_net_amount + item.item_tax_amount)
- item_exp_acc_type = frappe.db.get_value('Account', item.expense_account, 'account_type')
- if (not item.expense_account or item_exp_acc_type not in ['Asset Received But Not Billed', 'Fixed Asset']):
+ item_exp_acc_type = frappe.db.get_value("Account", item.expense_account, "account_type")
+ if not item.expense_account or item_exp_acc_type not in [
+ "Asset Received But Not Billed",
+ "Fixed Asset",
+ ]:
item.expense_account = arbnb_account
if not self.update_stock:
arbnb_currency = get_account_currency(item.expense_account)
- gl_entries.append(self.get_gl_dict({
- "account": item.expense_account,
- "against": self.supplier,
- "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
- "debit": base_asset_amount,
- "debit_in_account_currency": (base_asset_amount
- if arbnb_currency == self.company_currency else asset_amount),
- "cost_center": item.cost_center,
- "project": item.project or self.project
- }, item=item))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": item.expense_account,
+ "against": self.supplier,
+ "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
+ "debit": base_asset_amount,
+ "debit_in_account_currency": (
+ base_asset_amount if arbnb_currency == self.company_currency else asset_amount
+ ),
+ "cost_center": item.cost_center,
+ "project": item.project or self.project,
+ },
+ item=item,
+ )
+ )
if item.item_tax_amount:
asset_eiiav_currency = get_account_currency(eiiav_account)
- gl_entries.append(self.get_gl_dict({
- "account": eiiav_account,
- "against": self.supplier,
- "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
- "cost_center": item.cost_center,
- "project": item.project or self.project,
- "credit": item.item_tax_amount,
- "credit_in_account_currency": (item.item_tax_amount
- if asset_eiiav_currency == self.company_currency else
- item.item_tax_amount / self.conversion_rate)
- }, item=item))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": eiiav_account,
+ "against": self.supplier,
+ "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
+ "cost_center": item.cost_center,
+ "project": item.project or self.project,
+ "credit": item.item_tax_amount,
+ "credit_in_account_currency": (
+ item.item_tax_amount
+ if asset_eiiav_currency == self.company_currency
+ else item.item_tax_amount / self.conversion_rate
+ ),
+ },
+ item=item,
+ )
+ )
else:
- cwip_account = get_asset_account("capital_work_in_progress_account",
- asset_category=item.asset_category,company=self.company)
+ cwip_account = get_asset_account(
+ "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company
+ )
cwip_account_currency = get_account_currency(cwip_account)
- gl_entries.append(self.get_gl_dict({
- "account": cwip_account,
- "against": self.supplier,
- "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
- "debit": base_asset_amount,
- "debit_in_account_currency": (base_asset_amount
- if cwip_account_currency == self.company_currency else asset_amount),
- "cost_center": self.cost_center,
- "project": item.project or self.project
- }, item=item))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": cwip_account,
+ "against": self.supplier,
+ "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
+ "debit": base_asset_amount,
+ "debit_in_account_currency": (
+ base_asset_amount if cwip_account_currency == self.company_currency else asset_amount
+ ),
+ "cost_center": self.cost_center,
+ "project": item.project or self.project,
+ },
+ item=item,
+ )
+ )
if item.item_tax_amount and not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
asset_eiiav_currency = get_account_currency(eiiav_account)
- gl_entries.append(self.get_gl_dict({
- "account": eiiav_account,
- "against": self.supplier,
- "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
- "cost_center": item.cost_center,
- "credit": item.item_tax_amount,
- "project": item.project or self.project,
- "credit_in_account_currency": (item.item_tax_amount
- if asset_eiiav_currency == self.company_currency else
- item.item_tax_amount / self.conversion_rate)
- }, item=item))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": eiiav_account,
+ "against": self.supplier,
+ "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
+ "cost_center": item.cost_center,
+ "credit": item.item_tax_amount,
+ "project": item.project or self.project,
+ "credit_in_account_currency": (
+ item.item_tax_amount
+ if asset_eiiav_currency == self.company_currency
+ else item.item_tax_amount / self.conversion_rate
+ ),
+ },
+ item=item,
+ )
+ )
# When update stock is checked
# Assets are bought through this document then it will be linked to this document
if self.update_stock:
if flt(item.landed_cost_voucher_amount):
- gl_entries.append(self.get_gl_dict({
- "account": eiiav_account,
- "against": cwip_account,
- "cost_center": item.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": flt(item.landed_cost_voucher_amount),
- "project": item.project or self.project
- }, item=item))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": eiiav_account,
+ "against": cwip_account,
+ "cost_center": item.cost_center,
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
+ "credit": flt(item.landed_cost_voucher_amount),
+ "project": item.project or self.project,
+ },
+ item=item,
+ )
+ )
- gl_entries.append(self.get_gl_dict({
- "account": cwip_account,
- "against": eiiav_account,
- "cost_center": item.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "debit": flt(item.landed_cost_voucher_amount),
- "project": item.project or self.project
- }, item=item))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": cwip_account,
+ "against": eiiav_account,
+ "cost_center": item.cost_center,
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
+ "debit": flt(item.landed_cost_voucher_amount),
+ "project": item.project or self.project,
+ },
+ item=item,
+ )
+ )
# update gross amount of assets bought through this document
- assets = frappe.db.get_all('Asset',
- filters={ 'purchase_invoice': self.name, 'item_code': item.item_code }
+ assets = frappe.db.get_all(
+ "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
)
for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
- frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
+ frappe.db.set_value(
+ "Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)
+ )
return gl_entries
- def make_stock_adjustment_entry(self, gl_entries, item, voucher_wise_stock_value, account_currency):
+ def make_stock_adjustment_entry(
+ self, gl_entries, item, voucher_wise_stock_value, account_currency
+ ):
net_amt_precision = item.precision("base_net_amount")
val_rate_db_precision = 6 if cint(item.precision("valuation_rate")) <= 6 else 9
- warehouse_debit_amount = flt(flt(item.valuation_rate, val_rate_db_precision)
- * flt(item.qty) * flt(item.conversion_factor), net_amt_precision)
+ warehouse_debit_amount = flt(
+ flt(item.valuation_rate, val_rate_db_precision) * flt(item.qty) * flt(item.conversion_factor),
+ net_amt_precision,
+ )
# Stock ledger value is not matching with the warehouse amount
- if (self.update_stock and voucher_wise_stock_value.get(item.name) and
- warehouse_debit_amount != flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)):
+ if (
+ self.update_stock
+ and voucher_wise_stock_value.get(item.name)
+ and warehouse_debit_amount
+ != flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
+ ):
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
stock_amount = flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
stock_adjustment_amt = warehouse_debit_amount - stock_amount
gl_entries.append(
- self.get_gl_dict({
- "account": cost_of_goods_sold_account,
- "against": item.expense_account,
- "debit": stock_adjustment_amt,
- "remarks": self.get("remarks") or _("Stock Adjustment"),
- "cost_center": item.cost_center,
- "project": item.project or self.project
- }, account_currency, item=item)
+ self.get_gl_dict(
+ {
+ "account": cost_of_goods_sold_account,
+ "against": item.expense_account,
+ "debit": stock_adjustment_amt,
+ "remarks": self.get("remarks") or _("Stock Adjustment"),
+ "cost_center": item.cost_center,
+ "project": item.project or self.project,
+ },
+ account_currency,
+ item=item,
+ )
)
warehouse_debit_amount = stock_amount
@@ -868,24 +1118,35 @@ class PurchaseInvoice(BuyingController):
dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
gl_entries.append(
- self.get_gl_dict({
- "account": tax.account_head,
- "against": self.supplier,
- dr_or_cr: base_amount,
- dr_or_cr + "_in_account_currency": base_amount
- if account_currency==self.company_currency
+ self.get_gl_dict(
+ {
+ "account": tax.account_head,
+ "against": self.supplier,
+ dr_or_cr: base_amount,
+ dr_or_cr + "_in_account_currency": base_amount
+ if account_currency == self.company_currency
else amount,
- "cost_center": tax.cost_center
- }, account_currency, item=tax)
+ "cost_center": tax.cost_center,
+ },
+ account_currency,
+ item=tax,
+ )
)
# accumulate valuation tax
- if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(base_amount) \
- and not self.is_internal_transfer():
+ if (
+ self.is_opening == "No"
+ and tax.category in ("Valuation", "Valuation and Total")
+ and flt(base_amount)
+ and not self.is_internal_transfer()
+ ):
if self.auto_accounting_for_stock and not tax.cost_center:
- frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category)))
+ frappe.throw(
+ _("Cost Center is required in row {0} in Taxes table for type {1}").format(
+ tax.idx, _(tax.category)
+ )
+ )
valuation_tax.setdefault(tax.name, 0)
- valuation_tax[tax.name] += \
- (tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount)
+ valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount)
if self.is_opening == "No" and self.negative_expense_to_be_booked and valuation_tax:
# credit valuation tax amount in "Expenses Included In Valuation"
@@ -899,17 +1160,22 @@ class PurchaseInvoice(BuyingController):
if i == len(valuation_tax):
applicable_amount = amount_including_divisional_loss
else:
- applicable_amount = self.negative_expense_to_be_booked * (valuation_tax[tax.name] / total_valuation_amount)
+ applicable_amount = self.negative_expense_to_be_booked * (
+ valuation_tax[tax.name] / total_valuation_amount
+ )
amount_including_divisional_loss -= applicable_amount
gl_entries.append(
- self.get_gl_dict({
- "account": tax.account_head,
- "cost_center": tax.cost_center,
- "against": self.supplier,
- "credit": applicable_amount,
- "remarks": self.remarks or _("Accounting Entry for Stock"),
- }, item=tax)
+ self.get_gl_dict(
+ {
+ "account": tax.account_head,
+ "cost_center": tax.cost_center,
+ "against": self.supplier,
+ "credit": applicable_amount,
+ "remarks": self.remarks or _("Accounting Entry for Stock"),
+ },
+ item=tax,
+ )
)
i += 1
@@ -918,18 +1184,24 @@ class PurchaseInvoice(BuyingController):
for tax in self.get("taxes"):
if valuation_tax.get(tax.name):
gl_entries.append(
- self.get_gl_dict({
- "account": tax.account_head,
- "cost_center": tax.cost_center,
- "against": self.supplier,
- "credit": valuation_tax[tax.name],
- "remarks": self.remarks or _("Accounting Entry for Stock")
- }, item=tax))
+ self.get_gl_dict(
+ {
+ "account": tax.account_head,
+ "cost_center": tax.cost_center,
+ "against": self.supplier,
+ "credit": valuation_tax[tax.name],
+ "remarks": self.remarks or _("Accounting Entry for Stock"),
+ },
+ item=tax,
+ )
+ )
@property
def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"):
- self._enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
+ self._enable_discount_accounting = cint(
+ frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting")
+ )
return self._enable_discount_accounting
@@ -937,13 +1209,18 @@ class PurchaseInvoice(BuyingController):
if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges):
account_currency = get_account_currency(self.unrealized_profit_loss_account)
gl_entries.append(
- self.get_gl_dict({
- "account": self.unrealized_profit_loss_account,
- "against": self.supplier,
- "credit": flt(self.total_taxes_and_charges),
- "credit_in_account_currency": flt(self.base_total_taxes_and_charges),
- "cost_center": self.cost_center
- }, account_currency, item=self))
+ self.get_gl_dict(
+ {
+ "account": self.unrealized_profit_loss_account,
+ "against": self.supplier,
+ "credit": flt(self.total_taxes_and_charges),
+ "credit_in_account_currency": flt(self.base_total_taxes_and_charges),
+ "cost_center": self.cost_center,
+ },
+ account_currency,
+ item=self,
+ )
+ )
def make_payment_gl_entries(self, gl_entries):
# Make Cash GL Entries
@@ -951,30 +1228,42 @@ class PurchaseInvoice(BuyingController):
bank_account_currency = get_account_currency(self.cash_bank_account)
# CASH, make payment entries
gl_entries.append(
- self.get_gl_dict({
- "account": self.credit_to,
- "party_type": "Supplier",
- "party": self.supplier,
- "against": self.cash_bank_account,
- "debit": self.base_paid_amount,
- "debit_in_account_currency": self.base_paid_amount \
- if self.party_account_currency==self.company_currency else self.paid_amount,
- "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
- "against_voucher_type": self.doctype,
- "cost_center": self.cost_center,
- "project": self.project
- }, self.party_account_currency, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.credit_to,
+ "party_type": "Supplier",
+ "party": self.supplier,
+ "against": self.cash_bank_account,
+ "debit": self.base_paid_amount,
+ "debit_in_account_currency": self.base_paid_amount
+ if self.party_account_currency == self.company_currency
+ else self.paid_amount,
+ "against_voucher": self.return_against
+ if cint(self.is_return) and self.return_against
+ else self.name,
+ "against_voucher_type": self.doctype,
+ "cost_center": self.cost_center,
+ "project": self.project,
+ },
+ self.party_account_currency,
+ item=self,
+ )
)
gl_entries.append(
- self.get_gl_dict({
- "account": self.cash_bank_account,
- "against": self.supplier,
- "credit": self.base_paid_amount,
- "credit_in_account_currency": self.base_paid_amount \
- if bank_account_currency==self.company_currency else self.paid_amount,
- "cost_center": self.cost_center
- }, bank_account_currency, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.cash_bank_account,
+ "against": self.supplier,
+ "credit": self.base_paid_amount,
+ "credit_in_account_currency": self.base_paid_amount
+ if bank_account_currency == self.company_currency
+ else self.paid_amount,
+ "cost_center": self.cost_center,
+ },
+ bank_account_currency,
+ item=self,
+ )
)
def make_write_off_gl_entry(self, gl_entries):
@@ -984,48 +1273,66 @@ class PurchaseInvoice(BuyingController):
write_off_account_currency = get_account_currency(self.write_off_account)
gl_entries.append(
- self.get_gl_dict({
- "account": self.credit_to,
- "party_type": "Supplier",
- "party": self.supplier,
- "against": self.write_off_account,
- "debit": self.base_write_off_amount,
- "debit_in_account_currency": self.base_write_off_amount \
- if self.party_account_currency==self.company_currency else self.write_off_amount,
- "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
- "against_voucher_type": self.doctype,
- "cost_center": self.cost_center,
- "project": self.project
- }, self.party_account_currency, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.credit_to,
+ "party_type": "Supplier",
+ "party": self.supplier,
+ "against": self.write_off_account,
+ "debit": self.base_write_off_amount,
+ "debit_in_account_currency": self.base_write_off_amount
+ if self.party_account_currency == self.company_currency
+ else self.write_off_amount,
+ "against_voucher": self.return_against
+ if cint(self.is_return) and self.return_against
+ else self.name,
+ "against_voucher_type": self.doctype,
+ "cost_center": self.cost_center,
+ "project": self.project,
+ },
+ self.party_account_currency,
+ item=self,
+ )
)
gl_entries.append(
- self.get_gl_dict({
- "account": self.write_off_account,
- "against": self.supplier,
- "credit": flt(self.base_write_off_amount),
- "credit_in_account_currency": self.base_write_off_amount \
- if write_off_account_currency==self.company_currency else self.write_off_amount,
- "cost_center": self.cost_center or self.write_off_cost_center
- }, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.write_off_account,
+ "against": self.supplier,
+ "credit": flt(self.base_write_off_amount),
+ "credit_in_account_currency": self.base_write_off_amount
+ if write_off_account_currency == self.company_currency
+ else self.write_off_amount,
+ "cost_center": self.cost_center or self.write_off_cost_center,
+ },
+ item=self,
+ )
)
def make_gle_for_rounding_adjustment(self, gl_entries):
# if rounding adjustment in small and conversion rate is also small then
# base_rounding_adjustment may become zero due to small precision
# eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2
- # then base_rounding_adjustment becomes zero and error is thrown in GL Entry
- if not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment:
- round_off_account, round_off_cost_center = \
- get_round_off_account_and_cost_center(self.company)
+ # then base_rounding_adjustment becomes zero and error is thrown in GL Entry
+ if (
+ not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment
+ ):
+ round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
+ self.company, "Purchase Invoice", self.name
+ )
gl_entries.append(
- self.get_gl_dict({
- "account": round_off_account,
- "against": self.supplier,
- "debit_in_account_currency": self.rounding_adjustment,
- "debit": self.base_rounding_adjustment,
- "cost_center": self.cost_center or round_off_cost_center,
- }, item=self))
+ self.get_gl_dict(
+ {
+ "account": round_off_account,
+ "against": self.supplier,
+ "debit_in_account_currency": self.rounding_adjustment,
+ "debit": self.base_rounding_adjustment,
+ "cost_center": self.cost_center or round_off_cost_center,
+ },
+ item=self,
+ )
+ )
def on_cancel(self):
check_if_return_invoice_linked_with_payment_entry(self)
@@ -1056,10 +1363,10 @@ class PurchaseInvoice(BuyingController):
self.repost_future_sle_and_gle()
self.update_project()
- frappe.db.set(self, 'status', 'Cancelled')
+ frappe.db.set(self, "status", "Cancelled")
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.update_advance_tax_references(cancel=1)
def update_project(self):
@@ -1080,19 +1387,22 @@ class PurchaseInvoice(BuyingController):
if cint(frappe.db.get_single_value("Accounts Settings", "check_supplier_invoice_uniqueness")):
fiscal_year = get_fiscal_year(self.posting_date, company=self.company, as_dict=True)
- pi = frappe.db.sql('''select name from `tabPurchase Invoice`
+ pi = frappe.db.sql(
+ """select name from `tabPurchase Invoice`
where
bill_no = %(bill_no)s
and supplier = %(supplier)s
and name != %(name)s
and docstatus < 2
- and posting_date between %(year_start_date)s and %(year_end_date)s''', {
- "bill_no": self.bill_no,
- "supplier": self.supplier,
- "name": self.name,
- "year_start_date": fiscal_year.year_start_date,
- "year_end_date": fiscal_year.year_end_date
- })
+ and posting_date between %(year_start_date)s and %(year_end_date)s""",
+ {
+ "bill_no": self.bill_no,
+ "supplier": self.supplier,
+ "name": self.name,
+ "year_start_date": fiscal_year.year_start_date,
+ "year_end_date": fiscal_year.year_end_date,
+ },
+ )
if pi:
pi = pi[0][0]
@@ -1102,16 +1412,26 @@ class PurchaseInvoice(BuyingController):
updated_pr = []
for d in self.get("items"):
if d.pr_detail:
- billed_amt = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item`
- where pr_detail=%s and docstatus=1""", d.pr_detail)
+ billed_amt = frappe.db.sql(
+ """select sum(amount) from `tabPurchase Invoice Item`
+ where pr_detail=%s and docstatus=1""",
+ d.pr_detail,
+ )
billed_amt = billed_amt and billed_amt[0][0] or 0
- frappe.db.set_value("Purchase Receipt Item", d.pr_detail, "billed_amt", billed_amt, update_modified=update_modified)
+ frappe.db.set_value(
+ "Purchase Receipt Item",
+ d.pr_detail,
+ "billed_amt",
+ billed_amt,
+ update_modified=update_modified,
+ )
updated_pr.append(d.purchase_receipt)
elif d.po_detail:
updated_pr += update_billed_amount_based_on_po(d.po_detail, update_modified)
for pr in set(updated_pr):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage
+
pr_doc = frappe.get_doc("Purchase Receipt", pr)
update_billing_percentage(pr_doc, update_modified=update_modified)
@@ -1119,25 +1439,29 @@ class PurchaseInvoice(BuyingController):
self.due_date = None
def block_invoice(self, hold_comment=None, release_date=None):
- self.db_set('on_hold', 1)
- self.db_set('hold_comment', cstr(hold_comment))
- self.db_set('release_date', release_date)
+ self.db_set("on_hold", 1)
+ self.db_set("hold_comment", cstr(hold_comment))
+ self.db_set("release_date", release_date)
def unblock_invoice(self):
- self.db_set('on_hold', 0)
- self.db_set('release_date', None)
+ self.db_set("on_hold", 0)
+ self.db_set("release_date", None)
def set_tax_withholding(self):
if not self.apply_tds:
return
- if self.apply_tds and not self.get('tax_withholding_category'):
- self.tax_withholding_category = frappe.db.get_value('Supplier', self.supplier, 'tax_withholding_category')
+ if self.apply_tds and not self.get("tax_withholding_category"):
+ self.tax_withholding_category = frappe.db.get_value(
+ "Supplier", self.supplier, "tax_withholding_category"
+ )
if not self.tax_withholding_category:
return
- tax_withholding_details, advance_taxes = get_party_tax_withholding_details(self, self.tax_withholding_category)
+ tax_withholding_details, advance_taxes = get_party_tax_withholding_details(
+ self, self.tax_withholding_category
+ )
# Adjust TDS paid on advances
self.allocate_advance_tds(tax_withholding_details, advance_taxes)
@@ -1155,8 +1479,11 @@ class PurchaseInvoice(BuyingController):
if not accounts or tax_withholding_details.get("account_head") not in accounts:
self.append("taxes", tax_withholding_details)
- to_remove = [d for d in self.taxes
- if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")]
+ to_remove = [
+ d
+ for d in self.taxes
+ if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")
+ ]
for d in to_remove:
self.remove(d)
@@ -1165,27 +1492,33 @@ class PurchaseInvoice(BuyingController):
self.calculate_taxes_and_totals()
def allocate_advance_tds(self, tax_withholding_details, advance_taxes):
- self.set('advance_tax', [])
+ self.set("advance_tax", [])
for tax in advance_taxes:
allocated_amount = 0
pending_amount = flt(tax.tax_amount - tax.allocated_amount)
- if flt(tax_withholding_details.get('tax_amount')) >= pending_amount:
- tax_withholding_details['tax_amount'] -= pending_amount
+ if flt(tax_withholding_details.get("tax_amount")) >= pending_amount:
+ tax_withholding_details["tax_amount"] -= pending_amount
allocated_amount = pending_amount
- elif flt(tax_withholding_details.get('tax_amount')) and flt(tax_withholding_details.get('tax_amount')) < pending_amount:
- allocated_amount = tax_withholding_details['tax_amount']
- tax_withholding_details['tax_amount'] = 0
+ elif (
+ flt(tax_withholding_details.get("tax_amount"))
+ and flt(tax_withholding_details.get("tax_amount")) < pending_amount
+ ):
+ allocated_amount = tax_withholding_details["tax_amount"]
+ tax_withholding_details["tax_amount"] = 0
- self.append('advance_tax', {
- 'reference_type': 'Payment Entry',
- 'reference_name': tax.parent,
- 'reference_detail': tax.name,
- 'account_head': tax.account_head,
- 'allocated_amount': allocated_amount
- })
+ self.append(
+ "advance_tax",
+ {
+ "reference_type": "Payment Entry",
+ "reference_name": tax.parent,
+ "reference_detail": tax.name,
+ "account_head": tax.account_head,
+ "allocated_amount": allocated_amount,
+ },
+ )
def update_advance_tax_references(self, cancel=0):
- for tax in self.get('advance_tax'):
+ for tax in self.get("advance_tax"):
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
if cancel:
@@ -1199,8 +1532,8 @@ class PurchaseInvoice(BuyingController):
def set_status(self, update=False, status=None, update_modified=True):
if self.is_new():
- if self.get('amended_from'):
- self.status = 'Draft'
+ if self.get("amended_from"):
+ self.status = "Draft"
return
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
@@ -1211,19 +1544,25 @@ class PurchaseInvoice(BuyingController):
status = "Cancelled"
elif self.docstatus == 1:
if self.is_internal_transfer():
- self.status = 'Internal Transfer'
+ self.status = "Internal Transfer"
elif is_overdue(self, total):
self.status = "Overdue"
elif 0 < outstanding_amount < total:
self.status = "Partly Paid"
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
self.status = "Unpaid"
- #Check if outstanding amount is 0 due to debit note issued against invoice
- elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
+ # Check if outstanding amount is 0 due to debit note issued against invoice
+ elif (
+ outstanding_amount <= 0
+ and self.is_return == 0
+ and frappe.db.get_value(
+ "Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
+ )
+ ):
self.status = "Debit Note Issued"
elif self.is_return == 1:
self.status = "Return"
- elif outstanding_amount<=0:
+ elif outstanding_amount <= 0:
self.status = "Paid"
else:
self.status = "Submitted"
@@ -1231,76 +1570,86 @@ class PurchaseInvoice(BuyingController):
self.status = "Draft"
if update:
- self.db_set('status', self.status, update_modified = update_modified)
+ self.db_set("status", self.status, update_modified=update_modified)
+
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
+
list_context = get_list_context(context)
- list_context.update({
- 'show_sidebar': True,
- 'show_search': True,
- 'no_breadcrumbs': True,
- 'title': _('Purchase Invoices'),
- })
+ list_context.update(
+ {
+ "show_sidebar": True,
+ "show_search": True,
+ "no_breadcrumbs": True,
+ "title": _("Purchase Invoices"),
+ }
+ )
return list_context
+
@erpnext.allow_regional
def make_regional_gl_entries(gl_entries, doc):
return gl_entries
+
@frappe.whitelist()
def make_debit_note(source_name, target_doc=None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
+
return make_return_doc("Purchase Invoice", source_name, target_doc)
+
@frappe.whitelist()
def make_stock_entry(source_name, target_doc=None):
- doc = get_mapped_doc("Purchase Invoice", source_name, {
- "Purchase Invoice": {
- "doctype": "Stock Entry",
- "validation": {
- "docstatus": ["=", 1]
- }
- },
- "Purchase Invoice Item": {
- "doctype": "Stock Entry Detail",
- "field_map": {
- "stock_qty": "transfer_qty",
- "batch_no": "batch_no"
+ doc = get_mapped_doc(
+ "Purchase Invoice",
+ source_name,
+ {
+ "Purchase Invoice": {"doctype": "Stock Entry", "validation": {"docstatus": ["=", 1]}},
+ "Purchase Invoice Item": {
+ "doctype": "Stock Entry Detail",
+ "field_map": {"stock_qty": "transfer_qty", "batch_no": "batch_no"},
},
- }
- }, target_doc)
+ },
+ target_doc,
+ )
return doc
+
@frappe.whitelist()
def change_release_date(name, release_date=None):
- if frappe.db.exists('Purchase Invoice', name):
- pi = frappe.get_doc('Purchase Invoice', name)
- pi.db_set('release_date', release_date)
+ if frappe.db.exists("Purchase Invoice", name):
+ pi = frappe.get_doc("Purchase Invoice", name)
+ pi.db_set("release_date", release_date)
@frappe.whitelist()
def unblock_invoice(name):
- if frappe.db.exists('Purchase Invoice', name):
- pi = frappe.get_doc('Purchase Invoice', name)
+ if frappe.db.exists("Purchase Invoice", name):
+ pi = frappe.get_doc("Purchase Invoice", name)
pi.unblock_invoice()
@frappe.whitelist()
def block_invoice(name, release_date, hold_comment=None):
- if frappe.db.exists('Purchase Invoice', name):
- pi = frappe.get_doc('Purchase Invoice', name)
+ if frappe.db.exists("Purchase Invoice", name):
+ pi = frappe.get_doc("Purchase Invoice", name)
pi.block_invoice(hold_comment, release_date)
+
@frappe.whitelist()
def make_inter_company_sales_invoice(source_name, target_doc=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
+
return make_inter_company_transaction("Purchase Invoice", source_name, target_doc)
+
def on_doctype_update():
frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"])
+
@frappe.whitelist()
def make_purchase_receipt(source_name, target_doc=None):
def update_item(obj, target, source_parent):
@@ -1308,33 +1657,37 @@ def make_purchase_receipt(source_name, target_doc=None):
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor)
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
- target.base_amount = (flt(obj.qty) - flt(obj.received_qty)) * \
- flt(obj.rate) * flt(source_parent.conversion_rate)
+ target.base_amount = (
+ (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
+ )
- doc = get_mapped_doc("Purchase Invoice", source_name, {
- "Purchase Invoice": {
- "doctype": "Purchase Receipt",
- "validation": {
- "docstatus": ["=", 1],
- }
- },
- "Purchase Invoice Item": {
- "doctype": "Purchase Receipt Item",
- "field_map": {
- "name": "purchase_invoice_item",
- "parent": "purchase_invoice",
- "bom": "bom",
- "purchase_order": "purchase_order",
- "po_detail": "purchase_order_item",
- "material_request": "material_request",
- "material_request_item": "material_request_item"
+ doc = get_mapped_doc(
+ "Purchase Invoice",
+ source_name,
+ {
+ "Purchase Invoice": {
+ "doctype": "Purchase Receipt",
+ "validation": {
+ "docstatus": ["=", 1],
+ },
},
- "postprocess": update_item,
- "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
+ "Purchase Invoice Item": {
+ "doctype": "Purchase Receipt Item",
+ "field_map": {
+ "name": "purchase_invoice_item",
+ "parent": "purchase_invoice",
+ "bom": "bom",
+ "purchase_order": "purchase_order",
+ "po_detail": "purchase_order_item",
+ "material_request": "material_request",
+ "material_request_item": "material_request_item",
+ },
+ "postprocess": update_item,
+ "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty),
+ },
+ "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
},
- "Purchase Taxes and Charges": {
- "doctype": "Purchase Taxes and Charges"
- }
- }, target_doc)
+ target_doc,
+ )
return doc
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py
index f1878b480ed..10dd0ef6e25 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py
@@ -1,38 +1,28 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'purchase_invoice',
- 'non_standard_fieldnames': {
- 'Journal Entry': 'reference_name',
- 'Payment Entry': 'reference_name',
- 'Payment Request': 'reference_name',
- 'Landed Cost Voucher': 'receipt_document',
- 'Purchase Invoice': 'return_against',
- 'Auto Repeat': 'reference_document'
+ "fieldname": "purchase_invoice",
+ "non_standard_fieldnames": {
+ "Journal Entry": "reference_name",
+ "Payment Entry": "reference_name",
+ "Payment Request": "reference_name",
+ "Landed Cost Voucher": "receipt_document",
+ "Purchase Invoice": "return_against",
+ "Auto Repeat": "reference_document",
},
- 'internal_links': {
- 'Purchase Order': ['items', 'purchase_order'],
- 'Purchase Receipt': ['items', 'purchase_receipt'],
+ "internal_links": {
+ "Purchase Order": ["items", "purchase_order"],
+ "Purchase Receipt": ["items", "purchase_receipt"],
},
- 'transactions': [
+ "transactions": [
+ {"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]},
{
- 'label': _('Payment'),
- 'items': ['Payment Entry', 'Payment Request', 'Journal Entry']
+ "label": _("Reference"),
+ "items": ["Purchase Order", "Purchase Receipt", "Asset", "Landed Cost Voucher"],
},
- {
- 'label': _('Reference'),
- 'items': ['Purchase Order', 'Purchase Receipt', 'Asset', 'Landed Cost Voucher']
- },
- {
- 'label': _('Returns'),
- 'items': ['Purchase Invoice']
- },
- {
- 'label': _('Subscription'),
- 'items': ['Auto Repeat']
- },
- ]
+ {"label": _("Returns"), "items": ["Purchase Invoice"]},
+ {"label": _("Subscription"), "items": ["Auto Repeat"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 1d2dcdf2776..15803b5bfe1 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -2,7 +2,6 @@
# License: GNU General Public License v3. See license.txt
-
import unittest
import frappe
@@ -31,6 +30,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_tra
test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"]
test_ignore = ["Serial No"]
+
class TestPurchaseInvoice(unittest.TestCase):
@classmethod
def setUpClass(self):
@@ -43,16 +43,18 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_purchase_invoice_received_qty(self):
"""
- 1. Test if received qty is validated against accepted + rejected
- 2. Test if received qty is auto set on save
+ 1. Test if received qty is validated against accepted + rejected
+ 2. Test if received qty is auto set on save
"""
pi = make_purchase_invoice(
qty=1,
rejected_qty=1,
received_qty=3,
item_code="_Test Item Home Desktop 200",
- rejected_warehouse = "_Test Rejected Warehouse - _TC",
- update_stock=True, do_not_save=True)
+ rejected_warehouse="_Test Rejected Warehouse - _TC",
+ update_stock=True,
+ do_not_save=True,
+ )
self.assertRaises(QtyMismatchError, pi.save)
pi.items[0].received_qty = 0
@@ -79,18 +81,26 @@ class TestPurchaseInvoice(unittest.TestCase):
"_Test Account CST - _TC": [29.88, 0],
"_Test Account VAT - _TC": [156.25, 0],
"_Test Account Discount - _TC": [0, 168.03],
- "Round Off - _TC": [0, 0.3]
+ "Round Off - _TC": [0, 0.3],
}
- gl_entries = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
- where voucher_type = 'Purchase Invoice' and voucher_no = %s""", pi.name, as_dict=1)
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit from `tabGL Entry`
+ where voucher_type = 'Purchase Invoice' and voucher_no = %s""",
+ pi.name,
+ as_dict=1,
+ )
for d in gl_entries:
self.assertEqual([d.debit, d.credit], expected_gl_entries.get(d.account))
def test_gl_entries_with_perpetual_inventory(self):
- pi = make_purchase_invoice(company="_Test Company with perpetual inventory",
- warehouse= "Stores - TCP1", cost_center = "Main - TCP1",
- expense_account ="_Test Account Cost for Goods Sold - TCP1",
- get_taxes_and_charges=True, qty=10)
+ pi = make_purchase_invoice(
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ expense_account="_Test Account Cost for Goods Sold - TCP1",
+ get_taxes_and_charges=True,
+ qty=10,
+ )
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1)
@@ -104,6 +114,7 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_payment_entry_unlink_against_purchase_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
+
unlink_payment_on_cancel_of_invoice(0)
pi_doc = make_purchase_invoice()
@@ -119,7 +130,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pe.save(ignore_permissions=True)
pe.submit()
- pi_doc = frappe.get_doc('Purchase Invoice', pi_doc.name)
+ pi_doc = frappe.get_doc("Purchase Invoice", pi_doc.name)
pi_doc.load_from_db()
self.assertTrue(pi_doc.status, "Paid")
@@ -127,7 +138,7 @@ class TestPurchaseInvoice(unittest.TestCase):
unlink_payment_on_cancel_of_invoice()
def test_purchase_invoice_for_blocked_supplier(self):
- supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1
supplier.save()
@@ -137,9 +148,9 @@ class TestPurchaseInvoice(unittest.TestCase):
supplier.save()
def test_purchase_invoice_for_blocked_supplier_invoice(self):
- supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1
- supplier.hold_type = 'Invoices'
+ supplier.hold_type = "Invoices"
supplier.save()
self.assertRaises(frappe.ValidationError, make_purchase_invoice)
@@ -148,31 +159,40 @@ class TestPurchaseInvoice(unittest.TestCase):
supplier.save()
def test_purchase_invoice_for_blocked_supplier_payment(self):
- supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1
- supplier.hold_type = 'Payments'
+ supplier.hold_type = "Payments"
supplier.save()
pi = make_purchase_invoice()
self.assertRaises(
- frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name, bank_account="_Test Bank - _TC")
+ frappe.ValidationError,
+ get_payment_entry,
+ dt="Purchase Invoice",
+ dn=pi.name,
+ bank_account="_Test Bank - _TC",
+ )
supplier.on_hold = 0
supplier.save()
def test_purchase_invoice_for_blocked_supplier_payment_today_date(self):
- supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1
- supplier.hold_type = 'Payments'
+ supplier.hold_type = "Payments"
supplier.release_date = nowdate()
supplier.save()
pi = make_purchase_invoice()
self.assertRaises(
- frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name,
- bank_account="_Test Bank - _TC")
+ frappe.ValidationError,
+ get_payment_entry,
+ dt="Purchase Invoice",
+ dn=pi.name,
+ bank_account="_Test Bank - _TC",
+ )
supplier.on_hold = 0
supplier.save()
@@ -181,15 +201,15 @@ class TestPurchaseInvoice(unittest.TestCase):
# this test is meant to fail only if something fails in the try block
with self.assertRaises(Exception):
try:
- supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1
- supplier.hold_type = 'Payments'
- supplier.release_date = '2018-03-01'
+ supplier.hold_type = "Payments"
+ supplier.release_date = "2018-03-01"
supplier.save()
pi = make_purchase_invoice()
- get_payment_entry('Purchase Invoice', dn=pi.name, bank_account="_Test Bank - _TC")
+ get_payment_entry("Purchase Invoice", dn=pi.name, bank_account="_Test Bank - _TC")
supplier.on_hold = 0
supplier.save()
@@ -203,7 +223,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.release_date = nowdate()
self.assertRaises(frappe.ValidationError, pi.save)
- pi.release_date = ''
+ pi.release_date = ""
pi.save()
def test_purchase_invoice_temporary_blocked(self):
@@ -212,7 +232,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.save()
pi.submit()
- pe = get_payment_entry('Purchase Invoice', dn=pi.name, bank_account="_Test Bank - _TC")
+ pe = get_payment_entry("Purchase Invoice", dn=pi.name, bank_account="_Test Bank - _TC")
self.assertRaises(frappe.ValidationError, pe.save)
@@ -228,9 +248,24 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_gl_entries_with_perpetual_inventory_against_pr(self):
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", get_taxes_and_charges=True,)
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ supplier_warehouse="Work In Progress - TCP1",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ get_taxes_and_charges=True,
+ )
- pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10,do_not_save= "True")
+ pi = make_purchase_invoice(
+ company="_Test Company with perpetual inventory",
+ supplier_warehouse="Work In Progress - TCP1",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ expense_account="_Test Account Cost for Goods Sold - TCP1",
+ get_taxes_and_charges=True,
+ qty=10,
+ do_not_save="True",
+ )
for d in pi.items:
d.purchase_receipt = pr.name
@@ -243,18 +278,25 @@ class TestPurchaseInvoice(unittest.TestCase):
self.check_gle_for_pi(pi.name)
def check_gle_for_pi(self, pi):
- gl_entries = frappe.db.sql("""select account, sum(debit) as debit, sum(credit) as credit
+ gl_entries = frappe.db.sql(
+ """select account, sum(debit) as debit, sum(credit) as credit
from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s
- group by account""", pi, as_dict=1)
+ group by account""",
+ pi,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
- expected_values = dict((d[0], d) for d in [
- ["Creditors - TCP1", 0, 720],
- ["Stock Received But Not Billed - TCP1", 500.0, 0],
- ["_Test Account Shipping Charges - TCP1", 100.0, 0.0],
- ["_Test Account VAT - TCP1", 120.0, 0]
- ])
+ expected_values = dict(
+ (d[0], d)
+ for d in [
+ ["Creditors - TCP1", 0, 720],
+ ["Stock Received But Not Billed - TCP1", 500.0, 0],
+ ["_Test Account Shipping Charges - TCP1", 100.0, 0.0],
+ ["_Test Account VAT - TCP1", 120.0, 0],
+ ]
+ )
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][0], gle.account)
@@ -264,14 +306,17 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_purchase_invoice_with_discount_accounting_enabled(self):
enable_discount_accounting()
- discount_account = create_account(account_name="Discount Account",
- parent_account="Indirect Expenses - _TC", company="_Test Company")
+ discount_account = create_account(
+ account_name="Discount Account",
+ parent_account="Indirect Expenses - _TC",
+ company="_Test Company",
+ )
pi = make_purchase_invoice(discount_account=discount_account, rate=45)
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()],
["Creditors - _TC", 0.0, 225.0, nowdate()],
- ["Discount Account - _TC", 0.0, 25.0, nowdate()]
+ ["Discount Account - _TC", 0.0, 25.0, nowdate()],
]
check_gl_entries(self, pi.name, expected_gle, nowdate())
@@ -279,28 +324,34 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self):
enable_discount_accounting()
- additional_discount_account = create_account(account_name="Discount Account",
- parent_account="Indirect Expenses - _TC", company="_Test Company")
+ additional_discount_account = create_account(
+ account_name="Discount Account",
+ parent_account="Indirect Expenses - _TC",
+ company="_Test Company",
+ )
pi = make_purchase_invoice(do_not_save=1, parent_cost_center="Main - _TC")
pi.apply_discount_on = "Grand Total"
pi.additional_discount_account = additional_discount_account
pi.additional_discount_percentage = 10
pi.disable_rounded_total = 1
- pi.append("taxes", {
- "charge_type": "On Net Total",
- "account_head": "_Test Account VAT - _TC",
- "cost_center": "Main - _TC",
- "description": "Test",
- "rate": 10
- })
+ pi.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account VAT - _TC",
+ "cost_center": "Main - _TC",
+ "description": "Test",
+ "rate": 10,
+ },
+ )
pi.submit()
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()],
["_Test Account VAT - _TC", 25.0, 0.0, nowdate()],
["Creditors - _TC", 0.0, 247.5, nowdate()],
- ["Discount Account - _TC", 0.0, 27.5, nowdate()]
+ ["Discount Account - _TC", 0.0, 27.5, nowdate()],
]
check_gl_entries(self, pi.name, expected_gle, nowdate())
@@ -308,7 +359,7 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_purchase_invoice_change_naming_series(self):
pi = frappe.copy_doc(test_records[1])
pi.insert()
- pi.naming_series = 'TEST-'
+ pi.naming_series = "TEST-"
self.assertRaises(frappe.CannotChangeConstantError, pi.save)
@@ -317,25 +368,33 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.load_from_db()
self.assertTrue(pi.status, "Draft")
- pi.naming_series = 'TEST-'
+ pi.naming_series = "TEST-"
self.assertRaises(frappe.CannotChangeConstantError, pi.save)
def test_gl_entries_for_non_stock_items_with_perpetual_inventory(self):
- pi = make_purchase_invoice(item_code = "_Test Non Stock Item",
- company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
- cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
+ pi = make_purchase_invoice(
+ item_code="_Test Non Stock Item",
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ expense_account="_Test Account Cost for Goods Sold - TCP1",
+ )
self.assertTrue(pi.status, "Unpaid")
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit
from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s
- order by account asc""", pi.name, as_dict=1)
+ order by account asc""",
+ pi.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
expected_values = [
["_Test Account Cost for Goods Sold - TCP1", 250.0, 0],
- ["Creditors - TCP1", 0, 250]
+ ["Creditors - TCP1", 0, 250],
]
for i, gle in enumerate(gl_entries):
@@ -350,7 +409,7 @@ class TestPurchaseInvoice(unittest.TestCase):
expected_values = [
["_Test Item Home Desktop 100", 90, 59],
- ["_Test Item Home Desktop 200", 135, 177]
+ ["_Test Item Home Desktop 200", 135, 177],
]
for i, item in enumerate(pi.get("items")):
self.assertEqual(item.item_code, expected_values[i][0])
@@ -382,10 +441,7 @@ class TestPurchaseInvoice(unittest.TestCase):
wrapper.insert()
wrapper.load_from_db()
- expected_values = [
- ["_Test FG Item", 90, 59],
- ["_Test Item Home Desktop 200", 135, 177]
- ]
+ expected_values = [["_Test FG Item", 90, 59], ["_Test Item Home Desktop 200", 135, 177]]
for i, item in enumerate(wrapper.get("items")):
self.assertEqual(item.item_code, expected_values[i][0])
self.assertEqual(item.item_tax_amount, expected_values[i][1])
@@ -422,14 +478,17 @@ class TestPurchaseInvoice(unittest.TestCase):
pi = frappe.copy_doc(test_records[0])
pi.disable_rounded_total = 1
pi.allocate_advances_automatically = 0
- pi.append("advances", {
- "reference_type": "Journal Entry",
- "reference_name": jv.name,
- "reference_row": jv.get("accounts")[0].name,
- "advance_amount": 400,
- "allocated_amount": 300,
- "remarks": jv.remark
- })
+ pi.append(
+ "advances",
+ {
+ "reference_type": "Journal Entry",
+ "reference_name": jv.name,
+ "reference_row": jv.get("accounts")[0].name,
+ "advance_amount": 400,
+ "allocated_amount": 300,
+ "remarks": jv.remark,
+ },
+ )
pi.insert()
self.assertEqual(pi.outstanding_amount, 1212.30)
@@ -442,14 +501,24 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.submit()
pi.load_from_db()
- self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account`
+ self.assertTrue(
+ frappe.db.sql(
+ """select name from `tabJournal Entry Account`
where reference_type='Purchase Invoice'
- and reference_name=%s and debit_in_account_currency=300""", pi.name))
+ and reference_name=%s and debit_in_account_currency=300""",
+ pi.name,
+ )
+ )
pi.cancel()
- self.assertFalse(frappe.db.sql("""select name from `tabJournal Entry Account`
- where reference_type='Purchase Invoice' and reference_name=%s""", pi.name))
+ self.assertFalse(
+ frappe.db.sql(
+ """select name from `tabJournal Entry Account`
+ where reference_type='Purchase Invoice' and reference_name=%s""",
+ pi.name,
+ )
+ )
def test_invoice_with_advance_and_multi_payment_terms(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
@@ -463,20 +532,26 @@ class TestPurchaseInvoice(unittest.TestCase):
pi = frappe.copy_doc(test_records[0])
pi.disable_rounded_total = 1
pi.allocate_advances_automatically = 0
- pi.append("advances", {
- "reference_type": "Journal Entry",
- "reference_name": jv.name,
- "reference_row": jv.get("accounts")[0].name,
- "advance_amount": 400,
- "allocated_amount": 300,
- "remarks": jv.remark
- })
+ pi.append(
+ "advances",
+ {
+ "reference_type": "Journal Entry",
+ "reference_name": jv.name,
+ "reference_row": jv.get("accounts")[0].name,
+ "advance_amount": 400,
+ "allocated_amount": 300,
+ "remarks": jv.remark,
+ },
+ )
pi.insert()
- pi.update({
- "payment_schedule": get_payment_terms("_Test Payment Term Template",
- pi.posting_date, pi.grand_total, pi.base_grand_total)
- })
+ pi.update(
+ {
+ "payment_schedule": get_payment_terms(
+ "_Test Payment Term Template", pi.posting_date, pi.grand_total, pi.base_grand_total
+ )
+ }
+ )
pi.save()
pi.submit()
@@ -490,7 +565,9 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertTrue(
frappe.db.sql(
"select name from `tabJournal Entry Account` where reference_type='Purchase Invoice' and "
- "reference_name=%s and debit_in_account_currency=300", pi.name)
+ "reference_name=%s and debit_in_account_currency=300",
+ pi.name,
+ )
)
self.assertEqual(pi.outstanding_amount, 1212.30)
@@ -500,49 +577,76 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertFalse(
frappe.db.sql(
"select name from `tabJournal Entry Account` where reference_type='Purchase Invoice' and "
- "reference_name=%s", pi.name)
+ "reference_name=%s",
+ pi.name,
+ )
)
def test_total_purchase_cost_for_project(self):
if not frappe.db.exists("Project", {"project_name": "_Test Project for Purchase"}):
- project = make_project({'project_name':'_Test Project for Purchase'})
+ project = make_project({"project_name": "_Test Project for Purchase"})
else:
project = frappe.get_doc("Project", {"project_name": "_Test Project for Purchase"})
- existing_purchase_cost = frappe.db.sql("""select sum(base_net_amount)
+ existing_purchase_cost = frappe.db.sql(
+ """select sum(base_net_amount)
from `tabPurchase Invoice Item`
where project = '{0}'
- and docstatus=1""".format(project.name))
+ and docstatus=1""".format(
+ project.name
+ )
+ )
existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0
pi = make_purchase_invoice(currency="USD", conversion_rate=60, project=project.name)
- self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
- existing_purchase_cost + 15000)
+ self.assertEqual(
+ frappe.db.get_value("Project", project.name, "total_purchase_cost"),
+ existing_purchase_cost + 15000,
+ )
pi1 = make_purchase_invoice(qty=10, project=project.name)
- self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
- existing_purchase_cost + 15500)
+ self.assertEqual(
+ frappe.db.get_value("Project", project.name, "total_purchase_cost"),
+ existing_purchase_cost + 15500,
+ )
pi1.cancel()
- self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
- existing_purchase_cost + 15000)
+ self.assertEqual(
+ frappe.db.get_value("Project", project.name, "total_purchase_cost"),
+ existing_purchase_cost + 15000,
+ )
pi.cancel()
- self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost)
+ self.assertEqual(
+ frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost
+ )
def test_return_purchase_invoice_with_perpetual_inventory(self):
- pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
- cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
-
- return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2,
- company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
- cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
+ pi = make_purchase_invoice(
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ expense_account="_Test Account Cost for Goods Sold - TCP1",
+ )
+ return_pi = make_purchase_invoice(
+ is_return=1,
+ return_against=pi.name,
+ qty=-2,
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ expense_account="_Test Account Cost for Goods Sold - TCP1",
+ )
# check gl entries for return
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit
from `tabGL Entry` where voucher_type=%s and voucher_no=%s
- order by account desc""", ("Purchase Invoice", return_pi.name), as_dict=1)
+ order by account desc""",
+ ("Purchase Invoice", return_pi.name),
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -556,13 +660,21 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][1], gle.credit)
def test_multi_currency_gle(self):
- pi = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC",
- currency="USD", conversion_rate=50)
+ pi = make_purchase_invoice(
+ supplier="_Test Supplier USD",
+ credit_to="_Test Payable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
- gl_entries = frappe.db.sql("""select account, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s
- order by account asc""", pi.name, as_dict=1)
+ order by account asc""",
+ pi.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -572,53 +684,74 @@ class TestPurchaseInvoice(unittest.TestCase):
"debit": 0,
"debit_in_account_currency": 0,
"credit": 12500,
- "credit_in_account_currency": 250
+ "credit_in_account_currency": 250,
},
"_Test Account Cost for Goods Sold - _TC": {
"account_currency": "INR",
"debit": 12500,
"debit_in_account_currency": 12500,
"credit": 0,
- "credit_in_account_currency": 0
- }
+ "credit_in_account_currency": 0,
+ },
}
- for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"):
+ for field in (
+ "account_currency",
+ "debit",
+ "debit_in_account_currency",
+ "credit",
+ "credit_in_account_currency",
+ ):
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][field], gle[field])
-
# Check for valid currency
- pi1 = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC",
- do_not_save=True)
+ pi1 = make_purchase_invoice(
+ supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC", do_not_save=True
+ )
self.assertRaises(InvalidCurrency, pi1.save)
# cancel
pi.cancel()
- gle = frappe.db.sql("""select name from `tabGL Entry`
- where voucher_type='Sales Invoice' and voucher_no=%s""", pi.name)
+ gle = frappe.db.sql(
+ """select name from `tabGL Entry`
+ where voucher_type='Sales Invoice' and voucher_no=%s""",
+ pi.name,
+ )
self.assertFalse(gle)
def test_purchase_invoice_update_stock_gl_entry_with_perpetual_inventory(self):
- pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(),
- posting_time=frappe.utils.nowtime(), cash_bank_account="Cash - TCP1", company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
+ pi = make_purchase_invoice(
+ update_stock=1,
+ posting_date=frappe.utils.nowdate(),
+ posting_time=frappe.utils.nowtime(),
+ cash_bank_account="Cash - TCP1",
+ company="_Test Company with perpetual inventory",
+ supplier_warehouse="Work In Progress - TCP1",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ expense_account="_Test Account Cost for Goods Sold - TCP1",
+ )
- gl_entries = frappe.db.sql("""select account, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s
- order by account asc""", pi.name, as_dict=1)
+ order by account asc""",
+ pi.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
stock_in_hand_account = get_inventory_account(pi.company, pi.get("items")[0].warehouse)
- expected_gl_entries = dict((d[0], d) for d in [
- [pi.credit_to, 0.0, 250.0],
- [stock_in_hand_account, 250.0, 0.0]
- ])
+ expected_gl_entries = dict(
+ (d[0], d) for d in [[pi.credit_to, 0.0, 250.0], [stock_in_hand_account, 250.0, 0.0]]
+ )
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gl_entries[gle.account][0], gle.account)
@@ -627,22 +760,39 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_purchase_invoice_for_is_paid_and_update_stock_gl_entry_with_perpetual_inventory(self):
- pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(),
- posting_time=frappe.utils.nowtime(), cash_bank_account="Cash - TCP1", is_paid=1, company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
+ pi = make_purchase_invoice(
+ update_stock=1,
+ posting_date=frappe.utils.nowdate(),
+ posting_time=frappe.utils.nowtime(),
+ cash_bank_account="Cash - TCP1",
+ is_paid=1,
+ company="_Test Company with perpetual inventory",
+ supplier_warehouse="Work In Progress - TCP1",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ expense_account="_Test Account Cost for Goods Sold - TCP1",
+ )
- gl_entries = frappe.db.sql("""select account, account_currency, sum(debit) as debit,
+ gl_entries = frappe.db.sql(
+ """select account, account_currency, sum(debit) as debit,
sum(credit) as credit, debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s
- group by account, voucher_no order by account asc;""", pi.name, as_dict=1)
+ group by account, voucher_no order by account asc;""",
+ pi.name,
+ as_dict=1,
+ )
stock_in_hand_account = get_inventory_account(pi.company, pi.get("items")[0].warehouse)
self.assertTrue(gl_entries)
- expected_gl_entries = dict((d[0], d) for d in [
- [pi.credit_to, 250.0, 250.0],
- [stock_in_hand_account, 250.0, 0.0],
- ["Cash - TCP1", 0.0, 250.0]
- ])
+ expected_gl_entries = dict(
+ (d[0], d)
+ for d in [
+ [pi.credit_to, 250.0, 250.0],
+ [stock_in_hand_account, 250.0, 0.0],
+ ["Cash - TCP1", 0.0, 250.0],
+ ]
+ )
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gl_entries[gle.account][0], gle.account)
@@ -650,31 +800,36 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(expected_gl_entries[gle.account][2], gle.credit)
def test_auto_batch(self):
- item_code = frappe.db.get_value('Item',
- {'has_batch_no': 1, 'create_new_batch':1}, 'name')
+ item_code = frappe.db.get_value("Item", {"has_batch_no": 1, "create_new_batch": 1}, "name")
if not item_code:
- doc = frappe.get_doc({
- 'doctype': 'Item',
- 'is_stock_item': 1,
- 'item_code': 'test batch item',
- 'item_group': 'Products',
- 'has_batch_no': 1,
- 'create_new_batch': 1
- }).insert(ignore_permissions=True)
+ doc = frappe.get_doc(
+ {
+ "doctype": "Item",
+ "is_stock_item": 1,
+ "item_code": "test batch item",
+ "item_group": "Products",
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ }
+ ).insert(ignore_permissions=True)
item_code = doc.name
- pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(),
- posting_time=frappe.utils.nowtime(), item_code=item_code)
+ pi = make_purchase_invoice(
+ update_stock=1,
+ posting_date=frappe.utils.nowdate(),
+ posting_time=frappe.utils.nowtime(),
+ item_code=item_code,
+ )
- self.assertTrue(frappe.db.get_value('Batch',
- {'item': item_code, 'reference_name': pi.name}))
+ self.assertTrue(frappe.db.get_value("Batch", {"item": item_code, "reference_name": pi.name}))
def test_update_stock_and_purchase_return(self):
actual_qty_0 = get_qty_after_transaction()
- pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(),
- posting_time=frappe.utils.nowtime())
+ pi = make_purchase_invoice(
+ update_stock=1, posting_date=frappe.utils.nowdate(), posting_time=frappe.utils.nowtime()
+ )
actual_qty_1 = get_qty_after_transaction()
self.assertEqual(actual_qty_0 + 5, actual_qty_1)
@@ -701,13 +856,20 @@ class TestPurchaseInvoice(unittest.TestCase):
from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
- update_backflush_based_on('BOM')
- make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100)
- make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC",
- qty=100, basic_rate=100)
+ update_backflush_based_on("BOM")
+ make_stock_entry(
+ item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100
+ )
+ make_stock_entry(
+ item_code="_Test Item Home Desktop 100",
+ target="_Test Warehouse 1 - _TC",
+ qty=100,
+ basic_rate=100,
+ )
- pi = make_purchase_invoice(item_code="_Test FG Item", qty=10, rate=500,
- update_stock=1, is_subcontracted="Yes")
+ pi = make_purchase_invoice(
+ item_code="_Test FG Item", qty=10, rate=500, update_stock=1, is_subcontracted="Yes"
+ )
self.assertEqual(len(pi.get("supplied_items")), 2)
@@ -715,15 +877,26 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
def test_rejected_serial_no(self):
- pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1,
- rejected_qty=1, rate=500, update_stock=1, rejected_warehouse = "_Test Rejected Warehouse - _TC",
- allow_zero_valuation_rate=1)
+ pi = make_purchase_invoice(
+ item_code="_Test Serialized Item With Series",
+ received_qty=2,
+ qty=1,
+ rejected_qty=1,
+ rate=500,
+ update_stock=1,
+ rejected_warehouse="_Test Rejected Warehouse - _TC",
+ allow_zero_valuation_rate=1,
+ )
- self.assertEqual(frappe.db.get_value("Serial No", pi.get("items")[0].serial_no, "warehouse"),
- pi.get("items")[0].warehouse)
+ self.assertEqual(
+ frappe.db.get_value("Serial No", pi.get("items")[0].serial_no, "warehouse"),
+ pi.get("items")[0].warehouse,
+ )
- self.assertEqual(frappe.db.get_value("Serial No", pi.get("items")[0].rejected_serial_no,
- "warehouse"), pi.get("items")[0].rejected_warehouse)
+ self.assertEqual(
+ frappe.db.get_value("Serial No", pi.get("items")[0].rejected_serial_no, "warehouse"),
+ pi.get("items")[0].rejected_warehouse,
+ )
def test_outstanding_amount_after_advance_jv_cancelation(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
@@ -731,85 +904,95 @@ class TestPurchaseInvoice(unittest.TestCase):
)
jv = frappe.copy_doc(jv_test_records[1])
- jv.accounts[0].is_advance = 'Yes'
+ jv.accounts[0].is_advance = "Yes"
jv.insert()
jv.submit()
pi = frappe.copy_doc(test_records[0])
- pi.append("advances", {
- "reference_type": "Journal Entry",
- "reference_name": jv.name,
- "reference_row": jv.get("accounts")[0].name,
- "advance_amount": 400,
- "allocated_amount": 300,
- "remarks": jv.remark
- })
+ pi.append(
+ "advances",
+ {
+ "reference_type": "Journal Entry",
+ "reference_name": jv.name,
+ "reference_row": jv.get("accounts")[0].name,
+ "advance_amount": 400,
+ "allocated_amount": 300,
+ "remarks": jv.remark,
+ },
+ )
pi.insert()
pi.submit()
pi.load_from_db()
- #check outstanding after advance allocation
+ # check outstanding after advance allocation
self.assertEqual(flt(pi.outstanding_amount), flt(pi.rounded_total - pi.total_advance))
- #added to avoid Document has been modified exception
+ # added to avoid Document has been modified exception
jv = frappe.get_doc("Journal Entry", jv.name)
jv.cancel()
pi.load_from_db()
- #check outstanding after advance cancellation
+ # check outstanding after advance cancellation
self.assertEqual(flt(pi.outstanding_amount), flt(pi.rounded_total + pi.total_advance))
def test_outstanding_amount_after_advance_payment_entry_cancelation(self):
- pe = frappe.get_doc({
- "doctype": "Payment Entry",
- "payment_type": "Pay",
- "party_type": "Supplier",
- "party": "_Test Supplier",
- "company": "_Test Company",
- "paid_from_account_currency": "INR",
- "paid_to_account_currency": "INR",
- "source_exchange_rate": 1,
- "target_exchange_rate": 1,
- "reference_no": "1",
- "reference_date": nowdate(),
- "received_amount": 300,
- "paid_amount": 300,
- "paid_from": "_Test Cash - _TC",
- "paid_to": "_Test Payable - _TC"
- })
+ pe = frappe.get_doc(
+ {
+ "doctype": "Payment Entry",
+ "payment_type": "Pay",
+ "party_type": "Supplier",
+ "party": "_Test Supplier",
+ "company": "_Test Company",
+ "paid_from_account_currency": "INR",
+ "paid_to_account_currency": "INR",
+ "source_exchange_rate": 1,
+ "target_exchange_rate": 1,
+ "reference_no": "1",
+ "reference_date": nowdate(),
+ "received_amount": 300,
+ "paid_amount": 300,
+ "paid_from": "_Test Cash - _TC",
+ "paid_to": "_Test Payable - _TC",
+ }
+ )
pe.insert()
pe.submit()
pi = frappe.copy_doc(test_records[0])
pi.is_pos = 0
- pi.append("advances", {
- "doctype": "Purchase Invoice Advance",
- "reference_type": "Payment Entry",
- "reference_name": pe.name,
- "advance_amount": 300,
- "allocated_amount": 300,
- "remarks": pe.remarks
- })
+ pi.append(
+ "advances",
+ {
+ "doctype": "Purchase Invoice Advance",
+ "reference_type": "Payment Entry",
+ "reference_name": pe.name,
+ "advance_amount": 300,
+ "allocated_amount": 300,
+ "remarks": pe.remarks,
+ },
+ )
pi.insert()
pi.submit()
pi.load_from_db()
- #check outstanding after advance allocation
+ # check outstanding after advance allocation
self.assertEqual(flt(pi.outstanding_amount), flt(pi.rounded_total - pi.total_advance))
- #added to avoid Document has been modified exception
+ # added to avoid Document has been modified exception
pe = frappe.get_doc("Payment Entry", pe.name)
pe.cancel()
pi.load_from_db()
- #check outstanding after advance cancellation
+ # check outstanding after advance cancellation
self.assertEqual(flt(pi.outstanding_amount), flt(pi.rounded_total + pi.total_advance))
def test_purchase_invoice_with_shipping_rule(self):
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
- shipping_rule = create_shipping_rule(shipping_rule_type = "Buying", shipping_rule_name = "Shipping Rule - Purchase Invoice Test")
+ shipping_rule = create_shipping_rule(
+ shipping_rule_type="Buying", shipping_rule_name="Shipping Rule - Purchase Invoice Test"
+ )
pi = frappe.copy_doc(test_records[0])
@@ -825,16 +1008,20 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_make_pi_without_terms(self):
pi = make_purchase_invoice(do_not_save=1)
- self.assertFalse(pi.get('payment_schedule'))
+ self.assertFalse(pi.get("payment_schedule"))
pi.insert()
- self.assertTrue(pi.get('payment_schedule'))
+ self.assertTrue(pi.get("payment_schedule"))
def test_duplicate_due_date_in_terms(self):
pi = make_purchase_invoice(do_not_save=1)
- pi.append('payment_schedule', dict(due_date='2017-01-01', invoice_portion=50.00, payment_amount=50))
- pi.append('payment_schedule', dict(due_date='2017-01-01', invoice_portion=50.00, payment_amount=50))
+ pi.append(
+ "payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50)
+ )
+ pi.append(
+ "payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50)
+ )
self.assertRaises(frappe.ValidationError, pi.insert)
@@ -842,12 +1029,13 @@ class TestPurchaseInvoice(unittest.TestCase):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import get_outstanding_amount
- pi = make_purchase_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1)
+ pi = make_purchase_invoice(item_code="_Test Item", qty=(5 * -1), rate=500, is_return=1)
pi.load_from_db()
self.assertTrue(pi.status, "Return")
- outstanding_amount = get_outstanding_amount(pi.doctype,
- pi.name, "Creditors - _TC", pi.supplier, "Supplier")
+ outstanding_amount = get_outstanding_amount(
+ pi.doctype, pi.name, "Creditors - _TC", pi.supplier, "Supplier"
+ )
self.assertEqual(pi.outstanding_amount, outstanding_amount)
@@ -862,30 +1050,33 @@ class TestPurchaseInvoice(unittest.TestCase):
pe.insert()
pe.submit()
- pi_doc = frappe.get_doc('Purchase Invoice', pi.name)
+ pi_doc = frappe.get_doc("Purchase Invoice", pi.name)
self.assertEqual(pi_doc.outstanding_amount, 0)
def test_purchase_invoice_with_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+
cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
- pi = make_purchase_invoice_against_cost_center(cost_center=cost_center, credit_to="Creditors - _TC")
+ pi = make_purchase_invoice_against_cost_center(
+ cost_center=cost_center, credit_to="Creditors - _TC"
+ )
self.assertEqual(pi.cost_center, cost_center)
expected_values = {
- "Creditors - _TC": {
- "cost_center": cost_center
- },
- "_Test Account Cost for Goods Sold - _TC": {
- "cost_center": cost_center
- }
+ "Creditors - _TC": {"cost_center": cost_center},
+ "_Test Account Cost for Goods Sold - _TC": {"cost_center": cost_center},
}
- gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, cost_center, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s
- order by account asc""", pi.name, as_dict=1)
+ order by account asc""",
+ pi.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -894,21 +1085,21 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_purchase_invoice_without_cost_center(self):
cost_center = "_Test Cost Center - _TC"
- pi = make_purchase_invoice(credit_to="Creditors - _TC")
+ pi = make_purchase_invoice(credit_to="Creditors - _TC")
expected_values = {
- "Creditors - _TC": {
- "cost_center": None
- },
- "_Test Account Cost for Goods Sold - _TC": {
- "cost_center": cost_center
- }
+ "Creditors - _TC": {"cost_center": None},
+ "_Test Account Cost for Goods Sold - _TC": {"cost_center": cost_center},
}
- gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, cost_center, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s
- order by account asc""", pi.name, as_dict=1)
+ order by account asc""",
+ pi.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -916,36 +1107,40 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
def test_purchase_invoice_with_project_link(self):
- project = make_project({
- 'project_name': 'Purchase Invoice Project',
- 'project_template_name': 'Test Project Template',
- 'start_date': '2020-01-01'
- })
- item_project = make_project({
- 'project_name': 'Purchase Invoice Item Project',
- 'project_template_name': 'Test Project Template',
- 'start_date': '2019-06-01'
- })
+ project = make_project(
+ {
+ "project_name": "Purchase Invoice Project",
+ "project_template_name": "Test Project Template",
+ "start_date": "2020-01-01",
+ }
+ )
+ item_project = make_project(
+ {
+ "project_name": "Purchase Invoice Item Project",
+ "project_template_name": "Test Project Template",
+ "start_date": "2019-06-01",
+ }
+ )
- pi = make_purchase_invoice(credit_to="Creditors - _TC" ,do_not_save=1)
+ pi = make_purchase_invoice(credit_to="Creditors - _TC", do_not_save=1)
pi.items[0].project = item_project.name
pi.project = project.name
pi.submit()
expected_values = {
- "Creditors - _TC": {
- "project": project.name
- },
- "_Test Account Cost for Goods Sold - _TC": {
- "project": item_project.name
- }
+ "Creditors - _TC": {"project": project.name},
+ "_Test Account Cost for Goods Sold - _TC": {"project": item_project.name},
}
- gl_entries = frappe.db.sql("""select account, cost_center, project, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, cost_center, project, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s
- order by account asc""", pi.name, as_dict=1)
+ order by account asc""",
+ pi.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -953,10 +1148,11 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account]["project"], gle.project)
def test_deferred_expense_via_journal_entry(self):
- deferred_account = create_account(account_name="Deferred Expense",
- parent_account="Current Assets - _TC", company="_Test Company")
+ deferred_account = create_account(
+ account_name="Deferred Expense", parent_account="Current Assets - _TC", company="_Test Company"
+ )
- acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings')
+ acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
acc_settings.book_deferred_entries_via_journal_entry = 1
acc_settings.submit_journal_entries = 1
acc_settings.save()
@@ -968,7 +1164,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True)
pi.set_posting_time = 1
- pi.posting_date = '2019-01-10'
+ pi.posting_date = "2019-01-10"
pi.items[0].enable_deferred_expense = 1
pi.items[0].service_start_date = "2019-01-10"
pi.items[0].service_end_date = "2019-03-15"
@@ -976,14 +1172,16 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.save()
pi.submit()
- pda1 = frappe.get_doc(dict(
- doctype='Process Deferred Accounting',
- posting_date=nowdate(),
- start_date="2019-01-01",
- end_date="2019-03-31",
- type="Expense",
- company="_Test Company"
- ))
+ pda1 = frappe.get_doc(
+ dict(
+ doctype="Process Deferred Accounting",
+ posting_date=nowdate(),
+ start_date="2019-01-01",
+ end_date="2019-03-31",
+ type="Expense",
+ company="_Test Company",
+ )
+ )
pda1.insert()
pda1.submit()
@@ -994,13 +1192,17 @@ class TestPurchaseInvoice(unittest.TestCase):
["_Test Account Cost for Goods Sold - _TC", 0.0, 43.08, "2019-02-28"],
[deferred_account, 43.08, 0.0, "2019-02-28"],
["_Test Account Cost for Goods Sold - _TC", 0.0, 23.07, "2019-03-15"],
- [deferred_account, 23.07, 0.0, "2019-03-15"]
+ [deferred_account, 23.07, 0.0, "2019-03-15"],
]
- gl_entries = gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
+ gl_entries = gl_entries = frappe.db.sql(
+ """select account, debit, credit, posting_date
from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s
- order by posting_date asc, account asc""", (pi.items[0].name, pi.posting_date), as_dict=1)
+ order by posting_date asc, account asc""",
+ (pi.items[0].name, pi.posting_date),
+ as_dict=1,
+ )
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
@@ -1008,108 +1210,139 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(expected_gle[i][2], gle.debit)
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
- acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings')
+ acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
acc_settings.book_deferred_entries_via_journal_entry = 0
acc_settings.submit_journal_entriessubmit_journal_entries = 0
acc_settings.save()
def test_gain_loss_with_advance_entry(self):
unlink_enabled = frappe.db.get_value(
- "Accounts Settings", "Accounts Settings",
- "unlink_payment_on_cancel_of_invoice")
+ "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
+ )
frappe.db.set_value(
- "Accounts Settings", "Accounts Settings",
- "unlink_payment_on_cancel_of_invoice", 1)
+ "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1
+ )
original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account")
- frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", "Exchange Gain/Loss - _TC")
+ frappe.db.set_value(
+ "Company", "_Test Company", "exchange_gain_loss_account", "Exchange Gain/Loss - _TC"
+ )
- pay = frappe.get_doc({
- 'doctype': 'Payment Entry',
- 'company': '_Test Company',
- 'payment_type': 'Pay',
- 'party_type': 'Supplier',
- 'party': '_Test Supplier USD',
- 'paid_to': '_Test Payable USD - _TC',
- 'paid_from': 'Cash - _TC',
- 'paid_amount': 70000,
- 'target_exchange_rate': 70,
- 'received_amount': 1000,
- })
+ pay = frappe.get_doc(
+ {
+ "doctype": "Payment Entry",
+ "company": "_Test Company",
+ "payment_type": "Pay",
+ "party_type": "Supplier",
+ "party": "_Test Supplier USD",
+ "paid_to": "_Test Payable USD - _TC",
+ "paid_from": "Cash - _TC",
+ "paid_amount": 70000,
+ "target_exchange_rate": 70,
+ "received_amount": 1000,
+ }
+ )
pay.insert()
pay.submit()
- pi = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD",
- conversion_rate=75, rate=500, do_not_save=1, qty=1)
+ pi = make_purchase_invoice(
+ supplier="_Test Supplier USD",
+ currency="USD",
+ conversion_rate=75,
+ rate=500,
+ do_not_save=1,
+ qty=1,
+ )
pi.cost_center = "_Test Cost Center - _TC"
pi.advances = []
- pi.append("advances", {
- "reference_type": "Payment Entry",
- "reference_name": pay.name,
- "advance_amount": 1000,
- "remarks": pay.remarks,
- "allocated_amount": 500,
- "ref_exchange_rate": 70
- })
+ pi.append(
+ "advances",
+ {
+ "reference_type": "Payment Entry",
+ "reference_name": pay.name,
+ "advance_amount": 1000,
+ "remarks": pay.remarks,
+ "allocated_amount": 500,
+ "ref_exchange_rate": 70,
+ },
+ )
pi.save()
pi.submit()
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 37500.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(
+ """
select account, sum(debit - credit) as balance from `tabGL Entry`
where voucher_no=%s
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):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.balance)
- pi_2 = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD",
- conversion_rate=73, rate=500, do_not_save=1, qty=1)
+ pi_2 = make_purchase_invoice(
+ supplier="_Test Supplier USD",
+ currency="USD",
+ conversion_rate=73,
+ rate=500,
+ do_not_save=1,
+ qty=1,
+ )
pi_2.cost_center = "_Test Cost Center - _TC"
pi_2.advances = []
- pi_2.append("advances", {
- "reference_type": "Payment Entry",
- "reference_name": pay.name,
- "advance_amount": 500,
- "remarks": pay.remarks,
- "allocated_amount": 500,
- "ref_exchange_rate": 70
- })
+ pi_2.append(
+ "advances",
+ {
+ "reference_type": "Payment Entry",
+ "reference_name": pay.name,
+ "advance_amount": 500,
+ "remarks": pay.remarks,
+ "allocated_amount": 500,
+ "ref_exchange_rate": 70,
+ },
+ )
pi_2.save()
pi_2.submit()
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 36500.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(
+ """
select account, sum(debit - credit) as balance from `tabGL Entry`
where voucher_no=%s
- group by account order by account asc""", (pi_2.name), as_dict=1)
+ group by account order by account asc""",
+ (pi_2.name),
+ as_dict=1,
+ )
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.balance)
- expected_gle = [
- ["_Test Payable USD - _TC", 70000.0],
- ["Cash - _TC", -70000.0]
- ]
+ expected_gle = [["_Test Payable USD - _TC", 70000.0], ["Cash - _TC", -70000.0]]
- gl_entries = frappe.db.sql("""
+ gl_entries = frappe.db.sql(
+ """
select account, sum(debit - credit) as balance from `tabGL Entry`
where voucher_no=%s and is_cancelled=0
- group by account order by account asc""", (pay.name), as_dict=1)
+ group by account order by account asc""",
+ (pay.name),
+ as_dict=1,
+ )
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
@@ -1124,44 +1357,57 @@ class TestPurchaseInvoice(unittest.TestCase):
pay.reload()
pay.cancel()
- frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled)
+ frappe.db.set_value(
+ "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
+ )
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
def test_purchase_invoice_advance_taxes(self):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
# create a new supplier to test
- supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier',
- tax_withholding_category = 'TDS - 194 - Dividends - Individual')
+ supplier = create_supplier(
+ supplier_name="_Test TDS Advance Supplier",
+ tax_withholding_category="TDS - 194 - Dividends - Individual",
+ )
# Update tax withholding category with current fiscal year and rate details
- update_tax_witholding_category('_Test Company', 'TDS Payable - _TC')
+ update_tax_witholding_category("_Test Company", "TDS Payable - _TC")
# Create Purchase Order with TDS applied
- po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item',
- posting_date='2021-09-15')
+ po = create_purchase_order(
+ do_not_save=1,
+ supplier=supplier.name,
+ rate=3000,
+ item="_Test Non Stock Item",
+ posting_date="2021-09-15",
+ )
po.save()
po.submit()
# Create Payment Entry Against the order
- payment_entry = get_payment_entry(dt='Purchase Order', dn=po.name)
- payment_entry.paid_from = 'Cash - _TC'
+ payment_entry = get_payment_entry(dt="Purchase Order", dn=po.name)
+ payment_entry.paid_from = "Cash - _TC"
payment_entry.apply_tax_withholding_amount = 1
- payment_entry.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
+ payment_entry.tax_withholding_category = "TDS - 194 - Dividends - Individual"
payment_entry.save()
payment_entry.submit()
# Check GLE for Payment Entry
expected_gle = [
- ['Cash - _TC', 0, 27000],
- ['Creditors - _TC', 30000, 0],
- ['TDS Payable - _TC', 0, 3000],
+ ["Cash - _TC", 0, 27000],
+ ["Creditors - _TC", 30000, 0],
+ ["TDS Payable - _TC", 0, 3000],
]
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit
from `tabGL Entry`
where voucher_type='Payment Entry' and voucher_no=%s
- order by account asc""", (payment_entry.name), as_dict=1)
+ order by account asc""",
+ (payment_entry.name),
+ as_dict=1,
+ )
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
@@ -1171,23 +1417,24 @@ class TestPurchaseInvoice(unittest.TestCase):
# Create Purchase Invoice against Purchase Order
purchase_invoice = get_mapped_purchase_invoice(po.name)
purchase_invoice.allocate_advances_automatically = 1
- purchase_invoice.items[0].item_code = '_Test Non Stock Item'
- purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC'
+ purchase_invoice.items[0].item_code = "_Test Non Stock Item"
+ purchase_invoice.items[0].expense_account = "_Test Account Cost for Goods Sold - _TC"
purchase_invoice.save()
purchase_invoice.submit()
# Check GLE for Purchase Invoice
# Zero net effect on final TDS Payable on invoice
- expected_gle = [
- ['_Test Account Cost for Goods Sold - _TC', 30000],
- ['Creditors - _TC', -30000]
- ]
+ expected_gle = [["_Test Account Cost for Goods Sold - _TC", 30000], ["Creditors - _TC", -30000]]
- gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount
+ gl_entries = frappe.db.sql(
+ """select account, sum(debit - credit) as amount
from `tabGL Entry`
where voucher_type='Purchase Invoice' and voucher_no=%s
group by account
- order by account asc""", (purchase_invoice.name), as_dict=1)
+ order by account asc""",
+ (purchase_invoice.name),
+ as_dict=1,
+ )
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
@@ -1202,28 +1449,36 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
def test_provisional_accounting_entry(self):
- item = create_item("_Test Non Stock Item", is_stock_item=0)
- provisional_account = create_account(account_name="Provision Account",
- parent_account="Current Liabilities - _TC", company="_Test Company")
+ create_item("_Test Non Stock Item", is_stock_item=0)
- company = frappe.get_doc('Company', '_Test Company')
+ provisional_account = create_account(
+ account_name="Provision Account",
+ parent_account="Current Liabilities - _TC",
+ company="_Test Company",
+ )
+
+ company = frappe.get_doc("Company", "_Test Company")
company.enable_provisional_accounting_for_non_stock_items = 1
company.default_provisional_account = provisional_account
company.save()
- pr = make_purchase_receipt(item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2))
+ pr = make_purchase_receipt(
+ item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2)
+ )
pi = create_purchase_invoice_from_receipt(pr.name)
pi.set_posting_time = 1
pi.posting_date = add_days(pr.posting_date, -1)
- pi.items[0].expense_account = 'Cost of Goods Sold - _TC'
+ pi.items[0].expense_account = "Cost of Goods Sold - _TC"
pi.save()
pi.submit()
+ self.assertEquals(pr.items[0].provisional_expense_account, "Provision Account - _TC")
+
# Check GLE for Purchase Invoice
expected_gle = [
- ['Cost of Goods Sold - _TC', 250, 0, add_days(pr.posting_date, -1)],
- ['Creditors - _TC', 0, 250, add_days(pr.posting_date, -1)]
+ ["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, -1)],
+ ["Creditors - _TC", 0, 250, add_days(pr.posting_date, -1)],
]
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
@@ -1232,7 +1487,7 @@ class TestPurchaseInvoice(unittest.TestCase):
["Provision Account - _TC", 250, 0, pr.posting_date],
["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date],
["Provision Account - _TC", 0, 250, pi.posting_date],
- ["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date]
+ ["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date],
]
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
@@ -1240,11 +1495,16 @@ class TestPurchaseInvoice(unittest.TestCase):
company.enable_provisional_accounting_for_non_stock_items = 0
company.save()
+
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
- gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit, posting_date
from `tabGL Entry`
where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s
- order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1)
+ order by posting_date asc, account asc""",
+ (voucher_no, posting_date),
+ as_dict=1,
+ )
for i, gle in enumerate(gl_entries):
doc.assertEqual(expected_gle[i][0], gle.account)
@@ -1252,45 +1512,55 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
doc.assertEqual(expected_gle[i][2], gle.credit)
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
+
def update_tax_witholding_category(company, account):
from erpnext.accounts.utils import get_fiscal_year
fiscal_year = get_fiscal_year(date=nowdate())
- if not frappe.db.get_value('Tax Withholding Rate',
- {'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]),
- 'to_date': ('<=', fiscal_year[2])}):
- tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual')
- tds_category.set('rates', [])
+ if not frappe.db.get_value(
+ "Tax Withholding Rate",
+ {
+ "parent": "TDS - 194 - Dividends - Individual",
+ "from_date": (">=", fiscal_year[1]),
+ "to_date": ("<=", fiscal_year[2]),
+ },
+ ):
+ tds_category = frappe.get_doc("Tax Withholding Category", "TDS - 194 - Dividends - Individual")
+ tds_category.set("rates", [])
- tds_category.append('rates', {
- 'from_date': fiscal_year[1],
- 'to_date': fiscal_year[2],
- 'tax_withholding_rate': 10,
- 'single_threshold': 2500,
- 'cumulative_threshold': 0
- })
+ tds_category.append(
+ "rates",
+ {
+ "from_date": fiscal_year[1],
+ "to_date": fiscal_year[2],
+ "tax_withholding_rate": 10,
+ "single_threshold": 2500,
+ "cumulative_threshold": 0,
+ },
+ )
tds_category.save()
- if not frappe.db.get_value('Tax Withholding Account',
- {'parent': 'TDS - 194 - Dividends - Individual', 'account': account}):
- tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual')
- tds_category.append('accounts', {
- 'company': company,
- 'account': account
- })
+ if not frappe.db.get_value(
+ "Tax Withholding Account", {"parent": "TDS - 194 - Dividends - Individual", "account": account}
+ ):
+ tds_category = frappe.get_doc("Tax Withholding Category", "TDS - 194 - Dividends - Individual")
+ tds_category.append("accounts", {"company": company, "account": account})
tds_category.save()
+
def unlink_payment_on_cancel_of_invoice(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
accounts_settings.unlink_payment_on_cancellation_of_invoice = enable
accounts_settings.save()
+
def enable_discount_accounting(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
accounts_settings.enable_discount_accounting = enable
accounts_settings.save()
+
def make_purchase_invoice(**args):
pi = frappe.new_doc("Purchase Invoice")
args = frappe._dict(args)
@@ -1303,7 +1573,7 @@ def make_purchase_invoice(**args):
pi.is_paid = 1
if args.cash_bank_account:
- pi.cash_bank_account=args.cash_bank_account
+ pi.cash_bank_account = args.cash_bank_account
pi.company = args.company or "_Test Company"
pi.supplier = args.supplier or "_Test Supplier"
@@ -1315,27 +1585,30 @@ def make_purchase_invoice(**args):
pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC"
pi.cost_center = args.parent_cost_center
- pi.append("items", {
- "item_code": args.item or args.item_code or "_Test Item",
- "warehouse": args.warehouse or "_Test Warehouse - _TC",
- "qty": args.qty or 5,
- "received_qty": args.received_qty or 0,
- "rejected_qty": args.rejected_qty or 0,
- "rate": args.rate or 50,
- "price_list_rate": args.price_list_rate or 50,
- "expense_account": args.expense_account or '_Test Account Cost for Goods Sold - _TC',
- "discount_account": args.discount_account or None,
- "discount_amount": args.discount_amount or 0,
- "conversion_factor": 1.0,
- "serial_no": args.serial_no,
- "stock_uom": args.uom or "_Test UOM",
- "cost_center": args.cost_center or "_Test Cost Center - _TC",
- "project": args.project,
- "rejected_warehouse": args.rejected_warehouse or "",
- "rejected_serial_no": args.rejected_serial_no or "",
- "asset_location": args.location or "",
- "allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0
- })
+ pi.append(
+ "items",
+ {
+ "item_code": args.item or args.item_code or "_Test Item",
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "qty": args.qty or 5,
+ "received_qty": args.received_qty or 0,
+ "rejected_qty": args.rejected_qty or 0,
+ "rate": args.rate or 50,
+ "price_list_rate": args.price_list_rate or 50,
+ "expense_account": args.expense_account or "_Test Account Cost for Goods Sold - _TC",
+ "discount_account": args.discount_account or None,
+ "discount_amount": args.discount_amount or 0,
+ "conversion_factor": 1.0,
+ "serial_no": args.serial_no,
+ "stock_uom": args.uom or "_Test UOM",
+ "cost_center": args.cost_center or "_Test Cost Center - _TC",
+ "project": args.project,
+ "rejected_warehouse": args.rejected_warehouse or "",
+ "rejected_serial_no": args.rejected_serial_no or "",
+ "asset_location": args.location or "",
+ "allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0,
+ },
+ )
if args.get_taxes_and_charges:
taxes = get_taxes()
@@ -1348,6 +1621,7 @@ def make_purchase_invoice(**args):
pi.submit()
return pi
+
def make_purchase_invoice_against_cost_center(**args):
pi = frappe.new_doc("Purchase Invoice")
args = frappe._dict(args)
@@ -1360,7 +1634,7 @@ def make_purchase_invoice_against_cost_center(**args):
pi.is_paid = 1
if args.cash_bank_account:
- pi.cash_bank_account=args.cash_bank_account
+ pi.cash_bank_account = args.cash_bank_account
pi.company = args.company or "_Test Company"
pi.cost_center = args.cost_center or "_Test Cost Center - _TC"
@@ -1374,25 +1648,29 @@ def make_purchase_invoice_against_cost_center(**args):
if args.supplier_warehouse:
pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
- pi.append("items", {
- "item_code": args.item or args.item_code or "_Test Item",
- "warehouse": args.warehouse or "_Test Warehouse - _TC",
- "qty": args.qty or 5,
- "received_qty": args.received_qty or 0,
- "rejected_qty": args.rejected_qty or 0,
- "rate": args.rate or 50,
- "conversion_factor": 1.0,
- "serial_no": args.serial_no,
- "stock_uom": "_Test UOM",
- "cost_center": args.cost_center or "_Test Cost Center - _TC",
- "project": args.project,
- "rejected_warehouse": args.rejected_warehouse or "",
- "rejected_serial_no": args.rejected_serial_no or ""
- })
+ pi.append(
+ "items",
+ {
+ "item_code": args.item or args.item_code or "_Test Item",
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "qty": args.qty or 5,
+ "received_qty": args.received_qty or 0,
+ "rejected_qty": args.rejected_qty or 0,
+ "rate": args.rate or 50,
+ "conversion_factor": 1.0,
+ "serial_no": args.serial_no,
+ "stock_uom": "_Test UOM",
+ "cost_center": args.cost_center or "_Test Cost Center - _TC",
+ "project": args.project,
+ "rejected_warehouse": args.rejected_warehouse or "",
+ "rejected_serial_no": args.rejected_serial_no or "",
+ },
+ )
if not args.do_not_save:
pi.insert()
if not args.do_not_submit:
pi.submit()
return pi
-test_records = frappe.get_test_records('Purchase Invoice')
+
+test_records = frappe.get_test_records("Purchase Invoice")
diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py
index f5eb404d0a4..70d29bfda25 100644
--- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py
+++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py
@@ -16,5 +16,5 @@ class PurchaseTaxesandChargesTemplate(Document):
def autoname(self):
if self.company and self.title:
- abbr = frappe.get_cached_value('Company', self.company, 'abbr')
- self.name = '{0} - {1}'.format(self.title, abbr)
+ abbr = frappe.get_cached_value("Company", self.company, "abbr")
+ self.name = "{0} - {1}".format(self.title, abbr)
diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template_dashboard.py b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template_dashboard.py
index 95a7a1c889b..1f0ea211f20 100644
--- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template_dashboard.py
+++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template_dashboard.py
@@ -1,21 +1,17 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'taxes_and_charges',
- 'non_standard_fieldnames': {
- 'Tax Rule': 'purchase_tax_template',
+ "fieldname": "taxes_and_charges",
+ "non_standard_fieldnames": {
+ "Tax Rule": "purchase_tax_template",
},
- 'transactions': [
+ "transactions": [
{
- 'label': _('Transactions'),
- 'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt']
+ "label": _("Transactions"),
+ "items": ["Purchase Invoice", "Purchase Order", "Purchase Receipt"],
},
- {
- 'label': _('References'),
- 'items': ['Supplier Quotation', 'Tax Rule']
- }
- ]
+ {"label": _("References"), "items": ["Supplier Quotation", "Tax Rule"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.py b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.py
index b5b4a67d759..1d02f055048 100644
--- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.py
+++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Purchase Taxes and Charges Template')
+
class TestPurchaseTaxesandChargesTemplate(unittest.TestCase):
pass
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 8a6d3cd5935..c6a110dcab6 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -34,7 +34,9 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
var me = this;
this._super();
- this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', 'POS Closing Entry'];
+ this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
+ 'POS Closing Entry', 'Journal Entry', 'Payment Entry'];
+
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format
this.frm.set_df_property("debit_to", "print_hide", 0);
@@ -281,6 +283,9 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
}
var me = this;
if(this.frm.updating_party_details) return;
+
+ if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return;
+
erpnext.utils.get_party_details(this.frm,
"erpnext.accounts.party.get_party_details", {
posting_date: this.frm.doc.posting_date,
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 5062c1c807a..80b95db8868 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -2,7 +2,7 @@
"actions": [],
"allow_import": 1,
"autoname": "naming_series:",
- "creation": "2013-05-24 19:29:05",
+ "creation": "2022-01-25 10:29:57.771398",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
@@ -651,7 +651,6 @@
"hide_seconds": 1,
"label": "Ignore Pricing Rule",
"no_copy": 1,
- "permlevel": 0,
"print_hide": 1
},
{
@@ -1974,9 +1973,10 @@
},
{
"default": "0",
+ "description": "Issue a debit note with 0 qty against an existing Sales Invoice",
"fieldname": "is_debit_note",
"fieldtype": "Check",
- "label": "Is Debit Note"
+ "label": "Is Rate Adjustment Entry (Debit Note)"
},
{
"default": "0",
@@ -2038,7 +2038,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2021-12-23 20:19:38.667508",
+ "modified": "2022-03-08 16:08:53.517903",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
@@ -2089,8 +2089,9 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"timeline_field": "customer",
"title_field": "title",
"track_changes": 1,
"track_seen": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 42da6b7708f..e39822e4036 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -38,36 +38,41 @@ from erpnext.assets.doctype.asset.depreciation import (
get_gl_entries_on_asset_regain,
make_depreciation_entry,
)
+from erpnext.controllers.accounts_controller import validate_account_head
from erpnext.controllers.selling_controller import SellingController
from erpnext.healthcare.utils import manage_invoice_submit_cancel
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
from erpnext.setup.doctype.company.company import update_company_current_month_sales
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
-from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
-from erpnext.stock.utils import calculate_mapped_packed_items_return
+from erpnext.stock.doctype.serial_no.serial_no import (
+ get_delivery_note_serial_no,
+ get_serial_nos,
+ update_serial_nos_after_submit,
+)
+
+form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
-form_grid_templates = {
- "items": "templates/form_grid/item_grid.html"
-}
class SalesInvoice(SellingController):
def __init__(self, *args, **kwargs):
super(SalesInvoice, self).__init__(*args, **kwargs)
- self.status_updater = [{
- 'source_dt': 'Sales Invoice Item',
- 'target_field': 'billed_amt',
- 'target_ref_field': 'amount',
- 'target_dt': 'Sales Order Item',
- 'join_field': 'so_detail',
- 'target_parent_dt': 'Sales Order',
- 'target_parent_field': 'per_billed',
- 'source_field': 'amount',
- 'percent_join_field': 'sales_order',
- 'status_field': 'billing_status',
- 'keyword': 'Billed',
- 'overflow_type': 'billing'
- }]
+ self.status_updater = [
+ {
+ "source_dt": "Sales Invoice Item",
+ "target_field": "billed_amt",
+ "target_ref_field": "amount",
+ "target_dt": "Sales Order Item",
+ "join_field": "so_detail",
+ "target_parent_dt": "Sales Order",
+ "target_parent_field": "per_billed",
+ "source_field": "amount",
+ "percent_join_field": "sales_order",
+ "status_field": "billing_status",
+ "keyword": "Billed",
+ "overflow_type": "billing",
+ }
+ ]
def set_indicator(self):
"""Set indicator for portal"""
@@ -110,7 +115,11 @@ class SalesInvoice(SellingController):
self.validate_fixed_asset()
self.set_income_account_for_fixed_assets()
self.validate_item_cost_centers()
- validate_inter_company_party(self.doctype, self.customer, self.company, self.inter_company_invoice_reference)
+ self.validate_income_account()
+
+ validate_inter_company_party(
+ self.doctype, self.customer, self.company, self.inter_company_invoice_reference
+ )
if cint(self.is_pos):
self.validate_pos()
@@ -126,15 +135,21 @@ class SalesInvoice(SellingController):
validate_service_stop_date(self)
if not self.is_opening:
- self.is_opening = 'No'
+ self.is_opening = "No"
- if self._action != 'submit' and self.update_stock and not self.is_return:
- set_batch_nos(self, 'warehouse', True)
+ if self._action != "submit" and self.update_stock and not self.is_return:
+ set_batch_nos(self, "warehouse", True)
if self.redeem_loyalty_points:
- lp = frappe.get_doc('Loyalty Program', self.loyalty_program)
- self.loyalty_redemption_account = lp.expense_account if not self.loyalty_redemption_account else self.loyalty_redemption_account
- self.loyalty_redemption_cost_center = lp.cost_center if not self.loyalty_redemption_cost_center else self.loyalty_redemption_cost_center
+ lp = frappe.get_doc("Loyalty Program", self.loyalty_program)
+ self.loyalty_redemption_account = (
+ lp.expense_account if not self.loyalty_redemption_account else self.loyalty_redemption_account
+ )
+ self.loyalty_redemption_cost_center = (
+ lp.cost_center
+ if not self.loyalty_redemption_cost_center
+ else self.loyalty_redemption_cost_center
+ )
self.set_against_income_account()
self.validate_c_form()
@@ -151,11 +166,16 @@ class SalesInvoice(SellingController):
if self.is_pos and not self.is_return:
self.verify_payment_amount_is_positive()
- #validate amount in mode of payments for returned invoices for pos must be negative
+ # validate amount in mode of payments for returned invoices for pos must be negative
if self.is_pos and self.is_return:
self.verify_payment_amount_is_negative()
- if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated:
+ if (
+ self.redeem_loyalty_points
+ and self.loyalty_program
+ and self.loyalty_points
+ and not self.is_consolidated
+ ):
validate_loyalty_points(self, self.loyalty_points)
self.reset_default_field_value("set_warehouse", "items", "warehouse")
@@ -168,14 +188,28 @@ class SalesInvoice(SellingController):
if self.update_stock:
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
- elif asset.status in ("Scrapped", "Cancelled") or (asset.status == "Sold" and not self.is_return):
- frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(d.idx, d.asset, asset.status))
+ elif asset.status in ("Scrapped", "Cancelled") or (
+ asset.status == "Sold" and not self.is_return
+ ):
+ frappe.throw(
+ _("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(
+ d.idx, d.asset, asset.status
+ )
+ )
def validate_item_cost_centers(self):
for item in self.items:
cost_center_company = frappe.get_cached_value("Cost Center", item.cost_center, "company")
if cost_center_company != self.company:
- frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company)))
+ frappe.throw(
+ _("Row #{0}: Cost Center {1} does not belong to company {2}").format(
+ frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company)
+ )
+ )
+
+ def validate_income_account(self):
+ for item in self.get("items"):
+ validate_account_head(item.idx, item.income_account, self.company, "Income")
def set_tax_withholding(self):
tax_withholding_details = get_party_tax_withholding_details(self)
@@ -194,8 +228,11 @@ class SalesInvoice(SellingController):
if not accounts or tax_withholding_account not in accounts:
self.append("taxes", tax_withholding_details)
- to_remove = [d for d in self.taxes
- if not d.tax_amount and d.charge_type == "Actual" and d.account_head == tax_withholding_account]
+ to_remove = [
+ d
+ for d in self.taxes
+ if not d.tax_amount and d.charge_type == "Actual" and d.account_head == tax_withholding_account
+ ]
for d in to_remove:
self.remove(d)
@@ -210,8 +247,9 @@ class SalesInvoice(SellingController):
self.validate_pos_paid_amount()
if not self.auto_repeat:
- frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype,
- self.company, self.base_grand_total, self)
+ frappe.get_doc("Authorization Control").validate_approving_authority(
+ self.doctype, self.company, self.base_grand_total, self
+ )
self.check_prev_docstatus()
@@ -228,6 +266,8 @@ class SalesInvoice(SellingController):
# because updating reserved qty in bin depends upon updated delivered qty in SO
if self.update_stock == 1:
self.update_stock_ledger()
+ if self.is_return and self.update_stock:
+ update_serial_nos_after_submit(self, "items")
# this sequence because outstanding may get -ve
self.make_gl_entries()
@@ -247,7 +287,9 @@ class SalesInvoice(SellingController):
self.update_time_sheet(self.name)
- if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
+ if (
+ frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction"
+ ):
update_company_current_month_sales(self.company)
self.update_project()
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
@@ -255,7 +297,9 @@ class SalesInvoice(SellingController):
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
if not self.is_return and not self.is_consolidated and self.loyalty_program:
self.make_loyalty_point_entry()
- elif self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program:
+ elif (
+ self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program
+ ):
against_si_doc = frappe.get_doc("Sales Invoice", self.return_against)
against_si_doc.delete_loyalty_point_entry()
against_si_doc.make_loyalty_point_entry()
@@ -263,7 +307,7 @@ class SalesInvoice(SellingController):
self.apply_loyalty_points()
# Healthcare Service Invoice.
- domain_settings = frappe.get_doc('Domain Settings')
+ domain_settings = frappe.get_doc("Domain Settings")
active_domains = [d.domain for d in domain_settings.active_domains]
if "Healthcare" in active_domains:
@@ -272,6 +316,9 @@ class SalesInvoice(SellingController):
self.process_common_party_accounting()
def validate_pos_return(self):
+ if self.is_consolidated:
+ # pos return is already validated in pos invoice
+ return
if self.is_pos and self.is_return:
total_amount_in_payments = 0
@@ -288,16 +335,16 @@ class SalesInvoice(SellingController):
def check_if_consolidated_invoice(self):
# since POS Invoice extends Sales Invoice, we explicitly check if doctype is Sales Invoice
if self.doctype == "Sales Invoice" and self.is_consolidated:
- invoice_or_credit_note = "consolidated_credit_note" if self.is_return else "consolidated_invoice"
+ invoice_or_credit_note = (
+ "consolidated_credit_note" if self.is_return else "consolidated_invoice"
+ )
pos_closing_entry = frappe.get_all(
- "POS Invoice Merge Log",
- filters={ invoice_or_credit_note: self.name },
- pluck="pos_closing_entry"
+ "POS Invoice Merge Log", filters={invoice_or_credit_note: self.name}, pluck="pos_closing_entry"
)
if pos_closing_entry and pos_closing_entry[0]:
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
frappe.bold("Consolidated Sales Invoice"),
- get_link_to_form("POS Closing Entry", pos_closing_entry[0])
+ get_link_to_form("POS Closing Entry", pos_closing_entry[0]),
)
frappe.throw(msg, title=_("Not Allowed"))
@@ -339,14 +386,18 @@ class SalesInvoice(SellingController):
if self.update_stock == 1:
self.repost_future_sle_and_gle()
- frappe.db.set(self, 'status', 'Cancelled')
+ frappe.db.set(self, "status", "Cancelled")
- if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
+ if (
+ frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction"
+ ):
update_company_current_month_sales(self.company)
self.update_project()
if not self.is_return and not self.is_consolidated and self.loyalty_program:
self.delete_loyalty_point_entry()
- elif self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program:
+ elif (
+ self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program
+ ):
against_si_doc = frappe.get_doc("Sales Invoice", self.return_against)
against_si_doc.delete_loyalty_point_entry()
against_si_doc.make_loyalty_point_entry()
@@ -354,56 +405,62 @@ class SalesInvoice(SellingController):
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
# Healthcare Service Invoice.
- domain_settings = frappe.get_doc('Domain Settings')
+ domain_settings = frappe.get_doc("Domain Settings")
active_domains = [d.domain for d in domain_settings.active_domains]
if "Healthcare" in active_domains:
manage_invoice_submit_cancel(self, "on_cancel")
self.unlink_sales_invoice_from_timesheets()
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
def update_status_updater_args(self):
if cint(self.update_stock):
- self.status_updater.append({
- 'source_dt':'Sales Invoice Item',
- 'target_dt':'Sales Order Item',
- 'target_parent_dt':'Sales Order',
- 'target_parent_field':'per_delivered',
- 'target_field':'delivered_qty',
- 'target_ref_field':'qty',
- 'source_field':'qty',
- 'join_field':'so_detail',
- 'percent_join_field':'sales_order',
- 'status_field':'delivery_status',
- 'keyword':'Delivered',
- 'second_source_dt': 'Delivery Note Item',
- 'second_source_field': 'qty',
- 'second_join_field': 'so_detail',
- 'overflow_type': 'delivery',
- 'extra_cond': """ and exists(select name from `tabSales Invoice`
- where name=`tabSales Invoice Item`.parent and update_stock = 1)"""
- })
+ self.status_updater.append(
+ {
+ "source_dt": "Sales Invoice Item",
+ "target_dt": "Sales Order Item",
+ "target_parent_dt": "Sales Order",
+ "target_parent_field": "per_delivered",
+ "target_field": "delivered_qty",
+ "target_ref_field": "qty",
+ "source_field": "qty",
+ "join_field": "so_detail",
+ "percent_join_field": "sales_order",
+ "status_field": "delivery_status",
+ "keyword": "Delivered",
+ "second_source_dt": "Delivery Note Item",
+ "second_source_field": "qty",
+ "second_join_field": "so_detail",
+ "overflow_type": "delivery",
+ "extra_cond": """ and exists(select name from `tabSales Invoice`
+ where name=`tabSales Invoice Item`.parent and update_stock = 1)""",
+ }
+ )
if cint(self.is_return):
- self.status_updater.append({
- 'source_dt': 'Sales Invoice Item',
- 'target_dt': 'Sales Order Item',
- 'join_field': 'so_detail',
- 'target_field': 'returned_qty',
- 'target_parent_dt': 'Sales Order',
- 'source_field': '-1 * qty',
- 'second_source_dt': 'Delivery Note Item',
- 'second_source_field': '-1 * qty',
- 'second_join_field': 'so_detail',
- 'extra_cond': """ and exists (select name from `tabSales Invoice` where name=`tabSales Invoice Item`.parent and update_stock=1 and is_return=1)"""
- })
+ self.status_updater.append(
+ {
+ "source_dt": "Sales Invoice Item",
+ "target_dt": "Sales Order Item",
+ "join_field": "so_detail",
+ "target_field": "returned_qty",
+ "target_parent_dt": "Sales Order",
+ "source_field": "-1 * qty",
+ "second_source_dt": "Delivery Note Item",
+ "second_source_field": "-1 * qty",
+ "second_join_field": "so_detail",
+ "extra_cond": """ and exists (select name from `tabSales Invoice` where name=`tabSales Invoice Item`.parent and update_stock=1 and is_return=1)""",
+ }
+ )
def check_credit_limit(self):
from erpnext.selling.doctype.customer.customer import check_credit_limit
validate_against_credit_limit = False
- bypass_credit_limit_check_at_sales_order = frappe.db.get_value("Customer Credit Limit",
- filters={'parent': self.customer, 'parenttype': 'Customer', 'company': self.company},
- fieldname=["bypass_credit_limit_check"])
+ bypass_credit_limit_check_at_sales_order = frappe.db.get_value(
+ "Customer Credit Limit",
+ filters={"parent": self.customer, "parenttype": "Customer", "company": self.company},
+ fieldname=["bypass_credit_limit_check"],
+ )
if bypass_credit_limit_check_at_sales_order:
validate_against_credit_limit = True
@@ -417,7 +474,7 @@ class SalesInvoice(SellingController):
def unlink_sales_invoice_from_timesheets(self):
for row in self.timesheets:
- timesheet = frappe.get_doc('Timesheet', row.time_sheet)
+ timesheet = frappe.get_doc("Timesheet", row.time_sheet)
for time_log in timesheet.time_logs:
if time_log.sales_invoice == self.name:
time_log.sales_invoice = None
@@ -433,15 +490,17 @@ class SalesInvoice(SellingController):
if not self.debit_to:
self.debit_to = get_party_account("Customer", self.customer, self.company)
- self.party_account_currency = frappe.db.get_value("Account", self.debit_to, "account_currency", cache=True)
+ self.party_account_currency = frappe.db.get_value(
+ "Account", self.debit_to, "account_currency", cache=True
+ )
if not self.due_date and self.customer:
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
super(SalesInvoice, self).set_missing_values(for_validate)
print_format = pos.get("print_format") if pos else None
- if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')):
- print_format = 'POS Invoice'
+ if not print_format and not cint(frappe.db.get_value("Print Format", "POS Invoice", "disabled")):
+ print_format = "POS Invoice"
if pos:
return {
@@ -449,7 +508,7 @@ class SalesInvoice(SellingController):
"allow_edit_rate": pos.get("allow_user_to_edit_rate"),
"allow_edit_discount": pos.get("allow_user_to_edit_discount"),
"campaign": pos.get("campaign"),
- "allow_print_before_pay": pos.get("allow_print_before_pay")
+ "allow_print_before_pay": pos.get("allow_print_before_pay"),
}
def update_time_sheet(self, sales_invoice):
@@ -465,9 +524,11 @@ class SalesInvoice(SellingController):
def update_time_sheet_detail(self, timesheet, args, sales_invoice):
for data in timesheet.time_logs:
- if (self.project and args.timesheet_detail == data.name) or \
- (not self.project and not data.sales_invoice) or \
- (not sales_invoice and data.sales_invoice == self.name):
+ if (
+ (self.project and args.timesheet_detail == data.name)
+ or (not self.project and not data.sales_invoice)
+ or (not sales_invoice and data.sales_invoice == self.name)
+ ):
data.sales_invoice = sales_invoice
def on_update(self):
@@ -477,7 +538,7 @@ class SalesInvoice(SellingController):
paid_amount = 0.0
base_paid_amount = 0.0
for data in self.payments:
- data.base_amount = flt(data.amount*self.conversion_rate, self.precision("base_paid_amount"))
+ data.base_amount = flt(data.amount * self.conversion_rate, self.precision("base_paid_amount"))
paid_amount += data.amount
base_paid_amount += data.base_amount
@@ -488,7 +549,7 @@ class SalesInvoice(SellingController):
for data in self.timesheets:
if data.time_sheet:
status = frappe.db.get_value("Timesheet", data.time_sheet, "status")
- if status not in ['Submitted', 'Payslip']:
+ if status not in ["Submitted", "Payslip"]:
frappe.throw(_("Timesheet {0} is already completed or cancelled").format(data.time_sheet))
def set_pos_fields(self, for_validate=False):
@@ -497,20 +558,23 @@ class SalesInvoice(SellingController):
return
if not self.account_for_change_amount:
- self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
+ self.account_for_change_amount = frappe.get_cached_value(
+ "Company", self.company, "default_cash_account"
+ )
from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details
+
if not self.pos_profile and not self.flags.ignore_pos_profile:
pos_profile = get_pos_profile(self.company) or {}
if not pos_profile:
return
- self.pos_profile = pos_profile.get('name')
+ self.pos_profile = pos_profile.get("name")
pos = {}
if self.pos_profile:
- pos = frappe.get_doc('POS Profile', self.pos_profile)
+ pos = frappe.get_doc("POS Profile", self.pos_profile)
- if not self.get('payments') and not for_validate:
+ if not self.get("payments") and not for_validate:
update_multi_mode_option(self, pos)
if pos:
@@ -523,35 +587,52 @@ class SalesInvoice(SellingController):
if not for_validate:
self.ignore_pricing_rule = pos.ignore_pricing_rule
- if pos.get('account_for_change_amount'):
- self.account_for_change_amount = pos.get('account_for_change_amount')
+ if pos.get("account_for_change_amount"):
+ self.account_for_change_amount = pos.get("account_for_change_amount")
- for fieldname in ('currency', 'letter_head', 'tc_name',
- 'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges',
- 'write_off_cost_center', 'apply_discount_on', 'cost_center'):
- if (not for_validate) or (for_validate and not self.get(fieldname)):
- self.set(fieldname, pos.get(fieldname))
+ for fieldname in (
+ "currency",
+ "letter_head",
+ "tc_name",
+ "company",
+ "select_print_heading",
+ "write_off_account",
+ "taxes_and_charges",
+ "write_off_cost_center",
+ "apply_discount_on",
+ "cost_center",
+ ):
+ if (not for_validate) or (for_validate and not self.get(fieldname)):
+ self.set(fieldname, pos.get(fieldname))
if pos.get("company_address"):
self.company_address = pos.get("company_address")
if self.customer:
- customer_price_list, customer_group = frappe.get_value("Customer", self.customer, ['default_price_list', 'customer_group'])
- customer_group_price_list = frappe.get_value("Customer Group", customer_group, 'default_price_list')
- selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list')
+ customer_price_list, customer_group = frappe.get_value(
+ "Customer", self.customer, ["default_price_list", "customer_group"]
+ )
+ customer_group_price_list = frappe.get_value(
+ "Customer Group", customer_group, "default_price_list"
+ )
+ selling_price_list = (
+ customer_price_list or customer_group_price_list or pos.get("selling_price_list")
+ )
else:
- selling_price_list = pos.get('selling_price_list')
+ selling_price_list = pos.get("selling_price_list")
if selling_price_list:
- self.set('selling_price_list', selling_price_list)
+ self.set("selling_price_list", selling_price_list)
if not for_validate:
self.update_stock = cint(pos.get("update_stock"))
# set pos values in items
for item in self.get("items"):
- if item.get('item_code'):
- profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos, update_data=True)
+ if item.get("item_code"):
+ profile_details = get_pos_profile_item_details(
+ pos, frappe._dict(item.as_dict()), pos, update_data=True
+ )
for fname, val in iteritems(profile_details):
if (not for_validate) or (for_validate and not item.get(fname)):
item.set(fname, val)
@@ -575,22 +656,29 @@ class SalesInvoice(SellingController):
if not self.debit_to:
self.raise_missing_debit_credit_account_error("Customer", self.customer)
- account = frappe.get_cached_value("Account", self.debit_to,
- ["account_type", "report_type", "account_currency"], as_dict=True)
+ account = frappe.get_cached_value(
+ "Account", self.debit_to, ["account_type", "report_type", "account_currency"], as_dict=True
+ )
if not account:
frappe.throw(_("Debit To is required"), title=_("Account Missing"))
if account.report_type != "Balance Sheet":
- msg = _("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To")) + " "
- msg += _("You can change the parent account to a Balance Sheet account or select a different account.")
+ msg = (
+ _("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To")) + " "
+ )
+ msg += _(
+ "You can change the parent account to a Balance Sheet account or select a different account."
+ )
frappe.throw(msg, title=_("Invalid Account"))
if self.customer and account.account_type != "Receivable":
- msg = _("Please ensure {} account {} is a Receivable account.").format(
- frappe.bold("Debit To"),
- frappe.bold(self.debit_to)
- ) + " "
+ msg = (
+ _("Please ensure {} account {} is a Receivable account.").format(
+ frappe.bold("Debit To"), frappe.bold(self.debit_to)
+ )
+ + " "
+ )
msg += _("Change the account type to Receivable or select a different account.")
frappe.throw(msg, title=_("Invalid Account"))
@@ -599,52 +687,60 @@ class SalesInvoice(SellingController):
def clear_unallocated_mode_of_payments(self):
self.set("payments", self.get("payments", {"amount": ["not in", [0, None, ""]]}))
- frappe.db.sql("""delete from `tabSales Invoice Payment` where parent = %s
- and amount = 0""", self.name)
+ frappe.db.sql(
+ """delete from `tabSales Invoice Payment` where parent = %s
+ and amount = 0""",
+ self.name,
+ )
def validate_with_previous_doc(self):
- super(SalesInvoice, self).validate_with_previous_doc({
- "Sales Order": {
- "ref_dn_field": "sales_order",
- "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]]
- },
- "Sales Order Item": {
- "ref_dn_field": "so_detail",
- "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]],
- "is_child_table": True,
- "allow_duplicate_prev_row_id": True
- },
- "Delivery Note": {
- "ref_dn_field": "delivery_note",
- "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]]
- },
- "Delivery Note Item": {
- "ref_dn_field": "dn_detail",
- "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]],
- "is_child_table": True,
- "allow_duplicate_prev_row_id": True
- },
- })
+ super(SalesInvoice, self).validate_with_previous_doc(
+ {
+ "Sales Order": {
+ "ref_dn_field": "sales_order",
+ "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]],
+ },
+ "Sales Order Item": {
+ "ref_dn_field": "so_detail",
+ "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]],
+ "is_child_table": True,
+ "allow_duplicate_prev_row_id": True,
+ },
+ "Delivery Note": {
+ "ref_dn_field": "delivery_note",
+ "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]],
+ },
+ "Delivery Note Item": {
+ "ref_dn_field": "dn_detail",
+ "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]],
+ "is_child_table": True,
+ "allow_duplicate_prev_row_id": True,
+ },
+ }
+ )
- if cint(frappe.db.get_single_value('Selling Settings', 'maintain_same_sales_rate')) and not self.is_return:
- self.validate_rate_with_reference_doc([
- ["Sales Order", "sales_order", "so_detail"],
- ["Delivery Note", "delivery_note", "dn_detail"]
- ])
+ if (
+ cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate"))
+ and not self.is_return
+ ):
+ self.validate_rate_with_reference_doc(
+ [["Sales Order", "sales_order", "so_detail"], ["Delivery Note", "delivery_note", "dn_detail"]]
+ )
def set_against_income_account(self):
"""Set against account for debit to account"""
against_acc = []
- for d in self.get('items'):
+ for d in self.get("items"):
if d.income_account and d.income_account not in against_acc:
against_acc.append(d.income_account)
- self.against_income_account = ','.join(against_acc)
+ self.against_income_account = ",".join(against_acc)
def add_remarks(self):
if not self.remarks:
if self.po_no and self.po_date:
- self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no,
- formatdate(self.po_date))
+ self.remarks = _("Against Customer Order {0} dated {1}").format(
+ self.po_no, formatdate(self.po_date)
+ )
else:
self.remarks = _("No Remarks")
@@ -660,36 +756,41 @@ class SalesInvoice(SellingController):
if self.is_return:
return
- prev_doc_field_map = {'Sales Order': ['so_required', 'is_pos'],'Delivery Note': ['dn_required', 'update_stock']}
+ prev_doc_field_map = {
+ "Sales Order": ["so_required", "is_pos"],
+ "Delivery Note": ["dn_required", "update_stock"],
+ }
for key, value in iteritems(prev_doc_field_map):
- if frappe.db.get_single_value('Selling Settings', value[0]) == 'Yes':
+ if frappe.db.get_single_value("Selling Settings", value[0]) == "Yes":
- if frappe.get_value('Customer', self.customer, value[0]):
+ if frappe.get_value("Customer", self.customer, value[0]):
continue
- for d in self.get('items'):
- if (d.item_code and not d.get(key.lower().replace(' ', '_')) and not self.get(value[1])):
+ for d in self.get("items"):
+ if d.item_code and not d.get(key.lower().replace(" ", "_")) and not self.get(value[1]):
msgprint(_("{0} is mandatory for Item {1}").format(key, d.item_code), raise_exception=1)
-
def validate_proj_cust(self):
"""check for does customer belong to same project as entered.."""
if self.project and self.customer:
- res = frappe.db.sql("""select name from `tabProject`
+ res = frappe.db.sql(
+ """select name from `tabProject`
where name = %s and (customer = %s or customer is null or customer = '')""",
- (self.project, self.customer))
+ (self.project, self.customer),
+ )
if not res:
- throw(_("Customer {0} does not belong to project {1}").format(self.customer,self.project))
+ throw(_("Customer {0} does not belong to project {1}").format(self.customer, self.project))
def validate_pos(self):
if self.is_return:
invoice_total = self.rounded_total or self.grand_total
- if flt(self.paid_amount) + flt(self.write_off_amount) - flt(invoice_total) > \
- 1.0/(10.0**(self.precision("grand_total") + 1.0)):
- frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
+ if flt(self.paid_amount) + flt(self.write_off_amount) - flt(invoice_total) > 1.0 / (
+ 10.0 ** (self.precision("grand_total") + 1.0)
+ ):
+ frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
def validate_item_code(self):
- for d in self.get('items'):
+ for d in self.get("items"):
if not d.item_code and self.is_opening == "No":
msgprint(_("Item Code required at Row No {0}").format(d.idx), raise_exception=True)
@@ -697,17 +798,24 @@ class SalesInvoice(SellingController):
super(SalesInvoice, self).validate_warehouse()
for d in self.get_item_list():
- if not d.warehouse and d.item_code and frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
+ if (
+ not d.warehouse
+ and d.item_code
+ and frappe.get_cached_value("Item", d.item_code, "is_stock_item")
+ ):
frappe.throw(_("Warehouse required for stock Item {0}").format(d.item_code))
def validate_delivery_note(self):
for d in self.get("items"):
if d.delivery_note:
- msgprint(_("Stock cannot be updated against Delivery Note {0}").format(d.delivery_note), raise_exception=1)
+ msgprint(
+ _("Stock cannot be updated against Delivery Note {0}").format(d.delivery_note),
+ raise_exception=1,
+ )
def validate_write_off_account(self):
if flt(self.write_off_amount) and not self.write_off_account:
- self.write_off_account = frappe.get_cached_value('Company', self.company, 'write_off_account')
+ self.write_off_account = frappe.get_cached_value("Company", self.company, "write_off_account")
if flt(self.write_off_amount) and not self.write_off_account:
msgprint(_("Please enter Write Off Account"), raise_exception=1)
@@ -717,18 +825,23 @@ class SalesInvoice(SellingController):
msgprint(_("Please enter Account for Change Amount"), raise_exception=1)
def validate_c_form(self):
- """ Blank C-form no if C-form applicable marked as 'No'"""
- if self.amended_from and self.c_form_applicable == 'No' and self.c_form_no:
- frappe.db.sql("""delete from `tabC-Form Invoice Detail` where invoice_no = %s
- and parent = %s""", (self.amended_from, self.c_form_no))
+ """Blank C-form no if C-form applicable marked as 'No'"""
+ if self.amended_from and self.c_form_applicable == "No" and self.c_form_no:
+ frappe.db.sql(
+ """delete from `tabC-Form Invoice Detail` where invoice_no = %s
+ and parent = %s""",
+ (self.amended_from, self.c_form_no),
+ )
- frappe.db.set(self, 'c_form_no', '')
+ frappe.db.set(self, "c_form_no", "")
def validate_c_form_on_cancel(self):
- """ Display message if C-Form no exists on cancellation of Sales Invoice"""
- if self.c_form_applicable == 'Yes' and self.c_form_no:
- msgprint(_("Please remove this Invoice {0} from C-Form {1}")
- .format(self.name, self.c_form_no), raise_exception = 1)
+ """Display message if C-Form no exists on cancellation of Sales Invoice"""
+ if self.c_form_applicable == "Yes" and self.c_form_no:
+ msgprint(
+ _("Please remove this Invoice {0} from C-Form {1}").format(self.name, self.c_form_no),
+ raise_exception=1,
+ )
def validate_dropship_item(self):
for item in self.items:
@@ -737,30 +850,36 @@ class SalesInvoice(SellingController):
frappe.throw(_("Could not update stock, invoice contains drop shipping item."))
def update_current_stock(self):
- for d in self.get('items'):
+ for d in self.get("items"):
if d.item_code and d.warehouse:
- bin = frappe.db.sql("select actual_qty from `tabBin` where item_code = %s and warehouse = %s", (d.item_code, d.warehouse), as_dict = 1)
- d.actual_qty = bin and flt(bin[0]['actual_qty']) or 0
+ bin = frappe.db.sql(
+ "select actual_qty from `tabBin` where item_code = %s and warehouse = %s",
+ (d.item_code, d.warehouse),
+ as_dict=1,
+ )
+ d.actual_qty = bin and flt(bin[0]["actual_qty"]) or 0
- for d in self.get('packed_items'):
- bin = frappe.db.sql("select actual_qty, projected_qty from `tabBin` where item_code = %s and warehouse = %s", (d.item_code, d.warehouse), as_dict = 1)
- d.actual_qty = bin and flt(bin[0]['actual_qty']) or 0
- d.projected_qty = bin and flt(bin[0]['projected_qty']) or 0
+ for d in self.get("packed_items"):
+ bin = frappe.db.sql(
+ "select actual_qty, projected_qty from `tabBin` where item_code = %s and warehouse = %s",
+ (d.item_code, d.warehouse),
+ as_dict=1,
+ )
+ d.actual_qty = bin and flt(bin[0]["actual_qty"]) or 0
+ d.projected_qty = bin and flt(bin[0]["projected_qty"]) or 0
def update_packing_list(self):
if cint(self.update_stock) == 1:
- if cint(self.is_return) and self.return_against:
- calculate_mapped_packed_items_return(self)
- else:
- from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
- make_packing_list(self)
+ from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
+
+ make_packing_list(self)
else:
- self.set('packed_items', [])
+ self.set("packed_items", [])
def set_billing_hours_and_amount(self):
if not self.project:
for timesheet in self.timesheets:
- ts_doc = frappe.get_doc('Timesheet', timesheet.time_sheet)
+ ts_doc = frappe.get_doc("Timesheet", timesheet.time_sheet)
if not timesheet.billing_hours and ts_doc.total_billable_hours:
timesheet.billing_hours = ts_doc.total_billable_hours
@@ -775,17 +894,20 @@ class SalesInvoice(SellingController):
@frappe.whitelist()
def add_timesheet_data(self):
- self.set('timesheets', [])
+ self.set("timesheets", [])
if self.project:
for data in get_projectwise_timesheet_data(self.project):
- self.append('timesheets', {
- 'time_sheet': data.time_sheet,
- 'billing_hours': data.billing_hours,
- 'billing_amount': data.billing_amount,
- 'timesheet_detail': data.name,
- 'activity_type': data.activity_type,
- 'description': data.description
- })
+ self.append(
+ "timesheets",
+ {
+ "time_sheet": data.time_sheet,
+ "billing_hours": data.billing_hours,
+ "billing_amount": data.billing_amount,
+ "timesheet_detail": data.name,
+ "activity_type": data.activity_type,
+ "description": data.description,
+ },
+ )
self.calculate_billing_amount_for_timesheet()
@@ -797,13 +919,19 @@ class SalesInvoice(SellingController):
self.total_billing_hours = timesheet_sum("billing_hours")
def get_warehouse(self):
- user_pos_profile = frappe.db.sql("""select name, warehouse from `tabPOS Profile`
- where ifnull(user,'') = %s and company = %s""", (frappe.session['user'], self.company))
+ user_pos_profile = frappe.db.sql(
+ """select name, warehouse from `tabPOS Profile`
+ where ifnull(user,'') = %s and company = %s""",
+ (frappe.session["user"], self.company),
+ )
warehouse = user_pos_profile[0][1] if user_pos_profile else None
if not warehouse:
- global_pos_profile = frappe.db.sql("""select name, warehouse from `tabPOS Profile`
- where (user is null or user = '') and company = %s""", self.company)
+ global_pos_profile = frappe.db.sql(
+ """select name, warehouse from `tabPOS Profile`
+ where (user is null or user = '') and company = %s""",
+ self.company,
+ )
if global_pos_profile:
warehouse = global_pos_profile[0][1]
@@ -817,14 +945,16 @@ class SalesInvoice(SellingController):
for d in self.get("items"):
if d.is_fixed_asset:
if not disposal_account:
- disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(self.company)
+ disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(
+ self.company
+ )
d.income_account = disposal_account
if not d.cost_center:
d.cost_center = depreciation_cost_center
def check_prev_docstatus(self):
- for d in self.get('items'):
+ for d in self.get("items"):
if d.sales_order and frappe.db.get_value("Sales Order", d.sales_order, "docstatus") != 1:
frappe.throw(_("Sales Order {0} is not submitted").format(d.sales_order))
@@ -840,22 +970,35 @@ class SalesInvoice(SellingController):
if gl_entries:
# if POS and amount is written off, updating outstanding amt after posting all gl entries
- update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account or
- cint(self.redeem_loyalty_points)) else "Yes"
+ update_outstanding = (
+ "No"
+ if (cint(self.is_pos) or self.write_off_account or cint(self.redeem_loyalty_points))
+ else "Yes"
+ )
if self.docstatus == 1:
- make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost)
+ make_gl_entries(
+ gl_entries,
+ update_outstanding=update_outstanding,
+ merge_entries=False,
+ from_repost=from_repost,
+ )
elif self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
if update_outstanding == "No":
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
- update_outstanding_amt(self.debit_to, "Customer", self.customer,
- self.doctype, self.return_against if cint(self.is_return) and self.return_against else self.name)
- elif self.docstatus == 2 and cint(self.update_stock) \
- and cint(auto_accounting_for_stock):
- make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
+ update_outstanding_amt(
+ self.debit_to,
+ "Customer",
+ self.customer,
+ self.doctype,
+ self.return_against if cint(self.is_return) and self.return_against else self.name,
+ )
+
+ elif self.docstatus == 2 and cint(self.update_stock) and cint(auto_accounting_for_stock):
+ make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
def get_gl_entries(self, warehouse_account=None):
from erpnext.accounts.general_ledger import merge_similar_entries
@@ -885,27 +1028,40 @@ class SalesInvoice(SellingController):
def make_customer_gl_entry(self, gl_entries):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
- grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
- base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
- else self.base_grand_total, self.precision("base_grand_total"))
+ grand_total = (
+ self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
+ )
+ base_grand_total = flt(
+ self.base_rounded_total
+ if (self.base_rounding_adjustment and self.base_rounded_total)
+ else self.base_grand_total,
+ self.precision("base_grand_total"),
+ )
if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle
gl_entries.append(
- self.get_gl_dict({
- "account": self.debit_to,
- "party_type": "Customer",
- "party": self.customer,
- "due_date": self.due_date,
- "against": self.against_income_account,
- "debit": base_grand_total,
- "debit_in_account_currency": base_grand_total \
- if self.party_account_currency==self.company_currency else grand_total,
- "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
- "against_voucher_type": self.doctype,
- "cost_center": self.cost_center,
- "project": self.project
- }, self.party_account_currency, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.debit_to,
+ "party_type": "Customer",
+ "party": self.customer,
+ "due_date": self.due_date,
+ "against": self.against_income_account,
+ "debit": base_grand_total,
+ "debit_in_account_currency": base_grand_total
+ if self.party_account_currency == self.company_currency
+ else grand_total,
+ "against_voucher": self.return_against
+ if cint(self.is_return) and self.return_against
+ else self.name,
+ "against_voucher_type": self.doctype,
+ "cost_center": self.cost_center,
+ "project": self.project,
+ },
+ self.party_account_currency,
+ item=self,
+ )
)
def make_tax_gl_entries(self, gl_entries):
@@ -915,29 +1071,39 @@ class SalesInvoice(SellingController):
if flt(tax.base_tax_amount_after_discount_amount):
account_currency = get_account_currency(tax.account_head)
gl_entries.append(
- self.get_gl_dict({
- "account": tax.account_head,
- "against": self.customer,
- "credit": flt(base_amount,
- tax.precision("tax_amount_after_discount_amount")),
- "credit_in_account_currency": (flt(base_amount,
- tax.precision("base_tax_amount_after_discount_amount")) if account_currency==self.company_currency else
- flt(amount, tax.precision("tax_amount_after_discount_amount"))),
- "cost_center": tax.cost_center
- }, account_currency, item=tax)
+ self.get_gl_dict(
+ {
+ "account": tax.account_head,
+ "against": self.customer,
+ "credit": flt(base_amount, tax.precision("tax_amount_after_discount_amount")),
+ "credit_in_account_currency": (
+ flt(base_amount, tax.precision("base_tax_amount_after_discount_amount"))
+ if account_currency == self.company_currency
+ else flt(amount, tax.precision("tax_amount_after_discount_amount"))
+ ),
+ "cost_center": tax.cost_center,
+ },
+ account_currency,
+ item=tax,
+ )
)
def make_internal_transfer_gl_entries(self, gl_entries):
if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges):
account_currency = get_account_currency(self.unrealized_profit_loss_account)
gl_entries.append(
- self.get_gl_dict({
- "account": self.unrealized_profit_loss_account,
- "against": self.customer,
- "debit": flt(self.total_taxes_and_charges),
- "debit_in_account_currency": flt(self.base_total_taxes_and_charges),
- "cost_center": self.cost_center
- }, account_currency, item=self))
+ self.get_gl_dict(
+ {
+ "account": self.unrealized_profit_loss_account,
+ "against": self.customer,
+ "debit": flt(self.total_taxes_and_charges),
+ "debit_in_account_currency": flt(self.base_total_taxes_and_charges),
+ "cost_center": self.cost_center,
+ },
+ account_currency,
+ item=self,
+ )
+ )
def make_item_gl_entries(self, gl_entries):
# income account gl entries
@@ -947,8 +1113,9 @@ class SalesInvoice(SellingController):
asset = self.get_asset(item)
if self.is_return:
- fixed_asset_gl_entries = get_gl_entries_on_asset_regain(asset,
- item.base_net_amount, item.finance_book)
+ fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
+ asset, item.base_net_amount, item.finance_book
+ )
asset.db_set("disposal_date", None)
if asset.calculate_depreciation:
@@ -956,8 +1123,9 @@ class SalesInvoice(SellingController):
self.reset_depreciation_schedule(asset)
else:
- fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset,
- item.base_net_amount, item.finance_book)
+ fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
+ asset, item.base_net_amount, item.finance_book
+ )
asset.db_set("disposal_date", self.posting_date)
if asset.calculate_depreciation:
@@ -972,47 +1140,57 @@ class SalesInvoice(SellingController):
else:
# Do not book income for transfer within same company
if not self.is_internal_transfer():
- income_account = (item.income_account
- if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account)
+ income_account = (
+ item.income_account
+ if (not item.enable_deferred_revenue or self.is_return)
+ else item.deferred_revenue_account
+ )
amount, base_amount = self.get_amount_and_base_amount(item, self.enable_discount_accounting)
account_currency = get_account_currency(income_account)
gl_entries.append(
- self.get_gl_dict({
- "account": income_account,
- "against": self.customer,
- "credit": flt(base_amount, item.precision("base_net_amount")),
- "credit_in_account_currency": (flt(base_amount, item.precision("base_net_amount"))
- if account_currency==self.company_currency
- else flt(amount, item.precision("net_amount"))),
- "cost_center": item.cost_center,
- "project": item.project or self.project
- }, account_currency, item=item)
+ self.get_gl_dict(
+ {
+ "account": income_account,
+ "against": self.customer,
+ "credit": flt(base_amount, item.precision("base_net_amount")),
+ "credit_in_account_currency": (
+ flt(base_amount, item.precision("base_net_amount"))
+ if account_currency == self.company_currency
+ else flt(amount, item.precision("net_amount"))
+ ),
+ "cost_center": item.cost_center,
+ "project": item.project or self.project,
+ },
+ account_currency,
+ item=item,
+ )
)
# expense account gl entries
- if cint(self.update_stock) and \
- erpnext.is_perpetual_inventory_enabled(self.company):
+ if cint(self.update_stock) and erpnext.is_perpetual_inventory_enabled(self.company):
gl_entries += super(SalesInvoice, self).get_gl_entries()
def get_asset(self, item):
- if item.get('asset'):
+ if item.get("asset"):
asset = frappe.get_doc("Asset", item.asset)
else:
- frappe.throw(_(
- "Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name),
- title=_("Missing Asset")
+ frappe.throw(
+ _("Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name),
+ title=_("Missing Asset"),
)
self.check_finance_books(item, asset)
return asset
def check_finance_books(self, item, asset):
- if (len(asset.finance_books) > 1 and not item.finance_book
- and asset.finance_books[0].finance_book):
- frappe.throw(_("Select finance book for the item {0} at row {1}")
- .format(item.item_code, item.idx))
+ if (
+ len(asset.finance_books) > 1 and not item.finance_book and asset.finance_books[0].finance_book
+ ):
+ frappe.throw(
+ _("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx)
+ )
def depreciate_asset(self, asset):
asset.flags.ignore_validate_update_after_submit = True
@@ -1032,14 +1210,12 @@ class SalesInvoice(SellingController):
def modify_depreciation_schedule_for_asset_repairs(self, asset):
asset_repairs = frappe.get_all(
- 'Asset Repair',
- filters = {'asset': asset.name},
- fields = ['name', 'increase_in_asset_life']
+ "Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"]
)
for repair in asset_repairs:
if repair.increase_in_asset_life:
- asset_repair = frappe.get_doc('Asset Repair', repair.name)
+ asset_repair = frappe.get_doc("Asset Repair", repair.name)
asset_repair.modify_depreciation_schedule()
asset.prepare_depreciation_data()
@@ -1049,8 +1225,8 @@ class SalesInvoice(SellingController):
posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice()
row = -1
- finance_book = asset.get('schedules')[0].get('finance_book')
- for schedule in asset.get('schedules'):
+ finance_book = asset.get("schedules")[0].get("finance_book")
+ for schedule in asset.get("schedules"):
if schedule.finance_book != finance_book:
row = 0
finance_book = schedule.finance_book
@@ -1058,8 +1234,9 @@ class SalesInvoice(SellingController):
row += 1
if schedule.schedule_date == posting_date_of_original_invoice:
- if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice) \
- or self.sale_happens_in_the_future(posting_date_of_original_invoice):
+ if not self.sale_was_made_on_original_schedule_date(
+ asset, schedule, row, posting_date_of_original_invoice
+ ) or self.sale_happens_in_the_future(posting_date_of_original_invoice):
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate()
@@ -1074,14 +1251,17 @@ class SalesInvoice(SellingController):
asset.save()
def get_posting_date_of_sales_invoice(self):
- return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date')
+ return frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
- def sale_was_made_on_original_schedule_date(self, asset, schedule, row, posting_date_of_original_invoice):
- for finance_book in asset.get('finance_books'):
+ def sale_was_made_on_original_schedule_date(
+ self, asset, schedule, row, posting_date_of_original_invoice
+ ):
+ for finance_book in asset.get("finance_books"):
if schedule.finance_book == finance_book.finance_book:
- orginal_schedule_date = add_months(finance_book.depreciation_start_date,
- row * cint(finance_book.frequency_of_depreciation))
+ orginal_schedule_date = add_months(
+ finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation)
+ )
if orginal_schedule_date == posting_date_of_original_invoice:
return True
@@ -1102,7 +1282,9 @@ class SalesInvoice(SellingController):
@property
def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"):
- self._enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
+ self._enable_discount_accounting = cint(
+ frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting")
+ )
return self._enable_discount_accounting
@@ -1110,36 +1292,46 @@ class SalesInvoice(SellingController):
if self.is_return:
asset.set_status()
else:
- asset.set_status("Sold" if self.docstatus==1 else None)
+ asset.set_status("Sold" if self.docstatus == 1 else None)
def make_loyalty_point_redemption_gle(self, gl_entries):
if cint(self.redeem_loyalty_points):
gl_entries.append(
- self.get_gl_dict({
- "account": self.debit_to,
- "party_type": "Customer",
- "party": self.customer,
- "against": "Expense account - " + cstr(self.loyalty_redemption_account) + " for the Loyalty Program",
- "credit": self.loyalty_amount,
- "against_voucher": self.return_against if cint(self.is_return) else self.name,
- "against_voucher_type": self.doctype,
- "cost_center": self.cost_center
- }, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.debit_to,
+ "party_type": "Customer",
+ "party": self.customer,
+ "against": "Expense account - "
+ + cstr(self.loyalty_redemption_account)
+ + " for the Loyalty Program",
+ "credit": self.loyalty_amount,
+ "against_voucher": self.return_against if cint(self.is_return) else self.name,
+ "against_voucher_type": self.doctype,
+ "cost_center": self.cost_center,
+ },
+ item=self,
+ )
)
gl_entries.append(
- self.get_gl_dict({
- "account": self.loyalty_redemption_account,
- "cost_center": self.cost_center or self.loyalty_redemption_cost_center,
- "against": self.customer,
- "debit": self.loyalty_amount,
- "remark": "Loyalty Points redeemed by the customer"
- }, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.loyalty_redemption_account,
+ "cost_center": self.cost_center or self.loyalty_redemption_cost_center,
+ "against": self.customer,
+ "debit": self.loyalty_amount,
+ "remark": "Loyalty Points redeemed by the customer",
+ },
+ item=self,
+ )
)
def make_pos_gl_entries(self, gl_entries):
if cint(self.is_pos):
- skip_change_gl_entries = not cint(frappe.db.get_single_value('Accounts Settings', 'post_change_gl_entries'))
+ skip_change_gl_entries = not cint(
+ frappe.db.get_single_value("Accounts Settings", "post_change_gl_entries")
+ )
for payment_mode in self.payments:
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
@@ -1148,32 +1340,42 @@ class SalesInvoice(SellingController):
if payment_mode.amount:
# POS, make payment entries
gl_entries.append(
- self.get_gl_dict({
- "account": self.debit_to,
- "party_type": "Customer",
- "party": self.customer,
- "against": payment_mode.account,
- "credit": payment_mode.base_amount,
- "credit_in_account_currency": payment_mode.base_amount \
- if self.party_account_currency==self.company_currency \
+ self.get_gl_dict(
+ {
+ "account": self.debit_to,
+ "party_type": "Customer",
+ "party": self.customer,
+ "against": payment_mode.account,
+ "credit": payment_mode.base_amount,
+ "credit_in_account_currency": payment_mode.base_amount
+ if self.party_account_currency == self.company_currency
else payment_mode.amount,
- "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
- "against_voucher_type": self.doctype,
- "cost_center": self.cost_center
- }, self.party_account_currency, item=self)
+ "against_voucher": self.return_against
+ if cint(self.is_return) and self.return_against
+ else self.name,
+ "against_voucher_type": self.doctype,
+ "cost_center": self.cost_center,
+ },
+ self.party_account_currency,
+ item=self,
+ )
)
payment_mode_account_currency = get_account_currency(payment_mode.account)
gl_entries.append(
- self.get_gl_dict({
- "account": payment_mode.account,
- "against": self.customer,
- "debit": payment_mode.base_amount,
- "debit_in_account_currency": payment_mode.base_amount \
- if payment_mode_account_currency==self.company_currency \
+ self.get_gl_dict(
+ {
+ "account": payment_mode.account,
+ "against": self.customer,
+ "debit": payment_mode.base_amount,
+ "debit_in_account_currency": payment_mode.base_amount
+ if payment_mode_account_currency == self.company_currency
else payment_mode.amount,
- "cost_center": self.cost_center
- }, payment_mode_account_currency, item=self)
+ "cost_center": self.cost_center,
+ },
+ payment_mode_account_currency,
+ item=self,
+ )
)
if not skip_change_gl_entries:
@@ -1183,28 +1385,38 @@ class SalesInvoice(SellingController):
if self.change_amount:
if self.account_for_change_amount:
gl_entries.append(
- self.get_gl_dict({
- "account": self.debit_to,
- "party_type": "Customer",
- "party": self.customer,
- "against": self.account_for_change_amount,
- "debit": flt(self.base_change_amount),
- "debit_in_account_currency": flt(self.base_change_amount) \
- if self.party_account_currency==self.company_currency else flt(self.change_amount),
- "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
- "against_voucher_type": self.doctype,
- "cost_center": self.cost_center,
- "project": self.project
- }, self.party_account_currency, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.debit_to,
+ "party_type": "Customer",
+ "party": self.customer,
+ "against": self.account_for_change_amount,
+ "debit": flt(self.base_change_amount),
+ "debit_in_account_currency": flt(self.base_change_amount)
+ if self.party_account_currency == self.company_currency
+ else flt(self.change_amount),
+ "against_voucher": self.return_against
+ if cint(self.is_return) and self.return_against
+ else self.name,
+ "against_voucher_type": self.doctype,
+ "cost_center": self.cost_center,
+ "project": self.project,
+ },
+ self.party_account_currency,
+ item=self,
+ )
)
gl_entries.append(
- self.get_gl_dict({
- "account": self.account_for_change_amount,
- "against": self.customer,
- "credit": self.base_change_amount,
- "cost_center": self.cost_center
- }, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.account_for_change_amount,
+ "against": self.customer,
+ "credit": self.base_change_amount,
+ "cost_center": self.cost_center,
+ },
+ item=self,
+ )
)
else:
frappe.throw(_("Select change amount account"), title="Mandatory Field")
@@ -1213,61 +1425,86 @@ class SalesInvoice(SellingController):
# write off entries, applicable if only pos
if self.write_off_account and flt(self.write_off_amount, self.precision("write_off_amount")):
write_off_account_currency = get_account_currency(self.write_off_account)
- default_cost_center = frappe.get_cached_value('Company', self.company, 'cost_center')
+ default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
gl_entries.append(
- self.get_gl_dict({
- "account": self.debit_to,
- "party_type": "Customer",
- "party": self.customer,
- "against": self.write_off_account,
- "credit": flt(self.base_write_off_amount, self.precision("base_write_off_amount")),
- "credit_in_account_currency": (flt(self.base_write_off_amount,
- self.precision("base_write_off_amount")) if self.party_account_currency==self.company_currency
- else flt(self.write_off_amount, self.precision("write_off_amount"))),
- "against_voucher": self.return_against if cint(self.is_return) else self.name,
- "against_voucher_type": self.doctype,
- "cost_center": self.cost_center,
- "project": self.project
- }, self.party_account_currency, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.debit_to,
+ "party_type": "Customer",
+ "party": self.customer,
+ "against": self.write_off_account,
+ "credit": flt(self.base_write_off_amount, self.precision("base_write_off_amount")),
+ "credit_in_account_currency": (
+ flt(self.base_write_off_amount, self.precision("base_write_off_amount"))
+ if self.party_account_currency == self.company_currency
+ else flt(self.write_off_amount, self.precision("write_off_amount"))
+ ),
+ "against_voucher": self.return_against if cint(self.is_return) else self.name,
+ "against_voucher_type": self.doctype,
+ "cost_center": self.cost_center,
+ "project": self.project,
+ },
+ self.party_account_currency,
+ item=self,
+ )
)
gl_entries.append(
- self.get_gl_dict({
- "account": self.write_off_account,
- "against": self.customer,
- "debit": flt(self.base_write_off_amount, self.precision("base_write_off_amount")),
- "debit_in_account_currency": (flt(self.base_write_off_amount,
- self.precision("base_write_off_amount")) if write_off_account_currency==self.company_currency
- else flt(self.write_off_amount, self.precision("write_off_amount"))),
- "cost_center": self.cost_center or self.write_off_cost_center or default_cost_center
- }, write_off_account_currency, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.write_off_account,
+ "against": self.customer,
+ "debit": flt(self.base_write_off_amount, self.precision("base_write_off_amount")),
+ "debit_in_account_currency": (
+ flt(self.base_write_off_amount, self.precision("base_write_off_amount"))
+ if write_off_account_currency == self.company_currency
+ else flt(self.write_off_amount, self.precision("write_off_amount"))
+ ),
+ "cost_center": self.cost_center or self.write_off_cost_center or default_cost_center,
+ },
+ write_off_account_currency,
+ item=self,
+ )
)
def make_gle_for_rounding_adjustment(self, gl_entries):
- if flt(self.rounding_adjustment, self.precision("rounding_adjustment")) and self.base_rounding_adjustment \
- and not self.is_internal_transfer():
- round_off_account, round_off_cost_center = \
- get_round_off_account_and_cost_center(self.company)
+ if (
+ flt(self.rounding_adjustment, self.precision("rounding_adjustment"))
+ and self.base_rounding_adjustment
+ and not self.is_internal_transfer()
+ ):
+ round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
+ self.company, "Sales Invoice", self.name
+ )
gl_entries.append(
- self.get_gl_dict({
- "account": round_off_account,
- "against": self.customer,
- "credit_in_account_currency": flt(self.rounding_adjustment,
- self.precision("rounding_adjustment")),
- "credit": flt(self.base_rounding_adjustment,
- self.precision("base_rounding_adjustment")),
- "cost_center": self.cost_center or round_off_cost_center,
- }, item=self))
+ self.get_gl_dict(
+ {
+ "account": round_off_account,
+ "against": self.customer,
+ "credit_in_account_currency": flt(
+ self.rounding_adjustment, self.precision("rounding_adjustment")
+ ),
+ "credit": flt(self.base_rounding_adjustment, self.precision("base_rounding_adjustment")),
+ "cost_center": self.cost_center or round_off_cost_center,
+ },
+ item=self,
+ )
+ )
def update_billing_status_in_dn(self, update_modified=True):
updated_delivery_notes = []
for d in self.get("items"):
if d.dn_detail:
- billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
- where dn_detail=%s and docstatus=1""", d.dn_detail)
+ billed_amt = frappe.db.sql(
+ """select sum(amount) from `tabSales Invoice Item`
+ where dn_detail=%s and docstatus=1""",
+ d.dn_detail,
+ )
billed_amt = billed_amt and billed_amt[0][0] or 0
- frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified)
+ frappe.db.set_value(
+ "Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified
+ )
updated_delivery_notes.append(d.delivery_note)
elif d.so_detail:
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
@@ -1282,7 +1519,7 @@ class SalesInvoice(SellingController):
self.due_date = None
def update_serial_no(self, in_cancel=False):
- """ update Sales Invoice refrence in Serial No """
+ """update Sales Invoice refrence in Serial No"""
invoice = None if (in_cancel or self.is_return) else self.name
if in_cancel and self.is_return:
invoice = self.return_against
@@ -1292,26 +1529,25 @@ class SalesInvoice(SellingController):
continue
for serial_no in get_serial_nos(item.serial_no):
- if serial_no and frappe.db.get_value('Serial No', serial_no, 'item_code') == item.item_code:
- frappe.db.set_value('Serial No', serial_no, 'sales_invoice', invoice)
+ if serial_no and frappe.db.get_value("Serial No", serial_no, "item_code") == item.item_code:
+ frappe.db.set_value("Serial No", serial_no, "sales_invoice", invoice)
def validate_serial_numbers(self):
"""
- validate serial number agains Delivery Note and Sales Invoice
+ validate serial number agains Delivery Note and Sales Invoice
"""
self.set_serial_no_against_delivery_note()
self.validate_serial_against_delivery_note()
def set_serial_no_against_delivery_note(self):
for item in self.items:
- if item.serial_no and item.delivery_note and \
- item.qty != len(get_serial_nos(item.serial_no)):
+ if item.serial_no and item.delivery_note and item.qty != len(get_serial_nos(item.serial_no)):
item.serial_no = get_delivery_note_serial_no(item.item_code, item.qty, item.delivery_note)
def validate_serial_against_delivery_note(self):
"""
- validate if the serial numbers in Sales Invoice Items are same as in
- Delivery Note Item
+ validate if the serial numbers in Sales Invoice Items are same as in
+ Delivery Note Item
"""
for item in self.items:
@@ -1330,14 +1566,18 @@ class SalesInvoice(SellingController):
serial_no_msg = ", ".join(frappe.bold(d) for d in serial_no_diff)
msg = _("Row #{0}: The following Serial Nos are not present in Delivery Note {1}:").format(
- item.idx, dn_link)
+ item.idx, dn_link
+ )
msg += " " + serial_no_msg
frappe.throw(msg=msg, title=_("Serial Nos Mismatch"))
if item.serial_no and cint(item.qty) != len(si_serial_nos):
- frappe.throw(_("Row #{0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
- item.idx, item.qty, item.item_code, len(si_serial_nos)))
+ frappe.throw(
+ _("Row #{0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
+ item.idx, item.qty, item.item_code, len(si_serial_nos)
+ )
+ )
def update_project(self):
if self.project:
@@ -1345,7 +1585,6 @@ class SalesInvoice(SellingController):
project.update_billed_amount()
project.db_update()
-
def verify_payment_amount_is_positive(self):
for entry in self.payments:
if entry.amount < 0:
@@ -1361,63 +1600,90 @@ class SalesInvoice(SellingController):
returned_amount = self.get_returned_amount()
current_amount = flt(self.grand_total) - cint(self.loyalty_amount)
eligible_amount = current_amount - returned_amount
- lp_details = get_loyalty_program_details_with_points(self.customer, company=self.company,
- current_transaction_amount=current_amount, loyalty_program=self.loyalty_program,
- expiry_date=self.posting_date, include_expired_entry=True)
- if lp_details and getdate(lp_details.from_date) <= getdate(self.posting_date) and \
- (not lp_details.to_date or getdate(lp_details.to_date) >= getdate(self.posting_date)):
+ lp_details = get_loyalty_program_details_with_points(
+ self.customer,
+ company=self.company,
+ current_transaction_amount=current_amount,
+ loyalty_program=self.loyalty_program,
+ expiry_date=self.posting_date,
+ include_expired_entry=True,
+ )
+ if (
+ lp_details
+ and getdate(lp_details.from_date) <= getdate(self.posting_date)
+ and (not lp_details.to_date or getdate(lp_details.to_date) >= getdate(self.posting_date))
+ ):
collection_factor = lp_details.collection_factor if lp_details.collection_factor else 1.0
- points_earned = cint(eligible_amount/collection_factor)
+ points_earned = cint(eligible_amount / collection_factor)
- doc = frappe.get_doc({
- "doctype": "Loyalty Point Entry",
- "company": self.company,
- "loyalty_program": lp_details.loyalty_program,
- "loyalty_program_tier": lp_details.tier_name,
- "customer": self.customer,
- "invoice_type": self.doctype,
- "invoice": self.name,
- "loyalty_points": points_earned,
- "purchase_amount": eligible_amount,
- "expiry_date": add_days(self.posting_date, lp_details.expiry_duration),
- "posting_date": self.posting_date
- })
+ doc = frappe.get_doc(
+ {
+ "doctype": "Loyalty Point Entry",
+ "company": self.company,
+ "loyalty_program": lp_details.loyalty_program,
+ "loyalty_program_tier": lp_details.tier_name,
+ "customer": self.customer,
+ "invoice_type": self.doctype,
+ "invoice": self.name,
+ "loyalty_points": points_earned,
+ "purchase_amount": eligible_amount,
+ "expiry_date": add_days(self.posting_date, lp_details.expiry_duration),
+ "posting_date": self.posting_date,
+ }
+ )
doc.flags.ignore_permissions = 1
doc.save()
self.set_loyalty_program_tier()
# valdite the redemption and then delete the loyalty points earned on cancel of the invoice
def delete_loyalty_point_entry(self):
- lp_entry = frappe.db.sql("select name from `tabLoyalty Point Entry` where invoice=%s",
- (self.name), as_dict=1)
+ lp_entry = frappe.db.sql(
+ "select name from `tabLoyalty Point Entry` where invoice=%s", (self.name), as_dict=1
+ )
- if not lp_entry: return
- against_lp_entry = frappe.db.sql('''select name, invoice from `tabLoyalty Point Entry`
- where redeem_against=%s''', (lp_entry[0].name), as_dict=1)
+ if not lp_entry:
+ return
+ against_lp_entry = frappe.db.sql(
+ """select name, invoice from `tabLoyalty Point Entry`
+ where redeem_against=%s""",
+ (lp_entry[0].name),
+ as_dict=1,
+ )
if against_lp_entry:
invoice_list = ", ".join([d.invoice for d in against_lp_entry])
frappe.throw(
- _('''{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}''')
- .format(self.doctype, self.doctype, invoice_list)
+ _(
+ """{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}"""
+ ).format(self.doctype, self.doctype, invoice_list)
)
else:
- frappe.db.sql('''delete from `tabLoyalty Point Entry` where invoice=%s''', (self.name))
+ frappe.db.sql("""delete from `tabLoyalty Point Entry` where invoice=%s""", (self.name))
# Set loyalty program
self.set_loyalty_program_tier()
def set_loyalty_program_tier(self):
- lp_details = get_loyalty_program_details_with_points(self.customer, company=self.company,
- loyalty_program=self.loyalty_program, include_expired_entry=True)
+ lp_details = get_loyalty_program_details_with_points(
+ self.customer,
+ company=self.company,
+ loyalty_program=self.loyalty_program,
+ include_expired_entry=True,
+ )
frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name)
def get_returned_amount(self):
- returned_amount = frappe.db.sql("""
- select sum(grand_total)
- from `tabSales Invoice`
- where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s
- """, self.name)
- return abs(flt(returned_amount[0][0])) if returned_amount else 0
+ from frappe.query_builder.functions import Coalesce, Sum
+
+ doc = frappe.qb.DocType(self.doctype)
+ returned_amount = (
+ frappe.qb.from_(doc)
+ .select(Sum(doc.grand_total))
+ .where(
+ (doc.docstatus == 1) & (doc.is_return == 1) & (Coalesce(doc.return_against, "") == self.name)
+ )
+ ).run()
+
+ return abs(returned_amount[0][0]) if returned_amount[0][0] else 0
# redeem the loyalty points.
def apply_loyalty_points(self):
@@ -1425,7 +1691,10 @@ class SalesInvoice(SellingController):
get_loyalty_point_entries,
get_redemption_details,
)
- loyalty_point_entries = get_loyalty_point_entries(self.customer, self.loyalty_program, self.company, self.posting_date)
+
+ loyalty_point_entries = get_loyalty_point_entries(
+ self.customer, self.loyalty_program, self.company, self.posting_date
+ )
redemption_details = get_redemption_details(self.customer, self.loyalty_program, self.company)
points_to_redeem = self.loyalty_points
@@ -1439,24 +1708,26 @@ class SalesInvoice(SellingController):
redeemed_points = points_to_redeem
else:
redeemed_points = available_points
- doc = frappe.get_doc({
- "doctype": "Loyalty Point Entry",
- "company": self.company,
- "loyalty_program": self.loyalty_program,
- "loyalty_program_tier": lp_entry.loyalty_program_tier,
- "customer": self.customer,
- "invoice_type": self.doctype,
- "invoice": self.name,
- "redeem_against": lp_entry.name,
- "loyalty_points": -1*redeemed_points,
- "purchase_amount": self.grand_total,
- "expiry_date": lp_entry.expiry_date,
- "posting_date": self.posting_date
- })
+ doc = frappe.get_doc(
+ {
+ "doctype": "Loyalty Point Entry",
+ "company": self.company,
+ "loyalty_program": self.loyalty_program,
+ "loyalty_program_tier": lp_entry.loyalty_program_tier,
+ "customer": self.customer,
+ "invoice_type": self.doctype,
+ "invoice": self.name,
+ "redeem_against": lp_entry.name,
+ "loyalty_points": -1 * redeemed_points,
+ "purchase_amount": self.grand_total,
+ "expiry_date": lp_entry.expiry_date,
+ "posting_date": self.posting_date,
+ }
+ )
doc.flags.ignore_permissions = 1
doc.save()
points_to_redeem -= redeemed_points
- if points_to_redeem < 1: # since points_to_redeem is integer
+ if points_to_redeem < 1: # since points_to_redeem is integer
break
# Healthcare
@@ -1464,44 +1735,47 @@ class SalesInvoice(SellingController):
def set_healthcare_services(self, checked_values):
self.set("items", [])
from erpnext.stock.get_item_details import get_item_details
+
for checked_item in checked_values:
item_line = self.append("items", {})
- price_list, price_list_currency = frappe.db.get_values("Price List", {"selling": 1}, ['name', 'currency'])[0]
+ price_list, price_list_currency = frappe.db.get_values(
+ "Price List", {"selling": 1}, ["name", "currency"]
+ )[0]
args = {
- 'doctype': "Sales Invoice",
- 'item_code': checked_item['item'],
- 'company': self.company,
- 'customer': frappe.db.get_value("Patient", self.patient, "customer"),
- 'selling_price_list': price_list,
- 'price_list_currency': price_list_currency,
- 'plc_conversion_rate': 1.0,
- 'conversion_rate': 1.0
+ "doctype": "Sales Invoice",
+ "item_code": checked_item["item"],
+ "company": self.company,
+ "customer": frappe.db.get_value("Patient", self.patient, "customer"),
+ "selling_price_list": price_list,
+ "price_list_currency": price_list_currency,
+ "plc_conversion_rate": 1.0,
+ "conversion_rate": 1.0,
}
item_details = get_item_details(args)
- item_line.item_code = checked_item['item']
+ item_line.item_code = checked_item["item"]
item_line.qty = 1
- if checked_item['qty']:
- item_line.qty = checked_item['qty']
- if checked_item['rate']:
- item_line.rate = checked_item['rate']
+ if checked_item["qty"]:
+ item_line.qty = checked_item["qty"]
+ if checked_item["rate"]:
+ item_line.rate = checked_item["rate"]
else:
item_line.rate = item_details.price_list_rate
item_line.amount = float(item_line.rate) * float(item_line.qty)
- if checked_item['income_account']:
- item_line.income_account = checked_item['income_account']
- if checked_item['dt']:
- item_line.reference_dt = checked_item['dt']
- if checked_item['dn']:
- item_line.reference_dn = checked_item['dn']
- if checked_item['description']:
- item_line.description = checked_item['description']
+ if checked_item["income_account"]:
+ item_line.income_account = checked_item["income_account"]
+ if checked_item["dt"]:
+ item_line.reference_dt = checked_item["dt"]
+ if checked_item["dn"]:
+ item_line.reference_dn = checked_item["dn"]
+ if checked_item["description"]:
+ item_line.description = checked_item["description"]
- self.set_missing_values(for_validate = True)
+ self.set_missing_values(for_validate=True)
def set_status(self, update=False, status=None, update_modified=True):
if self.is_new():
- if self.get('amended_from'):
- self.status = 'Draft'
+ if self.get("amended_from"):
+ self.status = "Draft"
return
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
@@ -1512,7 +1786,7 @@ class SalesInvoice(SellingController):
status = "Cancelled"
elif self.docstatus == 1:
if self.is_internal_transfer():
- self.status = 'Internal Transfer'
+ self.status = "Internal Transfer"
elif is_overdue(self, total):
self.status = "Overdue"
elif 0 < outstanding_amount < total:
@@ -1520,11 +1794,17 @@ class SalesInvoice(SellingController):
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
self.status = "Unpaid"
# Check if outstanding amount is 0 due to credit note issued against invoice
- elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
+ elif (
+ outstanding_amount <= 0
+ and self.is_return == 0
+ and frappe.db.get_value(
+ "Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
+ )
+ ):
self.status = "Credit Note Issued"
elif self.is_return == 1:
self.status = "Return"
- elif outstanding_amount<=0:
+ elif outstanding_amount <= 0:
self.status = "Paid"
else:
self.status = "Submitted"
@@ -1540,34 +1820,29 @@ class SalesInvoice(SellingController):
self.status = "Draft"
if update:
- self.db_set('status', self.status, update_modified = update_modified)
+ self.db_set("status", self.status, update_modified=update_modified)
def get_total_in_party_account_currency(doc):
- total_fieldname = (
- "grand_total"
- if doc.disable_rounded_total
- else "rounded_total"
- )
+ total_fieldname = "grand_total" if doc.disable_rounded_total else "rounded_total"
if doc.party_account_currency != doc.currency:
total_fieldname = "base_" + total_fieldname
return flt(doc.get(total_fieldname), doc.precision(total_fieldname))
+
def is_overdue(doc, total):
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
if outstanding_amount <= 0:
return
today = getdate()
- if doc.get('is_pos') or not doc.get('payment_schedule'):
+ if doc.get("is_pos") or not doc.get("payment_schedule"):
return getdate(doc.due_date) < today
# calculate payable amount till date
payment_amount_field = (
- "base_payment_amount"
- if doc.party_account_currency != doc.currency
- else "payment_amount"
+ "base_payment_amount" if doc.party_account_currency != doc.currency else "payment_amount"
)
payable_amount = sum(
@@ -1582,7 +1857,8 @@ def is_overdue(doc, total):
def get_discounting_status(sales_invoice):
status = None
- invoice_discounting_list = frappe.db.sql("""
+ invoice_discounting_list = frappe.db.sql(
+ """
select status
from `tabInvoice Discounting` id, `tabDiscounted Invoice` d
where
@@ -1590,7 +1866,9 @@ def get_discounting_status(sales_invoice):
and d.sales_invoice=%s
and id.docstatus=1
and status in ('Disbursed', 'Settled')
- """, sales_invoice)
+ """,
+ sales_invoice,
+ )
for d in invoice_discounting_list:
status = d[0]
@@ -1599,6 +1877,7 @@ def get_discounting_status(sales_invoice):
return status
+
def validate_inter_company_party(doctype, party, company, inter_company_reference):
if not party:
return
@@ -1627,10 +1906,19 @@ def validate_inter_company_party(doctype, party, company, inter_company_referenc
frappe.throw(_("Invalid Company for Inter Company Transaction."))
elif frappe.db.get_value(partytype, {"name": party, internal: 1}, "name") == party:
- companies = frappe.get_all("Allowed To Transact With", fields=["company"], filters={"parenttype": partytype, "parent": party})
+ companies = frappe.get_all(
+ "Allowed To Transact With",
+ fields=["company"],
+ filters={"parenttype": partytype, "parent": party},
+ )
companies = [d.company for d in companies]
if not company in companies:
- frappe.throw(_("{0} not allowed to transact with {1}. Please change the Company.").format(partytype, company))
+ frappe.throw(
+ _("{0} not allowed to transact with {1}. Please change the Company.").format(
+ partytype, company
+ )
+ )
+
def update_linked_doc(doctype, name, inter_company_reference):
@@ -1640,8 +1928,8 @@ def update_linked_doc(doctype, name, inter_company_reference):
ref_field = "inter_company_order_reference"
if inter_company_reference:
- frappe.db.set_value(doctype, inter_company_reference,\
- ref_field, name)
+ frappe.db.set_value(doctype, inter_company_reference, ref_field, name)
+
def unlink_inter_company_doc(doctype, name, inter_company_reference):
@@ -1656,48 +1944,57 @@ def unlink_inter_company_doc(doctype, name, inter_company_reference):
frappe.db.set_value(doctype, name, ref_field, "")
frappe.db.set_value(ref_doc, inter_company_reference, ref_field, "")
+
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
+
list_context = get_list_context(context)
- list_context.update({
- 'show_sidebar': True,
- 'show_search': True,
- 'no_breadcrumbs': True,
- 'title': _('Invoices'),
- })
+ list_context.update(
+ {
+ "show_sidebar": True,
+ "show_search": True,
+ "no_breadcrumbs": True,
+ "title": _("Invoices"),
+ }
+ )
return list_context
+
@frappe.whitelist()
def get_bank_cash_account(mode_of_payment, company):
- account = frappe.db.get_value("Mode of Payment Account",
- {"parent": mode_of_payment, "company": company}, "default_account")
+ account = frappe.db.get_value(
+ "Mode of Payment Account", {"parent": mode_of_payment, "company": company}, "default_account"
+ )
if not account:
- frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
- .format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account"))
- return {
- "account": account
- }
+ frappe.throw(
+ _("Please set default Cash or Bank account in Mode of Payment {0}").format(
+ get_link_to_form("Mode of Payment", mode_of_payment)
+ ),
+ title=_("Missing Account"),
+ )
+ return {"account": account}
+
@frappe.whitelist()
def make_maintenance_schedule(source_name, target_doc=None):
- doclist = get_mapped_doc("Sales Invoice", source_name, {
- "Sales Invoice": {
- "doctype": "Maintenance Schedule",
- "validation": {
- "docstatus": ["=", 1]
- }
+ doclist = get_mapped_doc(
+ "Sales Invoice",
+ source_name,
+ {
+ "Sales Invoice": {"doctype": "Maintenance Schedule", "validation": {"docstatus": ["=", 1]}},
+ "Sales Invoice Item": {
+ "doctype": "Maintenance Schedule Item",
+ },
},
- "Sales Invoice Item": {
- "doctype": "Maintenance Schedule Item",
- },
- }, target_doc)
+ target_doc,
+ )
return doclist
+
@frappe.whitelist()
def make_delivery_note(source_name, target_doc=None):
def set_missing_values(source, target):
- target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals")
@@ -1709,82 +2006,104 @@ def make_delivery_note(source_name, target_doc=None):
target_doc.base_amount = target_doc.qty * flt(source_doc.base_rate)
target_doc.amount = target_doc.qty * flt(source_doc.rate)
- doclist = get_mapped_doc("Sales Invoice", source_name, {
- "Sales Invoice": {
- "doctype": "Delivery Note",
- "validation": {
- "docstatus": ["=", 1]
- }
- },
- "Sales Invoice Item": {
- "doctype": "Delivery Note Item",
- "field_map": {
- "name": "si_detail",
- "parent": "against_sales_invoice",
- "serial_no": "serial_no",
- "sales_order": "against_sales_order",
- "so_detail": "so_detail",
- "cost_center": "cost_center"
+ doclist = get_mapped_doc(
+ "Sales Invoice",
+ source_name,
+ {
+ "Sales Invoice": {"doctype": "Delivery Note", "validation": {"docstatus": ["=", 1]}},
+ "Sales Invoice Item": {
+ "doctype": "Delivery Note Item",
+ "field_map": {
+ "name": "si_detail",
+ "parent": "against_sales_invoice",
+ "serial_no": "serial_no",
+ "sales_order": "against_sales_order",
+ "so_detail": "so_detail",
+ "cost_center": "cost_center",
+ },
+ "postprocess": update_item,
+ "condition": lambda doc: doc.delivered_by_supplier != 1,
},
- "postprocess": update_item,
- "condition": lambda doc: doc.delivered_by_supplier!=1
- },
- "Sales Taxes and Charges": {
- "doctype": "Sales Taxes and Charges",
- "add_if_empty": True
- },
- "Sales Team": {
- "doctype": "Sales Team",
- "field_map": {
- "incentives": "incentives"
+ "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
+ "Sales Team": {
+ "doctype": "Sales Team",
+ "field_map": {"incentives": "incentives"},
+ "add_if_empty": True,
},
- "add_if_empty": True
- }
- }, target_doc, set_missing_values)
+ },
+ target_doc,
+ set_missing_values,
+ )
+ doclist.set_onload("ignore_price_list", True)
return doclist
+
@frappe.whitelist()
def make_sales_return(source_name, target_doc=None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
+
return make_return_doc("Sales Invoice", source_name, target_doc)
+
def set_account_for_mode_of_payment(self):
for data in self.payments:
if not data.account:
data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account")
+
def get_inter_company_details(doc, doctype):
if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]:
- parties = frappe.db.get_all("Supplier", fields=["name"], filters={"disabled": 0, "is_internal_supplier": 1, "represents_company": doc.company})
+ parties = frappe.db.get_all(
+ "Supplier",
+ fields=["name"],
+ filters={"disabled": 0, "is_internal_supplier": 1, "represents_company": doc.company},
+ )
company = frappe.get_cached_value("Customer", doc.customer, "represents_company")
if not parties:
- frappe.throw(_('No Supplier found for Inter Company Transactions which represents company {0}').format(frappe.bold(doc.company)))
+ frappe.throw(
+ _("No Supplier found for Inter Company Transactions which represents company {0}").format(
+ frappe.bold(doc.company)
+ )
+ )
party = get_internal_party(parties, "Supplier", doc)
else:
- parties = frappe.db.get_all("Customer", fields=["name"], filters={"disabled": 0, "is_internal_customer": 1, "represents_company": doc.company})
+ parties = frappe.db.get_all(
+ "Customer",
+ fields=["name"],
+ filters={"disabled": 0, "is_internal_customer": 1, "represents_company": doc.company},
+ )
company = frappe.get_cached_value("Supplier", doc.supplier, "represents_company")
if not parties:
- frappe.throw(_('No Customer found for Inter Company Transactions which represents company {0}').format(frappe.bold(doc.company)))
+ frappe.throw(
+ _("No Customer found for Inter Company Transactions which represents company {0}").format(
+ frappe.bold(doc.company)
+ )
+ )
party = get_internal_party(parties, "Customer", doc)
- return {
- "party": party,
- "company": company
- }
+ return {"party": party, "company": company}
+
def get_internal_party(parties, link_doctype, doc):
if len(parties) == 1:
- party = parties[0].name
+ party = parties[0].name
else:
# If more than one Internal Supplier/Customer, get supplier/customer on basis of address
- if doc.get('company_address') or doc.get('shipping_address'):
- party = frappe.db.get_value("Dynamic Link", {"parent": doc.get('company_address') or doc.get('shipping_address'),
- "parenttype": "Address", "link_doctype": link_doctype}, "link_name")
+ if doc.get("company_address") or doc.get("shipping_address"):
+ party = frappe.db.get_value(
+ "Dynamic Link",
+ {
+ "parent": doc.get("company_address") or doc.get("shipping_address"),
+ "parenttype": "Address",
+ "link_doctype": link_doctype,
+ },
+ "link_name",
+ )
if not party:
party = parties[0].name
@@ -1793,11 +2112,18 @@ def get_internal_party(parties, link_doctype, doc):
return party
+
def validate_inter_company_transaction(doc, doctype):
details = get_inter_company_details(doc, doctype)
- price_list = doc.selling_price_list if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"] else doc.buying_price_list
- valid_price_list = frappe.db.get_value("Price List", {"name": price_list, "buying": 1, "selling": 1})
+ price_list = (
+ doc.selling_price_list
+ if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]
+ else doc.buying_price_list
+ )
+ valid_price_list = frappe.db.get_value(
+ "Price List", {"name": price_list, "buying": 1, "selling": 1}
+ )
if not valid_price_list and not doc.is_internal_transfer():
frappe.throw(_("Selected Price List should have buying and selling fields checked."))
@@ -1807,28 +2133,32 @@ def validate_inter_company_transaction(doc, doctype):
frappe.throw(_("No {0} found for Inter Company Transactions.").format(partytype))
company = details.get("company")
- default_currency = frappe.get_cached_value('Company', company, "default_currency")
+ default_currency = frappe.get_cached_value("Company", company, "default_currency")
if default_currency != doc.currency:
- frappe.throw(_("Company currencies of both the companies should match for Inter Company Transactions."))
+ frappe.throw(
+ _("Company currencies of both the companies should match for Inter Company Transactions.")
+ )
return
+
@frappe.whitelist()
def make_inter_company_purchase_invoice(source_name, target_doc=None):
return make_inter_company_transaction("Sales Invoice", source_name, target_doc)
+
def make_inter_company_transaction(doctype, source_name, target_doc=None):
if doctype in ["Sales Invoice", "Sales Order"]:
source_doc = frappe.get_doc(doctype, source_name)
target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order"
target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item"
- source_document_warehouse_field = 'target_warehouse'
- target_document_warehouse_field = 'from_warehouse'
+ source_document_warehouse_field = "target_warehouse"
+ target_document_warehouse_field = "from_warehouse"
else:
source_doc = frappe.get_doc(doctype, source_name)
target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order"
- source_document_warehouse_field = 'from_warehouse'
- target_document_warehouse_field = 'target_warehouse'
+ source_document_warehouse_field = "from_warehouse"
+ target_document_warehouse_field = "target_warehouse"
validate_inter_company_transaction(source_doc, doctype)
details = get_inter_company_details(source_doc, doctype)
@@ -1840,7 +2170,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
def update_details(source_doc, target_doc, source_parent):
target_doc.inter_company_invoice_reference = source_doc.name
if target_doc.doctype in ["Purchase Invoice", "Purchase Order"]:
- currency = frappe.db.get_value('Supplier', details.get('party'), 'default_currency')
+ currency = frappe.db.get_value("Supplier", details.get("party"), "default_currency")
target_doc.company = details.get("company")
target_doc.supplier = details.get("party")
target_doc.is_internal_supplier = 1
@@ -1848,130 +2178,176 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
target_doc.buying_price_list = source_doc.selling_price_list
# Invert Addresses
- update_address(target_doc, 'supplier_address', 'address_display', source_doc.company_address)
- update_address(target_doc, 'shipping_address', 'shipping_address_display', source_doc.customer_address)
+ update_address(target_doc, "supplier_address", "address_display", source_doc.company_address)
+ update_address(
+ target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address
+ )
if currency:
target_doc.currency = currency
- update_taxes(target_doc, party=target_doc.supplier, party_type='Supplier', company=target_doc.company,
- doctype=target_doc.doctype, party_address=target_doc.supplier_address,
- company_address=target_doc.shipping_address)
+ update_taxes(
+ target_doc,
+ party=target_doc.supplier,
+ party_type="Supplier",
+ company=target_doc.company,
+ doctype=target_doc.doctype,
+ party_address=target_doc.supplier_address,
+ company_address=target_doc.shipping_address,
+ )
else:
- currency = frappe.db.get_value('Customer', details.get('party'), 'default_currency')
+ currency = frappe.db.get_value("Customer", details.get("party"), "default_currency")
target_doc.company = details.get("company")
target_doc.customer = details.get("party")
target_doc.selling_price_list = source_doc.buying_price_list
- update_address(target_doc, 'company_address', 'company_address_display', source_doc.supplier_address)
- update_address(target_doc, 'shipping_address_name', 'shipping_address', source_doc.shipping_address)
- update_address(target_doc, 'customer_address', 'address_display', source_doc.shipping_address)
+ update_address(
+ target_doc, "company_address", "company_address_display", source_doc.supplier_address
+ )
+ update_address(
+ target_doc, "shipping_address_name", "shipping_address", source_doc.shipping_address
+ )
+ update_address(target_doc, "customer_address", "address_display", source_doc.shipping_address)
if currency:
target_doc.currency = currency
- update_taxes(target_doc, party=target_doc.customer, party_type='Customer', company=target_doc.company,
- doctype=target_doc.doctype, party_address=target_doc.customer_address,
- company_address=target_doc.company_address, shipping_address_name=target_doc.shipping_address_name)
+ update_taxes(
+ target_doc,
+ party=target_doc.customer,
+ party_type="Customer",
+ company=target_doc.company,
+ doctype=target_doc.doctype,
+ party_address=target_doc.customer_address,
+ company_address=target_doc.company_address,
+ shipping_address_name=target_doc.shipping_address_name,
+ )
item_field_map = {
"doctype": target_doctype + " Item",
- "field_no_map": [
- "income_account",
- "expense_account",
- "cost_center",
- "warehouse"
- ],
+ "field_no_map": ["income_account", "expense_account", "cost_center", "warehouse"],
"field_map": {
- 'rate': 'rate',
- }
+ "rate": "rate",
+ },
}
if doctype in ["Sales Invoice", "Sales Order"]:
- item_field_map["field_map"].update({
- "name": target_detail_field,
- })
+ item_field_map["field_map"].update(
+ {
+ "name": target_detail_field,
+ }
+ )
- if source_doc.get('update_stock'):
- item_field_map["field_map"].update({
- source_document_warehouse_field: target_document_warehouse_field,
- 'batch_no': 'batch_no',
- 'serial_no': 'serial_no'
- })
+ if source_doc.get("update_stock"):
+ item_field_map["field_map"].update(
+ {
+ source_document_warehouse_field: target_document_warehouse_field,
+ "batch_no": "batch_no",
+ "serial_no": "serial_no",
+ }
+ )
- doclist = get_mapped_doc(doctype, source_name, {
- doctype: {
- "doctype": target_doctype,
- "postprocess": update_details,
- "set_target_warehouse": "set_from_warehouse",
- "field_no_map": [
- "taxes_and_charges",
- "set_warehouse",
- "shipping_address"
- ]
+ doclist = get_mapped_doc(
+ doctype,
+ source_name,
+ {
+ doctype: {
+ "doctype": target_doctype,
+ "postprocess": update_details,
+ "set_target_warehouse": "set_from_warehouse",
+ "field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address"],
+ },
+ doctype + " Item": item_field_map,
},
- doctype +" Item": item_field_map
-
- }, target_doc, set_missing_values)
+ target_doc,
+ set_missing_values,
+ )
return doclist
+
def set_purchase_references(doc):
# add internal PO or PR links if any
if doc.is_internal_transfer():
- if doc.doctype == 'Purchase Receipt':
+ if doc.doctype == "Purchase Receipt":
so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference)
if so_item_map:
- pd_item_map, parent_child_map, warehouse_map = \
- get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item')
+ pd_item_map, parent_child_map, warehouse_map = get_pd_details(
+ "Purchase Order Item", so_item_map, "sales_order_item"
+ )
update_pr_items(doc, so_item_map, pd_item_map, parent_child_map, warehouse_map)
- elif doc.doctype == 'Purchase Invoice':
+ elif doc.doctype == "Purchase Invoice":
dn_item_map, so_item_map = get_sales_invoice_details(doc.inter_company_invoice_reference)
# First check for Purchase receipt
if list(dn_item_map.values()):
- pd_item_map, parent_child_map, warehouse_map = \
- get_pd_details('Purchase Receipt Item', dn_item_map, 'delivery_note_item')
+ pd_item_map, parent_child_map, warehouse_map = get_pd_details(
+ "Purchase Receipt Item", dn_item_map, "delivery_note_item"
+ )
- update_pi_items(doc, 'pr_detail', 'purchase_receipt',
- dn_item_map, pd_item_map, parent_child_map, warehouse_map)
+ update_pi_items(
+ doc,
+ "pr_detail",
+ "purchase_receipt",
+ dn_item_map,
+ pd_item_map,
+ parent_child_map,
+ warehouse_map,
+ )
if list(so_item_map.values()):
- pd_item_map, parent_child_map, warehouse_map = \
- get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item')
+ pd_item_map, parent_child_map, warehouse_map = get_pd_details(
+ "Purchase Order Item", so_item_map, "sales_order_item"
+ )
- update_pi_items(doc, 'po_detail', 'purchase_order',
- so_item_map, pd_item_map, parent_child_map, warehouse_map)
+ update_pi_items(
+ doc, "po_detail", "purchase_order", so_item_map, pd_item_map, parent_child_map, warehouse_map
+ )
-def update_pi_items(doc, detail_field, parent_field, sales_item_map,
- purchase_item_map, parent_child_map, warehouse_map):
- for item in doc.get('items'):
+
+def update_pi_items(
+ doc,
+ detail_field,
+ parent_field,
+ sales_item_map,
+ purchase_item_map,
+ parent_child_map,
+ warehouse_map,
+):
+ for item in doc.get("items"):
item.set(detail_field, purchase_item_map.get(sales_item_map.get(item.sales_invoice_item)))
item.set(parent_field, parent_child_map.get(sales_item_map.get(item.sales_invoice_item)))
if doc.update_stock:
item.warehouse = warehouse_map.get(sales_item_map.get(item.sales_invoice_item))
+
def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, warehouse_map):
- for item in doc.get('items'):
+ for item in doc.get("items"):
item.purchase_order_item = purchase_item_map.get(sales_item_map.get(item.delivery_note_item))
item.warehouse = warehouse_map.get(sales_item_map.get(item.delivery_note_item))
item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item))
+
def get_delivery_note_details(internal_reference):
- si_item_details = frappe.get_all('Delivery Note Item', fields=['name', 'so_detail'],
- filters={'parent': internal_reference})
+ si_item_details = frappe.get_all(
+ "Delivery Note Item", fields=["name", "so_detail"], filters={"parent": internal_reference}
+ )
return {d.name: d.so_detail for d in si_item_details if d.so_detail}
+
def get_sales_invoice_details(internal_reference):
dn_item_map = {}
so_item_map = {}
- si_item_details = frappe.get_all('Sales Invoice Item', fields=['name', 'so_detail',
- 'dn_detail'], filters={'parent': internal_reference})
+ si_item_details = frappe.get_all(
+ "Sales Invoice Item",
+ fields=["name", "so_detail", "dn_detail"],
+ filters={"parent": internal_reference},
+ )
for d in si_item_details:
if d.dn_detail:
@@ -1981,13 +2357,17 @@ def get_sales_invoice_details(internal_reference):
return dn_item_map, so_item_map
+
def get_pd_details(doctype, sd_detail_map, sd_detail_field):
pd_item_map = {}
accepted_warehouse_map = {}
parent_child_map = {}
- pd_item_details = frappe.get_all(doctype,
- fields=[sd_detail_field, 'name', 'warehouse', 'parent'], filters={sd_detail_field: ('in', list(sd_detail_map.values()))})
+ pd_item_details = frappe.get_all(
+ doctype,
+ fields=[sd_detail_field, "name", "warehouse", "parent"],
+ filters={sd_detail_field: ("in", list(sd_detail_map.values()))},
+ )
for d in pd_item_details:
pd_item_map.setdefault(d.get(sd_detail_field), d.name)
@@ -1996,16 +2376,33 @@ def get_pd_details(doctype, sd_detail_map, sd_detail_field):
return pd_item_map, parent_child_map, accepted_warehouse_map
-def update_taxes(doc, party=None, party_type=None, company=None, doctype=None, party_address=None,
- company_address=None, shipping_address_name=None, master_doctype=None):
+
+def update_taxes(
+ doc,
+ party=None,
+ party_type=None,
+ company=None,
+ doctype=None,
+ party_address=None,
+ company_address=None,
+ shipping_address_name=None,
+ master_doctype=None,
+):
# Update Party Details
- party_details = get_party_details(party=party, party_type=party_type, company=company,
- doctype=doctype, party_address=party_address, company_address=company_address,
- shipping_address=shipping_address_name)
+ party_details = get_party_details(
+ party=party,
+ party_type=party_type,
+ company=company,
+ doctype=doctype,
+ party_address=party_address,
+ company_address=company_address,
+ shipping_address=shipping_address_name,
+ )
# Update taxes and charges if any
- doc.taxes_and_charges = party_details.get('taxes_and_charges')
- doc.set('taxes', party_details.get('taxes'))
+ doc.taxes_and_charges = party_details.get("taxes_and_charges")
+ doc.set("taxes", party_details.get("taxes"))
+
def update_address(doc, address_field, address_display_field, address_name):
doc.set(address_field, address_name)
@@ -2016,53 +2413,61 @@ def update_address(doc, address_field, address_display_field, address_name):
doc.set(address_display_field, get_address_display(doc.get(address_field)))
+
@frappe.whitelist()
def get_loyalty_programs(customer):
- ''' sets applicable loyalty program to the customer or returns a list of applicable programs '''
+ """sets applicable loyalty program to the customer or returns a list of applicable programs"""
from erpnext.selling.doctype.customer.customer import get_loyalty_programs
- customer = frappe.get_doc('Customer', customer)
- if customer.loyalty_program: return [customer.loyalty_program]
+ customer = frappe.get_doc("Customer", customer)
+ if customer.loyalty_program:
+ return [customer.loyalty_program]
lp_details = get_loyalty_programs(customer)
if len(lp_details) == 1:
- frappe.db.set(customer, 'loyalty_program', lp_details[0])
+ frappe.db.set(customer, "loyalty_program", lp_details[0])
return lp_details
else:
return lp_details
+
def on_doctype_update():
frappe.db.add_index("Sales Invoice", ["customer", "is_return", "return_against"])
+
@frappe.whitelist()
def create_invoice_discounting(source_name, target_doc=None):
invoice = frappe.get_doc("Sales Invoice", source_name)
invoice_discounting = frappe.new_doc("Invoice Discounting")
invoice_discounting.company = invoice.company
- invoice_discounting.append("invoices", {
- "sales_invoice": source_name,
- "customer": invoice.customer,
- "posting_date": invoice.posting_date,
- "outstanding_amount": invoice.outstanding_amount
- })
+ invoice_discounting.append(
+ "invoices",
+ {
+ "sales_invoice": source_name,
+ "customer": invoice.customer,
+ "posting_date": invoice.posting_date,
+ "outstanding_amount": invoice.outstanding_amount,
+ },
+ )
return invoice_discounting
+
def update_multi_mode_option(doc, pos_profile):
def append_payment(payment_mode):
- payment = doc.append('payments', {})
+ payment = doc.append("payments", {})
payment.default = payment_mode.default
payment.mode_of_payment = payment_mode.mop
payment.account = payment_mode.default_account
payment.type = payment_mode.type
- doc.set('payments', [])
+ doc.set("payments", [])
invalid_modes = []
- mode_of_payments = [d.mode_of_payment for d in pos_profile.get('payments')]
+ mode_of_payments = [d.mode_of_payment for d in pos_profile.get("payments")]
mode_of_payments_info = get_mode_of_payments_info(mode_of_payments, doc.company)
- for row in pos_profile.get('payments'):
+ for row in pos_profile.get("payments"):
payment_mode = mode_of_payments_info.get(row.mode_of_payment)
if not payment_mode:
invalid_modes.append(get_link_to_form("Mode of Payment", row.mode_of_payment))
@@ -2078,12 +2483,17 @@ def update_multi_mode_option(doc, pos_profile):
msg = _("Please set default Cash or Bank account in Mode of Payments {}")
frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
+
def get_all_mode_of_payments(doc):
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
select mpa.default_account, mpa.parent, mp.type as type
from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""",
- {'company': doc.company}, as_dict=1)
+ {"company": doc.company},
+ as_dict=1,
+ )
+
def get_mode_of_payments_info(mode_of_payments, company):
data = frappe.db.sql(
@@ -2099,16 +2509,24 @@ def get_mode_of_payments_info(mode_of_payments, company):
mp.name in %s
group by
mp.name
- """, (company, mode_of_payments), as_dict=1)
+ """,
+ (company, mode_of_payments),
+ as_dict=1,
+ )
+
+ return {row.get("mop"): row for row in data}
- return {row.get('mop'): row for row in data}
def get_mode_of_payment_info(mode_of_payment, company):
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
select mpa.default_account, mpa.parent, mp.type as type
from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""",
- (company, mode_of_payment), as_dict=1)
+ (company, mode_of_payment),
+ as_dict=1,
+ )
+
@frappe.whitelist()
def create_dunning(source_name, target_doc=None):
@@ -2118,41 +2536,58 @@ def create_dunning(source_name, target_doc=None):
calculate_interest_and_amount,
get_dunning_letter_text,
)
+
def set_missing_values(source, target):
target.sales_invoice = source_name
target.outstanding_amount = source.outstanding_amount
overdue_days = (getdate(target.posting_date) - getdate(source.due_date)).days
target.overdue_days = overdue_days
- if frappe.db.exists('Dunning Type', {'start_day': [
- '<', overdue_days], 'end_day': ['>=', overdue_days]}):
- dunning_type = frappe.get_doc('Dunning Type', {'start_day': [
- '<', overdue_days], 'end_day': ['>=', overdue_days]})
+ if frappe.db.exists(
+ "Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]}
+ ):
+ dunning_type = frappe.get_doc(
+ "Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]}
+ )
target.dunning_type = dunning_type.name
target.rate_of_interest = dunning_type.rate_of_interest
target.dunning_fee = dunning_type.dunning_fee
- letter_text = get_dunning_letter_text(dunning_type = dunning_type.name, doc = target.as_dict())
+ letter_text = get_dunning_letter_text(dunning_type=dunning_type.name, doc=target.as_dict())
if letter_text:
- target.body_text = letter_text.get('body_text')
- target.closing_text = letter_text.get('closing_text')
- target.language = letter_text.get('language')
- amounts = calculate_interest_and_amount(target.posting_date, target.outstanding_amount,
- target.rate_of_interest, target.dunning_fee, target.overdue_days)
- target.interest_amount = amounts.get('interest_amount')
- target.dunning_amount = amounts.get('dunning_amount')
- target.grand_total = amounts.get('grand_total')
+ target.body_text = letter_text.get("body_text")
+ target.closing_text = letter_text.get("closing_text")
+ target.language = letter_text.get("language")
+ amounts = calculate_interest_and_amount(
+ target.posting_date,
+ target.outstanding_amount,
+ target.rate_of_interest,
+ target.dunning_fee,
+ target.overdue_days,
+ )
+ target.interest_amount = amounts.get("interest_amount")
+ target.dunning_amount = amounts.get("dunning_amount")
+ target.grand_total = amounts.get("grand_total")
- doclist = get_mapped_doc("Sales Invoice", source_name, {
- "Sales Invoice": {
- "doctype": "Dunning",
- }
- }, target_doc, set_missing_values)
+ doclist = get_mapped_doc(
+ "Sales Invoice",
+ source_name,
+ {
+ "Sales Invoice": {
+ "doctype": "Dunning",
+ }
+ },
+ target_doc,
+ set_missing_values,
+ )
return doclist
+
def check_if_return_invoice_linked_with_payment_entry(self):
# If a Return invoice is linked with payment entry along with other invoices,
# the cancellation of the Return causes allocated amount to be greater than paid
- if not frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'):
+ if not frappe.db.get_single_value(
+ "Accounts Settings", "unlink_payment_on_cancellation_of_invoice"
+ ):
return
payment_entries = []
@@ -2161,7 +2596,8 @@ def check_if_return_invoice_linked_with_payment_entry(self):
else:
invoice = self.name
- payment_entries = frappe.db.sql_list("""
+ payment_entries = frappe.db.sql_list(
+ """
SELECT
t1.name
FROM
@@ -2171,7 +2607,9 @@ def check_if_return_invoice_linked_with_payment_entry(self):
and t1.docstatus = 1
and t2.reference_name = %s
and t2.allocated_amount < 0
- """, invoice)
+ """,
+ invoice,
+ )
links_to_pe = []
if payment_entries:
@@ -2180,7 +2618,9 @@ def check_if_return_invoice_linked_with_payment_entry(self):
if len(payment_entry.references) > 1:
links_to_pe.append(payment_entry.name)
if links_to_pe:
- payment_entries_link = [get_link_to_form('Payment Entry', name, label=name) for name in links_to_pe]
+ payment_entries_link = [
+ get_link_to_form("Payment Entry", name, label=name) for name in links_to_pe
+ ]
message = _("Please cancel and amend the Payment Entry")
message += " " + ", ".join(payment_entries_link) + " "
message += _("to unallocate the amount of this Return Invoice before cancelling it.")
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
index 104d4f9b8aa..c0005f78cfd 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
@@ -1,37 +1,34 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'sales_invoice',
- 'non_standard_fieldnames': {
- 'Delivery Note': 'against_sales_invoice',
- 'Journal Entry': 'reference_name',
- 'Payment Entry': 'reference_name',
- 'Payment Request': 'reference_name',
- 'Sales Invoice': 'return_against',
- 'Auto Repeat': 'reference_document',
+ "fieldname": "sales_invoice",
+ "non_standard_fieldnames": {
+ "Delivery Note": "against_sales_invoice",
+ "Journal Entry": "reference_name",
+ "Payment Entry": "reference_name",
+ "Payment Request": "reference_name",
+ "Sales Invoice": "return_against",
+ "Auto Repeat": "reference_document",
},
- 'internal_links': {
- 'Sales Order': ['items', 'sales_order']
+ "internal_links": {
+ "Sales Order": ["items", "sales_order"],
+ "Timesheet": ["timesheets", "time_sheet"],
},
- 'transactions': [
+ "transactions": [
{
- 'label': _('Payment'),
- 'items': ['Payment Entry', 'Payment Request', 'Journal Entry', 'Invoice Discounting', 'Dunning']
+ "label": _("Payment"),
+ "items": [
+ "Payment Entry",
+ "Payment Request",
+ "Journal Entry",
+ "Invoice Discounting",
+ "Dunning",
+ ],
},
- {
- 'label': _('Reference'),
- 'items': ['Timesheet', 'Delivery Note', 'Sales Order']
- },
- {
- 'label': _('Returns'),
- 'items': ['Sales Invoice']
- },
- {
- 'label': _('Subscription'),
- 'items': ['Auto Repeat']
- },
- ]
+ {"label": _("Reference"), "items": ["Timesheet", "Delivery Note", "Sales Order"]},
+ {"label": _("Returns"), "items": ["Sales Invoice"]},
+ {"label": _("Subscription"), "items": ["Auto Repeat"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 69ab1738bc6..bd79f7997d6 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -58,23 +58,25 @@ class TestSalesInvoice(unittest.TestCase):
w2 = frappe.get_doc(w.doctype, w.name)
import time
+
time.sleep(1)
w.save()
import time
+
time.sleep(1)
self.assertRaises(frappe.TimestampMismatchError, w2.save)
def test_sales_invoice_change_naming_series(self):
si = frappe.copy_doc(test_records[2])
si.insert()
- si.naming_series = 'TEST-'
+ si.naming_series = "TEST-"
self.assertRaises(frappe.CannotChangeConstantError, si.save)
si = frappe.copy_doc(test_records[1])
si.insert()
- si.naming_series = 'TEST-'
+ si.naming_series = "TEST-"
self.assertRaises(frappe.CannotChangeConstantError, si.save)
@@ -90,15 +92,21 @@ class TestSalesInvoice(unittest.TestCase):
si.insert()
expected_values = {
- "keys": ["price_list_rate", "discount_percentage", "rate", "amount",
- "base_price_list_rate", "base_rate", "base_amount"],
+ "keys": [
+ "price_list_rate",
+ "discount_percentage",
+ "rate",
+ "amount",
+ "base_price_list_rate",
+ "base_rate",
+ "base_amount",
+ ],
"_Test Item Home Desktop 100": [50, 0, 50, 500, 50, 50, 500],
"_Test Item Home Desktop 200": [150, 0, 150, 750, 150, 150, 750],
}
# check if children are saved
- self.assertEqual(len(si.get("items")),
- len(expected_values)-1)
+ self.assertEqual(len(si.get("items")), len(expected_values) - 1)
# check if item values are calculated
for d in si.get("items"):
@@ -119,7 +127,7 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account S&H Education Cess - _TC": [1.4, 1619.2],
"_Test Account CST - _TC": [32.38, 1651.58],
"_Test Account VAT - _TC": [156.25, 1807.83],
- "_Test Account Discount - _TC": [-180.78, 1627.05]
+ "_Test Account Discount - _TC": [-180.78, 1627.05],
}
for d in si.get("taxes"):
@@ -149,7 +157,7 @@ class TestSalesInvoice(unittest.TestCase):
pe.submit()
unlink_payment_on_cancel_of_invoice(0)
- si = frappe.get_doc('Sales Invoice', si.name)
+ si = frappe.get_doc("Sales Invoice", si.name)
self.assertRaises(frappe.LinkExistsError, si.cancel)
unlink_payment_on_cancel_of_invoice()
@@ -160,25 +168,30 @@ class TestSalesInvoice(unittest.TestCase):
si2 = create_sales_invoice(rate=300)
si3 = create_sales_invoice(qty=-1, rate=300, is_return=1)
-
pe = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Bank - _TC")
- pe.append('references', {
- 'reference_doctype': 'Sales Invoice',
- 'reference_name': si2.name,
- 'total_amount': si2.grand_total,
- 'outstanding_amount': si2.outstanding_amount,
- 'allocated_amount': si2.outstanding_amount
- })
+ pe.append(
+ "references",
+ {
+ "reference_doctype": "Sales Invoice",
+ "reference_name": si2.name,
+ "total_amount": si2.grand_total,
+ "outstanding_amount": si2.outstanding_amount,
+ "allocated_amount": si2.outstanding_amount,
+ },
+ )
- pe.append('references', {
- 'reference_doctype': 'Sales Invoice',
- 'reference_name': si3.name,
- 'total_amount': si3.grand_total,
- 'outstanding_amount': si3.outstanding_amount,
- 'allocated_amount': si3.outstanding_amount
- })
+ pe.append(
+ "references",
+ {
+ "reference_doctype": "Sales Invoice",
+ "reference_name": si3.name,
+ "total_amount": si3.grand_total,
+ "outstanding_amount": si3.outstanding_amount,
+ "allocated_amount": si3.outstanding_amount,
+ },
+ )
- pe.reference_no = 'Test001'
+ pe.reference_no = "Test001"
pe.reference_date = nowdate()
pe.save()
pe.submit()
@@ -189,7 +202,6 @@ class TestSalesInvoice(unittest.TestCase):
si1.load_from_db()
self.assertRaises(PaymentEntryUnlinkError, si1.cancel)
-
def test_sales_invoice_calculation_export_currency(self):
si = frappe.copy_doc(test_records[2])
si.currency = "USD"
@@ -204,14 +216,21 @@ class TestSalesInvoice(unittest.TestCase):
si.insert()
expected_values = {
- "keys": ["price_list_rate", "discount_percentage", "rate", "amount",
- "base_price_list_rate", "base_rate", "base_amount"],
+ "keys": [
+ "price_list_rate",
+ "discount_percentage",
+ "rate",
+ "amount",
+ "base_price_list_rate",
+ "base_rate",
+ "base_amount",
+ ],
"_Test Item Home Desktop 100": [1, 0, 1, 10, 50, 50, 500],
"_Test Item Home Desktop 200": [3, 0, 3, 15, 150, 150, 750],
}
# check if children are saved
- self.assertEqual(len(si.get("items")), len(expected_values)-1)
+ self.assertEqual(len(si.get("items")), len(expected_values) - 1)
# check if item values are calculated
for d in si.get("items"):
@@ -234,7 +253,7 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account S&H Education Cess - _TC": [1.5, 1619.5, 0.03, 32.39],
"_Test Account CST - _TC": [32.5, 1652, 0.65, 33.04],
"_Test Account VAT - _TC": [156.5, 1808.5, 3.13, 36.17],
- "_Test Account Discount - _TC": [-181.0, 1627.5, -3.62, 32.55]
+ "_Test Account Discount - _TC": [-181.0, 1627.5, -3.62, 32.55],
}
for d in si.get("taxes"):
@@ -246,22 +265,28 @@ class TestSalesInvoice(unittest.TestCase):
def test_sales_invoice_with_discount_and_inclusive_tax(self):
si = create_sales_invoice(qty=100, rate=50, do_not_save=True)
- si.append("taxes", {
- "charge_type": "On Net Total",
- "account_head": "_Test Account Service Tax - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Service Tax",
- "rate": 14,
- 'included_in_print_rate': 1
- })
- si.append("taxes", {
- "charge_type": "On Item Quantity",
- "account_head": "_Test Account Education Cess - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "description": "CESS",
- "rate": 5,
- 'included_in_print_rate': 1
- })
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 14,
+ "included_in_print_rate": 1,
+ },
+ )
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Item Quantity",
+ "account_head": "_Test Account Education Cess - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "CESS",
+ "rate": 5,
+ "included_in_print_rate": 1,
+ },
+ )
si.insert()
# with inclusive tax
@@ -273,7 +298,7 @@ class TestSalesInvoice(unittest.TestCase):
# additional discount
si.discount_amount = 100
- si.apply_discount_on = 'Net Total'
+ si.apply_discount_on = "Net Total"
si.payment_schedule = []
si.save()
@@ -286,7 +311,7 @@ class TestSalesInvoice(unittest.TestCase):
# additional discount on grand total
si.discount_amount = 100
- si.apply_discount_on = 'Grand Total'
+ si.apply_discount_on = "Grand Total"
si.payment_schedule = []
si.save()
@@ -298,14 +323,17 @@ class TestSalesInvoice(unittest.TestCase):
def test_sales_invoice_discount_amount(self):
si = frappe.copy_doc(test_records[3])
si.discount_amount = 104.94
- si.append("taxes", {
- "charge_type": "On Previous Row Amount",
- "account_head": "_Test Account Service Tax - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Service Tax",
- "rate": 10,
- "row_id": 8,
- })
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Previous Row Amount",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 10,
+ "row_id": 8,
+ },
+ )
si.insert()
expected_values = [
@@ -321,7 +349,7 @@ class TestSalesInvoice(unittest.TestCase):
"net_rate": 46.54,
"net_amount": 465.37,
"base_net_rate": 46.54,
- "base_net_amount": 465.37
+ "base_net_amount": 465.37,
},
{
"item_code": "_Test Item Home Desktop 200",
@@ -335,12 +363,12 @@ class TestSalesInvoice(unittest.TestCase):
"net_rate": 139.62,
"net_amount": 698.08,
"base_net_rate": 139.62,
- "base_net_amount": 698.08
- }
+ "base_net_amount": 698.08,
+ },
]
# check if children are saved
- self.assertEqual(len(si.get("items")), len(expected_values))
+ self.assertEqual(len(si.get("items")), len(expected_values))
# check if item values are calculated
for i, d in enumerate(si.get("items")):
@@ -362,7 +390,7 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account Customs Duty - _TC": [125, 116.35, 1585.40],
"_Test Account Shipping Charges - _TC": [100, 100, 1685.40],
"_Test Account Discount - _TC": [-180.33, -168.54, 1516.86],
- "_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.01]
+ "_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.01],
}
for d in si.get("taxes"):
@@ -377,38 +405,48 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
si = frappe.copy_doc(test_records[3])
si.discount_amount = 104.94
- si.append("taxes", {
- "doctype": "Sales Taxes and Charges",
- "charge_type": "On Previous Row Amount",
- "account_head": "_Test Account Service Tax - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Service Tax",
- "rate": 10,
- "row_id": 8
- })
+ si.append(
+ "taxes",
+ {
+ "doctype": "Sales Taxes and Charges",
+ "charge_type": "On Previous Row Amount",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 10,
+ "row_id": 8,
+ },
+ )
si.insert()
si.submit()
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
- order by account asc""", si.name, as_dict=1)
+ order by account asc""",
+ si.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
- expected_values = dict((d[0], d) for d in [
- [si.debit_to, 1500, 0.0],
- [test_records[3]["items"][0]["income_account"], 0.0, 1163.45],
- [test_records[3]["taxes"][0]["account_head"], 0.0, 130.31],
- [test_records[3]["taxes"][1]["account_head"], 0.0, 2.61],
- [test_records[3]["taxes"][2]["account_head"], 0.0, 1.30],
- [test_records[3]["taxes"][3]["account_head"], 0.0, 25.95],
- [test_records[3]["taxes"][4]["account_head"], 0.0, 145.43],
- [test_records[3]["taxes"][5]["account_head"], 0.0, 116.35],
- [test_records[3]["taxes"][6]["account_head"], 0.0, 100],
- [test_records[3]["taxes"][7]["account_head"], 168.54, 0.0],
- ["_Test Account Service Tax - _TC", 16.85, 0.0],
- ["Round Off - _TC", 0.01, 0.0]
- ])
+ expected_values = dict(
+ (d[0], d)
+ for d in [
+ [si.debit_to, 1500, 0.0],
+ [test_records[3]["items"][0]["income_account"], 0.0, 1163.45],
+ [test_records[3]["taxes"][0]["account_head"], 0.0, 130.31],
+ [test_records[3]["taxes"][1]["account_head"], 0.0, 2.61],
+ [test_records[3]["taxes"][2]["account_head"], 0.0, 1.30],
+ [test_records[3]["taxes"][3]["account_head"], 0.0, 25.95],
+ [test_records[3]["taxes"][4]["account_head"], 0.0, 145.43],
+ [test_records[3]["taxes"][5]["account_head"], 0.0, 116.35],
+ [test_records[3]["taxes"][6]["account_head"], 0.0, 100],
+ [test_records[3]["taxes"][7]["account_head"], 168.54, 0.0],
+ ["_Test Account Service Tax - _TC", 16.85, 0.0],
+ ["Round Off - _TC", 0.01, 0.0],
+ ]
+ )
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
@@ -418,8 +456,11 @@ class TestSalesInvoice(unittest.TestCase):
# cancel
si.cancel()
- gle = frappe.db.sql("""select * from `tabGL Entry`
- where voucher_type='Sales Invoice' and voucher_no=%s""", si.name)
+ gle = frappe.db.sql(
+ """select * from `tabGL Entry`
+ where voucher_type='Sales Invoice' and voucher_no=%s""",
+ si.name,
+ )
self.assertTrue(gle)
@@ -431,14 +472,17 @@ class TestSalesInvoice(unittest.TestCase):
item_row_copy.qty = qty
si.append("items", item_row_copy)
- si.append("taxes", {
- "account_head": "_Test Account VAT - _TC",
- "charge_type": "On Net Total",
- "cost_center": "_Test Cost Center - _TC",
- "description": "VAT",
- "doctype": "Sales Taxes and Charges",
- "rate": 19
- })
+ si.append(
+ "taxes",
+ {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 19,
+ },
+ )
si.insert()
self.assertEqual(si.net_total, 4600)
@@ -453,10 +497,10 @@ class TestSalesInvoice(unittest.TestCase):
item_row = si.get("items")[0]
add_items = [
- (54, '_Test Account Excise Duty @ 12 - _TC'),
- (288, '_Test Account Excise Duty @ 15 - _TC'),
- (144, '_Test Account Excise Duty @ 20 - _TC'),
- (430, '_Test Item Tax Template 1 - _TC')
+ (54, "_Test Account Excise Duty @ 12 - _TC"),
+ (288, "_Test Account Excise Duty @ 15 - _TC"),
+ (144, "_Test Account Excise Duty @ 20 - _TC"),
+ (430, "_Test Item Tax Template 1 - _TC"),
]
for qty, item_tax_template in add_items:
item_row_copy = copy.deepcopy(item_row)
@@ -464,30 +508,39 @@ class TestSalesInvoice(unittest.TestCase):
item_row_copy.item_tax_template = item_tax_template
si.append("items", item_row_copy)
- si.append("taxes", {
- "account_head": "_Test Account Excise Duty - _TC",
- "charge_type": "On Net Total",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Excise Duty",
- "doctype": "Sales Taxes and Charges",
- "rate": 11
- })
- si.append("taxes", {
- "account_head": "_Test Account Education Cess - _TC",
- "charge_type": "On Net Total",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Education Cess",
- "doctype": "Sales Taxes and Charges",
- "rate": 0
- })
- si.append("taxes", {
- "account_head": "_Test Account S&H Education Cess - _TC",
- "charge_type": "On Net Total",
- "cost_center": "_Test Cost Center - _TC",
- "description": "S&H Education Cess",
- "doctype": "Sales Taxes and Charges",
- "rate": 3
- })
+ si.append(
+ "taxes",
+ {
+ "account_head": "_Test Account Excise Duty - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Excise Duty",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 11,
+ },
+ )
+ si.append(
+ "taxes",
+ {
+ "account_head": "_Test Account Education Cess - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Education Cess",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 0,
+ },
+ )
+ si.append(
+ "taxes",
+ {
+ "account_head": "_Test Account S&H Education Cess - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "S&H Education Cess",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 3,
+ },
+ )
si.insert()
self.assertEqual(si.net_total, 4600)
@@ -517,14 +570,17 @@ class TestSalesInvoice(unittest.TestCase):
si.apply_discount_on = "Net Total"
si.discount_amount = 75.0
- si.append("taxes", {
- "account_head": "_Test Account VAT - _TC",
- "charge_type": "On Net Total",
- "cost_center": "_Test Cost Center - _TC",
- "description": "VAT",
- "doctype": "Sales Taxes and Charges",
- "rate": 24
- })
+ si.append(
+ "taxes",
+ {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 24,
+ },
+ )
si.insert()
self.assertEqual(si.total, 975)
@@ -538,7 +594,7 @@ class TestSalesInvoice(unittest.TestCase):
def test_inclusive_rate_validations(self):
si = frappe.copy_doc(test_records[2])
for i, tax in enumerate(si.get("taxes")):
- tax.idx = i+1
+ tax.idx = i + 1
si.get("items")[0].price_list_rate = 62.5
si.get("items")[0].price_list_rate = 191
@@ -558,14 +614,43 @@ class TestSalesInvoice(unittest.TestCase):
si.insert()
expected_values = {
- "keys": ["price_list_rate", "discount_percentage", "rate", "amount",
- "base_price_list_rate", "base_rate", "base_amount", "net_rate", "net_amount"],
- "_Test Item Home Desktop 100": [62.5, 0, 62.5, 625.0, 62.5, 62.5, 625.0, 50, 499.97600115194473],
- "_Test Item Home Desktop 200": [190.66, 0, 190.66, 953.3, 190.66, 190.66, 953.3, 150, 749.9968530500239],
+ "keys": [
+ "price_list_rate",
+ "discount_percentage",
+ "rate",
+ "amount",
+ "base_price_list_rate",
+ "base_rate",
+ "base_amount",
+ "net_rate",
+ "net_amount",
+ ],
+ "_Test Item Home Desktop 100": [
+ 62.5,
+ 0,
+ 62.5,
+ 625.0,
+ 62.5,
+ 62.5,
+ 625.0,
+ 50,
+ 499.97600115194473,
+ ],
+ "_Test Item Home Desktop 200": [
+ 190.66,
+ 0,
+ 190.66,
+ 953.3,
+ 190.66,
+ 190.66,
+ 953.3,
+ 150,
+ 749.9968530500239,
+ ],
}
# check if children are saved
- self.assertEqual(len(si.get("items")), len(expected_values)-1)
+ self.assertEqual(len(si.get("items")), len(expected_values) - 1)
# check if item values are calculated
for d in si.get("items"):
@@ -586,7 +671,7 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account VAT - _TC": [156.25, 1578.30],
"_Test Account Customs Duty - _TC": [125, 1703.30],
"_Test Account Shipping Charges - _TC": [100, 1803.30],
- "_Test Account Discount - _TC": [-180.33, 1622.97]
+ "_Test Account Discount - _TC": [-180.33, 1622.97],
}
for d in si.get("taxes"):
@@ -624,7 +709,7 @@ class TestSalesInvoice(unittest.TestCase):
"net_rate": 40,
"net_amount": 399.9808009215558,
"base_net_rate": 2000,
- "base_net_amount": 19999
+ "base_net_amount": 19999,
},
{
"item_code": "_Test Item Home Desktop 200",
@@ -638,8 +723,8 @@ class TestSalesInvoice(unittest.TestCase):
"net_rate": 118.01,
"net_amount": 590.0531205155963,
"base_net_rate": 5900.5,
- "base_net_amount": 29502.5
- }
+ "base_net_amount": 29502.5,
+ },
]
# check if children are saved
@@ -664,8 +749,8 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account CST - _TC": [1104, 56312.0, 22.08, 1126.24],
"_Test Account VAT - _TC": [6187.5, 62499.5, 123.75, 1249.99],
"_Test Account Customs Duty - _TC": [4950.0, 67449.5, 99.0, 1348.99],
- "_Test Account Shipping Charges - _TC": [ 100, 67549.5, 2, 1350.99],
- "_Test Account Discount - _TC": [ -6755, 60794.5, -135.10, 1215.89]
+ "_Test Account Shipping Charges - _TC": [100, 67549.5, 2, 1350.99],
+ "_Test Account Discount - _TC": [-6755, 60794.5, -135.10, 1215.89],
}
for d in si.get("taxes"):
@@ -677,7 +762,6 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.rounding_adjustment, 0.01)
self.assertEqual(si.base_rounding_adjustment, 0.50)
-
def test_outstanding(self):
w = self.make()
self.assertEqual(w.outstanding_amount, w.base_rounded_total)
@@ -697,11 +781,11 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(frappe.db.get_value("Sales Invoice", w.name, "outstanding_amount"), 162.0)
- link_data = get_dynamic_link_map().get('Sales Invoice', [])
+ link_data = get_dynamic_link_map().get("Sales Invoice", [])
link_doctypes = [d.parent for d in link_data]
# test case for dynamic link order
- self.assertTrue(link_doctypes.index('GL Entry') > link_doctypes.index('Journal Entry Account'))
+ self.assertTrue(link_doctypes.index("GL Entry") > link_doctypes.index("Journal Entry Account"))
jv.cancel()
self.assertEqual(frappe.db.get_value("Sales Invoice", w.name, "outstanding_amount"), 562.0)
@@ -711,18 +795,25 @@ class TestSalesInvoice(unittest.TestCase):
si.insert()
si.submit()
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
- order by account asc""", si.name, as_dict=1)
+ order by account asc""",
+ si.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
- expected_values = dict((d[0], d) for d in [
- [si.debit_to, 630.0, 0.0],
- [test_records[1]["items"][0]["income_account"], 0.0, 500.0],
- [test_records[1]["taxes"][0]["account_head"], 0.0, 80.0],
- [test_records[1]["taxes"][1]["account_head"], 0.0, 50.0],
- ])
+ expected_values = dict(
+ (d[0], d)
+ for d in [
+ [si.debit_to, 630.0, 0.0],
+ [test_records[1]["items"][0]["income_account"], 0.0, 500.0],
+ [test_records[1]["taxes"][0]["account_head"], 0.0, 80.0],
+ [test_records[1]["taxes"][1]["account_head"], 0.0, 50.0],
+ ]
+ )
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][0], gle.account)
@@ -732,25 +823,49 @@ class TestSalesInvoice(unittest.TestCase):
# cancel
si.cancel()
- gle = frappe.db.sql("""select * from `tabGL Entry`
- where voucher_type='Sales Invoice' and voucher_no=%s""", si.name)
+ gle = frappe.db.sql(
+ """select * from `tabGL Entry`
+ where voucher_type='Sales Invoice' and voucher_no=%s""",
+ si.name,
+ )
self.assertTrue(gle)
def test_pos_gl_entry_with_perpetual_inventory(self):
- make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
- expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
+ make_pos_profile(
+ company="_Test Company with perpetual inventory",
+ income_account="Sales - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ write_off_account="_Test Write Off - TCP1",
+ )
- pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1")
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ item_code="_Test FG Item",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ )
- pos = create_sales_invoice(company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1",
- income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1", do_not_save=True)
+ pos = create_sales_invoice(
+ company="_Test Company with perpetual inventory",
+ debit_to="Debtors - TCP1",
+ item_code="_Test FG Item",
+ warehouse="Stores - TCP1",
+ income_account="Sales - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1",
+ do_not_save=True,
+ )
pos.is_pos = 1
pos.update_stock = 1
- pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
- pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 50})
+ pos.append(
+ "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50}
+ )
+ pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 50})
taxes = get_taxes_and_charges()
pos.taxes = []
@@ -770,20 +885,19 @@ class TestSalesInvoice(unittest.TestCase):
pos_profile = make_pos_profile()
pos_profile.payments = []
- pos_profile.append('payments', {
- 'default': 1,
- 'mode_of_payment': 'Cash'
- })
+ pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"})
pos_profile.save()
- pos = create_sales_invoice(qty = 10, do_not_save=True)
+ pos = create_sales_invoice(qty=10, do_not_save=True)
pos.is_pos = 1
pos.pos_profile = pos_profile.name
- pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500})
- pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500})
+ pos.append(
+ "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500}
+ )
+ pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500})
pos.insert()
pos.submit()
@@ -792,46 +906,123 @@ class TestSalesInvoice(unittest.TestCase):
pos_return.insert()
pos_return.submit()
- self.assertEqual(pos_return.get('payments')[0].amount, -1000)
+ self.assertEqual(pos_return.get("payments")[0].amount, -1000)
def test_pos_change_amount(self):
- make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
- expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
+ make_pos_profile(
+ company="_Test Company with perpetual inventory",
+ income_account="Sales - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ write_off_account="_Test Write Off - TCP1",
+ )
- make_purchase_receipt(company= "_Test Company with perpetual inventory",
- item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
+ make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ item_code="_Test FG Item",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ )
- pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
- debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1",
- income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1",
- cost_center = "Main - TCP1", do_not_save=True)
+ pos = create_sales_invoice(
+ company="_Test Company with perpetual inventory",
+ debit_to="Debtors - TCP1",
+ item_code="_Test FG Item",
+ warehouse="Stores - TCP1",
+ income_account="Sales - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1",
+ do_not_save=True,
+ )
pos.is_pos = 1
pos.update_stock = 1
- pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
- pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 60})
+ pos.append(
+ "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50}
+ )
+ pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 60})
- pos.change_amount = 5.0
+ pos.write_off_outstanding_amount_automatically = 1
pos.insert()
pos.submit()
self.assertEqual(pos.grand_total, 100.0)
- self.assertEqual(pos.write_off_amount, -5)
+ self.assertEqual(pos.write_off_amount, 0)
+
+ def test_auto_write_off_amount(self):
+ make_pos_profile(
+ company="_Test Company with perpetual inventory",
+ income_account="Sales - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ write_off_account="_Test Write Off - TCP1",
+ )
+
+ make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ item_code="_Test FG Item",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ )
+
+ pos = create_sales_invoice(
+ company="_Test Company with perpetual inventory",
+ debit_to="Debtors - TCP1",
+ item_code="_Test FG Item",
+ warehouse="Stores - TCP1",
+ income_account="Sales - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1",
+ do_not_save=True,
+ )
+
+ pos.is_pos = 1
+ pos.update_stock = 1
+
+ pos.append(
+ "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50}
+ )
+ pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 40})
+
+ pos.write_off_outstanding_amount_automatically = 1
+ pos.insert()
+ pos.submit()
+
+ self.assertEqual(pos.grand_total, 100.0)
+ self.assertEqual(pos.write_off_amount, 10)
def test_pos_with_no_gl_entry_for_change_amount(self):
- frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 0)
+ frappe.db.set_value("Accounts Settings", None, "post_change_gl_entries", 0)
- make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
- expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
+ make_pos_profile(
+ company="_Test Company with perpetual inventory",
+ income_account="Sales - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ write_off_account="_Test Write Off - TCP1",
+ )
- make_purchase_receipt(company= "_Test Company with perpetual inventory",
- item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
+ make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ item_code="_Test FG Item",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ )
- pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
- debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1",
- income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1",
- cost_center = "Main - TCP1", do_not_save=True)
+ pos = create_sales_invoice(
+ company="_Test Company with perpetual inventory",
+ debit_to="Debtors - TCP1",
+ item_code="_Test FG Item",
+ warehouse="Stores - TCP1",
+ income_account="Sales - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1",
+ do_not_save=True,
+ )
pos.is_pos = 1
pos.update_stock = 1
@@ -841,8 +1032,10 @@ class TestSalesInvoice(unittest.TestCase):
for tax in taxes:
pos.append("taxes", tax)
- pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
- pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 60})
+ pos.append(
+ "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50}
+ )
+ pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 60})
pos.insert()
pos.submit()
@@ -852,40 +1045,50 @@ class TestSalesInvoice(unittest.TestCase):
self.validate_pos_gl_entry(pos, pos, 60, validate_without_change_gle=True)
- frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 1)
+ frappe.db.set_value("Accounts Settings", None, "post_change_gl_entries", 1)
def validate_pos_gl_entry(self, si, pos, cash_amount, validate_without_change_gle=False):
if validate_without_change_gle:
cash_amount -= pos.change_amount
# check stock ledger entries
- sle = frappe.db.sql("""select * from `tabStock Ledger Entry`
+ sle = frappe.db.sql(
+ """select * from `tabStock Ledger Entry`
where voucher_type = 'Sales Invoice' and voucher_no = %s""",
- si.name, as_dict=1)[0]
+ si.name,
+ as_dict=1,
+ )[0]
self.assertTrue(sle)
- self.assertEqual([sle.item_code, sle.warehouse, sle.actual_qty],
- ['_Test FG Item', 'Stores - TCP1', -1.0])
+ self.assertEqual(
+ [sle.item_code, sle.warehouse, sle.actual_qty], ["_Test FG Item", "Stores - TCP1", -1.0]
+ )
# check gl entries
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
- order by account asc, debit asc, credit asc""", si.name, as_dict=1)
+ order by account asc, debit asc, credit asc""",
+ si.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
- stock_in_hand = get_inventory_account('_Test Company with perpetual inventory')
- expected_gl_entries = sorted([
- [si.debit_to, 100.0, 0.0],
- [pos.items[0].income_account, 0.0, 89.09],
- ['Round Off - TCP1', 0.0, 0.01],
- [pos.taxes[0].account_head, 0.0, 10.69],
- [pos.taxes[1].account_head, 0.0, 0.21],
- [stock_in_hand, 0.0, abs(sle.stock_value_difference)],
- [pos.items[0].expense_account, abs(sle.stock_value_difference), 0.0],
- [si.debit_to, 0.0, 50.0],
- [si.debit_to, 0.0, cash_amount],
- ["_Test Bank - TCP1", 50, 0.0],
- ["Cash - TCP1", cash_amount, 0.0]
- ])
+ stock_in_hand = get_inventory_account("_Test Company with perpetual inventory")
+ expected_gl_entries = sorted(
+ [
+ [si.debit_to, 100.0, 0.0],
+ [pos.items[0].income_account, 0.0, 89.09],
+ ["Round Off - TCP1", 0.0, 0.01],
+ [pos.taxes[0].account_head, 0.0, 10.69],
+ [pos.taxes[1].account_head, 0.0, 0.21],
+ [stock_in_hand, 0.0, abs(sle.stock_value_difference)],
+ [pos.items[0].expense_account, abs(sle.stock_value_difference), 0.0],
+ [si.debit_to, 0.0, 50.0],
+ [si.debit_to, 0.0, cash_amount],
+ ["_Test Bank - TCP1", 50, 0.0],
+ ["Cash - TCP1", cash_amount, 0.0],
+ ]
+ )
for i, gle in enumerate(sorted(gl_entries, key=lambda gle: gle.account)):
self.assertEqual(expected_gl_entries[i][0], gle.account)
@@ -893,8 +1096,11 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(expected_gl_entries[i][2], gle.credit)
si.cancel()
- gle = frappe.db.sql("""select * from `tabGL Entry`
- where voucher_type='Sales Invoice' and voucher_no=%s""", si.name)
+ gle = frappe.db.sql(
+ """select * from `tabGL Entry`
+ where voucher_type='Sales Invoice' and voucher_no=%s""",
+ si.name,
+ )
self.assertTrue(gle)
@@ -914,21 +1120,29 @@ class TestSalesInvoice(unittest.TestCase):
self.assertRaises(frappe.ValidationError, si.submit)
def test_sales_invoice_gl_entry_with_perpetual_inventory_no_item_code(self):
- si = create_sales_invoice(company="_Test Company with perpetual inventory", debit_to = "Debtors - TCP1",
- income_account="Sales - TCP1", cost_center = "Main - TCP1", do_not_save=True)
+ si = create_sales_invoice(
+ company="_Test Company with perpetual inventory",
+ debit_to="Debtors - TCP1",
+ income_account="Sales - TCP1",
+ cost_center="Main - TCP1",
+ do_not_save=True,
+ )
si.get("items")[0].item_code = None
si.insert()
si.submit()
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
- order by account asc""", si.name, as_dict=1)
+ order by account asc""",
+ si.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
- expected_values = dict((d[0], d) for d in [
- ["Debtors - TCP1", 100.0, 0.0],
- ["Sales - TCP1", 0.0, 100.0]
- ])
+ expected_values = dict(
+ (d[0], d) for d in [["Debtors - TCP1", 100.0, 0.0], ["Sales - TCP1", 0.0, 100.0]]
+ )
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
@@ -937,25 +1151,32 @@ class TestSalesInvoice(unittest.TestCase):
def test_sales_invoice_gl_entry_with_perpetual_inventory_non_stock_item(self):
si = create_sales_invoice(item="_Test Non Stock Item")
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
- order by account asc""", si.name, as_dict=1)
+ order by account asc""",
+ si.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
- expected_values = dict((d[0], d) for d in [
- [si.debit_to, 100.0, 0.0],
- [test_records[1]["items"][0]["income_account"], 0.0, 100.0]
- ])
+ expected_values = dict(
+ (d[0], d)
+ for d in [
+ [si.debit_to, 100.0, 0.0],
+ [test_records[1]["items"][0]["income_account"], 0.0, 100.0],
+ ]
+ )
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
-
def _insert_purchase_receipt(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
test_records as pr_test_records,
)
+
pr = frappe.copy_doc(pr_test_records[0])
pr.naming_series = "_T-Purchase Receipt-"
pr.insert()
@@ -965,6 +1186,7 @@ class TestSalesInvoice(unittest.TestCase):
from erpnext.stock.doctype.delivery_note.test_delivery_note import (
test_records as dn_test_records,
)
+
dn = frappe.copy_doc(dn_test_records[0])
dn.naming_series = "_T-Delivery Note-"
dn.insert()
@@ -982,24 +1204,37 @@ class TestSalesInvoice(unittest.TestCase):
si = frappe.copy_doc(test_records[0])
si.allocate_advances_automatically = 0
- si.append("advances", {
- "doctype": "Sales Invoice Advance",
- "reference_type": "Journal Entry",
- "reference_name": jv.name,
- "reference_row": jv.get("accounts")[0].name,
- "advance_amount": 400,
- "allocated_amount": 300,
- "remarks": jv.remark
- })
+ si.append(
+ "advances",
+ {
+ "doctype": "Sales Invoice Advance",
+ "reference_type": "Journal Entry",
+ "reference_name": jv.name,
+ "reference_row": jv.get("accounts")[0].name,
+ "advance_amount": 400,
+ "allocated_amount": 300,
+ "remarks": jv.remark,
+ },
+ )
si.insert()
si.submit()
si.load_from_db()
- self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account`
- where reference_name=%s""", si.name))
+ self.assertTrue(
+ frappe.db.sql(
+ """select name from `tabJournal Entry Account`
+ where reference_name=%s""",
+ si.name,
+ )
+ )
- self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account`
- where reference_name=%s and credit_in_account_currency=300""", si.name))
+ self.assertTrue(
+ frappe.db.sql(
+ """select name from `tabJournal Entry Account`
+ where reference_name=%s and credit_in_account_currency=300""",
+ si.name,
+ )
+ )
self.assertEqual(si.outstanding_amount, 262.0)
@@ -1021,29 +1256,34 @@ class TestSalesInvoice(unittest.TestCase):
si.submit()
self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse"))
- self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0],
- "delivery_document_no"), si.name)
+ self.assertEqual(
+ frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"), si.name
+ )
return si
def test_serialized_cancel(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
si = self.test_serialized()
si.cancel()
serial_nos = get_serial_nos(si.get("items")[0].serial_no)
- self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC")
- self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0],
- "delivery_document_no"))
+ self.assertEqual(
+ frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC"
+ )
+ self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"))
self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "sales_invoice"))
def test_serialize_status(self):
- serial_no = frappe.get_doc({
- "doctype": "Serial No",
- "item_code": "_Test Serialized Item With Series",
- "serial_no": make_autoname("SR", "Serial No")
- })
+ serial_no = frappe.get_doc(
+ {
+ "doctype": "Serial No",
+ "item_code": "_Test Serialized Item With Series",
+ "serial_no": make_autoname("SR", "Serial No"),
+ }
+ )
serial_no.save()
si = frappe.copy_doc(test_records[0])
@@ -1057,8 +1297,8 @@ class TestSalesInvoice(unittest.TestCase):
def test_serial_numbers_against_delivery_note(self):
"""
- check if the sales invoice item serial numbers and the delivery note items
- serial numbers are same
+ check if the sales invoice item serial numbers and the delivery note items
+ serial numbers are same
"""
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -1078,40 +1318,84 @@ class TestSalesInvoice(unittest.TestCase):
def test_return_sales_invoice(self):
make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=50, basic_rate=100)
- actual_qty_0 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1")
+ actual_qty_0 = get_qty_after_transaction(item_code="_Test Item", warehouse="Stores - TCP1")
- si = create_sales_invoice(qty = 5, rate=500, update_stock=1, company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1")
+ si = create_sales_invoice(
+ qty=5,
+ rate=500,
+ update_stock=1,
+ company="_Test Company with perpetual inventory",
+ debit_to="Debtors - TCP1",
+ item_code="_Test Item",
+ warehouse="Stores - TCP1",
+ income_account="Sales - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1",
+ )
-
- actual_qty_1 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1")
+ actual_qty_1 = get_qty_after_transaction(item_code="_Test Item", warehouse="Stores - TCP1")
self.assertEqual(actual_qty_0 - 5, actual_qty_1)
# outgoing_rate
- outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Sales Invoice",
- "voucher_no": si.name}, "stock_value_difference") / 5
+ outgoing_rate = (
+ frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Sales Invoice", "voucher_no": si.name},
+ "stock_value_difference",
+ )
+ / 5
+ )
# return entry
- si1 = create_sales_invoice(is_return=1, return_against=si.name, qty=-2, rate=500, update_stock=1, company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1")
+ si1 = create_sales_invoice(
+ is_return=1,
+ return_against=si.name,
+ qty=-2,
+ rate=500,
+ update_stock=1,
+ company="_Test Company with perpetual inventory",
+ debit_to="Debtors - TCP1",
+ item_code="_Test Item",
+ warehouse="Stores - TCP1",
+ income_account="Sales - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1",
+ )
- actual_qty_2 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1")
+ actual_qty_2 = get_qty_after_transaction(item_code="_Test Item", warehouse="Stores - TCP1")
self.assertEqual(actual_qty_1 + 2, actual_qty_2)
- incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ incoming_rate, stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
{"voucher_type": "Sales Invoice", "voucher_no": si1.name},
- ["incoming_rate", "stock_value_difference"])
+ ["incoming_rate", "stock_value_difference"],
+ )
self.assertEqual(flt(incoming_rate, 3), abs(flt(outgoing_rate, 3)))
- stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory', si1.items[0].warehouse)
+ stock_in_hand_account = get_inventory_account(
+ "_Test Company with perpetual inventory", si1.items[0].warehouse
+ )
# Check gl entry
- gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Sales Invoice",
- "voucher_no": si1.name, "account": stock_in_hand_account}, "debit")
+ gle_warehouse_amount = frappe.db.get_value(
+ "GL Entry",
+ {"voucher_type": "Sales Invoice", "voucher_no": si1.name, "account": stock_in_hand_account},
+ "debit",
+ )
self.assertEqual(gle_warehouse_amount, stock_value_difference)
- party_credited = frappe.db.get_value("GL Entry", {"voucher_type": "Sales Invoice",
- "voucher_no": si1.name, "account": "Debtors - TCP1", "party": "_Test Customer"}, "credit")
+ party_credited = frappe.db.get_value(
+ "GL Entry",
+ {
+ "voucher_type": "Sales Invoice",
+ "voucher_no": si1.name,
+ "account": "Debtors - TCP1",
+ "party": "_Test Customer",
+ },
+ "credit",
+ )
self.assertEqual(party_credited, 1000)
@@ -1122,51 +1406,87 @@ class TestSalesInvoice(unittest.TestCase):
def test_gle_made_when_asset_is_returned(self):
create_asset_data()
- pi = frappe.new_doc('Purchase Invoice')
- pi.supplier = '_Test Supplier'
- pi.append('items', {
- 'item_code': 'Macbook Pro',
- 'qty': 1
- })
+ pi = frappe.new_doc("Purchase Invoice")
+ pi.supplier = "_Test Supplier"
+ pi.append("items", {"item_code": "Macbook Pro", "qty": 1})
pi.set_missing_values()
asset = create_asset(item_code="Macbook Pro")
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,
+ )
disposal_account = frappe.get_cached_value("Company", "_Test Company", "disposal_account")
# 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(
"GL Entry",
- filters = {
- "voucher_no": si.name,
- "account": disposal_account
- },
- fields = ["credit", "debit"]
+ filters={"voucher_no": si.name, "account": disposal_account},
+ fields=["credit", "debit"],
)[0]
loss_for_return_si = frappe.get_all(
"GL Entry",
- filters = {
- "voucher_no": return_si.name,
- "account": disposal_account
- },
- fields = ["credit", "debit"]
+ filters={"voucher_no": return_si.name, "account": disposal_account},
+ fields=["credit", "debit"],
)[0]
- self.assertEqual(loss_for_si['credit'], loss_for_return_si['debit'])
- self.assertEqual(loss_for_si['debit'], loss_for_return_si['credit'])
+ self.assertEqual(loss_for_si["credit"], loss_for_return_si["debit"])
+ self.assertEqual(loss_for_si["debit"], loss_for_return_si["credit"])
def test_incoming_rate_for_stand_alone_credit_note(self):
- return_si = create_sales_invoice(is_return=1, update_stock=1, qty=-1, rate=90000, incoming_rate=10,
- company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', debit_to='Debtors - TCP1',
- income_account='Sales - TCP1', expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1')
+ return_si = create_sales_invoice(
+ is_return=1,
+ update_stock=1,
+ qty=-1,
+ rate=90000,
+ incoming_rate=10,
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ debit_to="Debtors - TCP1",
+ income_account="Sales - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1",
+ )
- incoming_rate = frappe.db.get_value('Stock Ledger Entry', {'voucher_no': return_si.name}, 'incoming_rate')
- debit_amount = frappe.db.get_value('GL Entry',
- {'voucher_no': return_si.name, 'account': 'Stock In Hand - TCP1'}, 'debit')
+ incoming_rate = frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_no": return_si.name}, "incoming_rate"
+ )
+ debit_amount = frappe.db.get_value(
+ "GL Entry", {"voucher_no": return_si.name, "account": "Stock In Hand - TCP1"}, "debit"
+ )
+
+ self.assertEqual(debit_amount, 10.0)
+ self.assertEqual(incoming_rate, 10.0)
+
+ def test_incoming_rate_for_stand_alone_credit_note(self):
+ return_si = create_sales_invoice(
+ is_return=1,
+ update_stock=1,
+ qty=-1,
+ rate=90000,
+ incoming_rate=10,
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ debit_to="Debtors - TCP1",
+ income_account="Sales - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1",
+ )
+
+ incoming_rate = frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_no": return_si.name}, "incoming_rate"
+ )
+ debit_amount = frappe.db.get_value(
+ "GL Entry", {"voucher_no": return_si.name, "account": "Stock In Hand - TCP1"}, "debit"
+ )
self.assertEqual(debit_amount, 10.0)
self.assertEqual(incoming_rate, 10.0)
@@ -1178,16 +1498,25 @@ class TestSalesInvoice(unittest.TestCase):
si.insert()
expected_values = {
- "keys": ["price_list_rate", "discount_percentage", "rate", "amount",
- "base_price_list_rate", "base_rate", "base_amount",
- "net_rate", "base_net_rate", "net_amount", "base_net_amount"],
+ "keys": [
+ "price_list_rate",
+ "discount_percentage",
+ "rate",
+ "amount",
+ "base_price_list_rate",
+ "base_rate",
+ "base_amount",
+ "net_rate",
+ "base_net_rate",
+ "net_amount",
+ "base_net_amount",
+ ],
"_Test Item Home Desktop 100": [50, 0, 50, 500, 50, 50, 500, 25, 25, 250, 250],
"_Test Item Home Desktop 200": [150, 0, 150, 750, 150, 150, 750, 75, 75, 375, 375],
}
# check if children are saved
- self.assertEqual(len(si.get("items")),
- len(expected_values)-1)
+ self.assertEqual(len(si.get("items")), len(expected_values) - 1)
# check if item values are calculated
for d in si.get("items"):
@@ -1202,16 +1531,19 @@ class TestSalesInvoice(unittest.TestCase):
# check tax calculation
expected_values = {
- "keys": ["tax_amount", "tax_amount_after_discount_amount",
- "base_tax_amount_after_discount_amount"],
+ "keys": [
+ "tax_amount",
+ "tax_amount_after_discount_amount",
+ "base_tax_amount_after_discount_amount",
+ ],
"_Test Account Shipping Charges - _TC": [100, 100, 100],
"_Test Account Customs Duty - _TC": [62.5, 62.5, 62.5],
"_Test Account Excise Duty - _TC": [70, 70, 70],
"_Test Account Education Cess - _TC": [1.4, 1.4, 1.4],
- "_Test Account S&H Education Cess - _TC": [.7, 0.7, 0.7],
+ "_Test Account S&H Education Cess - _TC": [0.7, 0.7, 0.7],
"_Test Account CST - _TC": [17.19, 17.19, 17.19],
"_Test Account VAT - _TC": [78.13, 78.13, 78.13],
- "_Test Account Discount - _TC": [-95.49, -95.49, -95.49]
+ "_Test Account Discount - _TC": [-95.49, -95.49, -95.49],
}
for d in si.get("taxes"):
@@ -1219,19 +1551,26 @@ class TestSalesInvoice(unittest.TestCase):
if expected_values.get(d.account_head):
self.assertEqual(d.get(k), expected_values[d.account_head][i])
-
self.assertEqual(si.total_taxes_and_charges, 234.43)
self.assertEqual(si.base_grand_total, 859.43)
self.assertEqual(si.grand_total, 859.43)
def test_multi_currency_gle(self):
- si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
- currency="USD", conversion_rate=50)
+ si = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
- gl_entries = frappe.db.sql("""select account, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
- order by account asc""", si.name, as_dict=1)
+ order by account asc""",
+ si.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -1241,26 +1580,35 @@ class TestSalesInvoice(unittest.TestCase):
"debit": 5000,
"debit_in_account_currency": 100,
"credit": 0,
- "credit_in_account_currency": 0
+ "credit_in_account_currency": 0,
},
"Sales - _TC": {
"account_currency": "INR",
"debit": 0,
"debit_in_account_currency": 0,
"credit": 5000,
- "credit_in_account_currency": 5000
- }
+ "credit_in_account_currency": 5000,
+ },
}
- for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"):
+ for field in (
+ "account_currency",
+ "debit",
+ "debit_in_account_currency",
+ "credit",
+ "credit_in_account_currency",
+ ):
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][field], gle[field])
# cancel
si.cancel()
- gle = frappe.db.sql("""select name from `tabGL Entry`
- where voucher_type='Sales Invoice' and voucher_no=%s""", si.name)
+ gle = frappe.db.sql(
+ """select name from `tabGL Entry`
+ where voucher_type='Sales Invoice' and voucher_no=%s""",
+ si.name,
+ )
self.assertTrue(gle)
@@ -1268,32 +1616,52 @@ class TestSalesInvoice(unittest.TestCase):
# Customer currency = USD
# Transaction currency cannot be INR
- si1 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
- do_not_save=True)
+ si1 = create_sales_invoice(
+ customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", do_not_save=True
+ )
self.assertRaises(InvalidCurrency, si1.save)
# Transaction currency cannot be EUR
- si2 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
- currency="EUR", conversion_rate=80, do_not_save=True)
+ si2 = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="EUR",
+ conversion_rate=80,
+ do_not_save=True,
+ )
self.assertRaises(InvalidCurrency, si2.save)
# Transaction currency only allowed in USD
- si3 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
- currency="USD", conversion_rate=50)
+ si3 = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
# Party Account currency must be in USD, as there is existing GLE with USD
- si4 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable - _TC",
- currency="USD", conversion_rate=50, do_not_submit=True)
+ si4 = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable - _TC",
+ currency="USD",
+ conversion_rate=50,
+ do_not_submit=True,
+ )
self.assertRaises(InvalidAccountCurrency, si4.submit)
# Party Account currency must be in USD, force customer currency as there is no GLE
si3.cancel()
- si5 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable - _TC",
- currency="USD", conversion_rate=50, do_not_submit=True)
+ si5 = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable - _TC",
+ currency="USD",
+ conversion_rate=50,
+ do_not_submit=True,
+ )
self.assertRaises(InvalidAccountCurrency, si5.submit)
@@ -1301,12 +1669,12 @@ class TestSalesInvoice(unittest.TestCase):
si = create_sales_invoice(item_code="_Test Item", qty=1, do_not_submit=True)
price_list_rate = flt(100) * flt(si.plc_conversion_rate)
si.items[0].price_list_rate = price_list_rate
- si.items[0].margin_type = 'Percentage'
+ si.items[0].margin_type = "Percentage"
si.items[0].margin_rate_or_amount = 25
si.items[0].discount_amount = 0.0
si.items[0].discount_percentage = 0.0
si.save()
- self.assertEqual(si.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate))
+ self.assertEqual(si.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate))
def test_outstanding_amount_after_advance_jv_cancelation(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
@@ -1314,89 +1682,107 @@ class TestSalesInvoice(unittest.TestCase):
)
jv = frappe.copy_doc(jv_test_records[0])
- jv.accounts[0].is_advance = 'Yes'
+ jv.accounts[0].is_advance = "Yes"
jv.insert()
jv.submit()
si = frappe.copy_doc(test_records[0])
- si.append("advances", {
- "doctype": "Sales Invoice Advance",
- "reference_type": "Journal Entry",
- "reference_name": jv.name,
- "reference_row": jv.get("accounts")[0].name,
- "advance_amount": 400,
- "allocated_amount": 300,
- "remarks": jv.remark
- })
+ si.append(
+ "advances",
+ {
+ "doctype": "Sales Invoice Advance",
+ "reference_type": "Journal Entry",
+ "reference_name": jv.name,
+ "reference_row": jv.get("accounts")[0].name,
+ "advance_amount": 400,
+ "allocated_amount": 300,
+ "remarks": jv.remark,
+ },
+ )
si.insert()
si.submit()
si.load_from_db()
- #check outstanding after advance allocation
- self.assertEqual(flt(si.outstanding_amount),
- flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")))
+ # check outstanding after advance allocation
+ self.assertEqual(
+ flt(si.outstanding_amount),
+ flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")),
+ )
- #added to avoid Document has been modified exception
+ # added to avoid Document has been modified exception
jv = frappe.get_doc("Journal Entry", jv.name)
jv.cancel()
si.load_from_db()
- #check outstanding after advance cancellation
- self.assertEqual(flt(si.outstanding_amount),
- flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount")))
+ # check outstanding after advance cancellation
+ self.assertEqual(
+ flt(si.outstanding_amount),
+ flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount")),
+ )
def test_outstanding_amount_after_advance_payment_entry_cancelation(self):
- pe = frappe.get_doc({
- "doctype": "Payment Entry",
- "payment_type": "Receive",
- "party_type": "Customer",
- "party": "_Test Customer",
- "company": "_Test Company",
- "paid_from_account_currency": "INR",
- "paid_to_account_currency": "INR",
- "source_exchange_rate": 1,
- "target_exchange_rate": 1,
- "reference_no": "1",
- "reference_date": nowdate(),
- "received_amount": 300,
- "paid_amount": 300,
- "paid_from": "_Test Receivable - _TC",
- "paid_to": "_Test Cash - _TC"
- })
+ pe = frappe.get_doc(
+ {
+ "doctype": "Payment Entry",
+ "payment_type": "Receive",
+ "party_type": "Customer",
+ "party": "_Test Customer",
+ "company": "_Test Company",
+ "paid_from_account_currency": "INR",
+ "paid_to_account_currency": "INR",
+ "source_exchange_rate": 1,
+ "target_exchange_rate": 1,
+ "reference_no": "1",
+ "reference_date": nowdate(),
+ "received_amount": 300,
+ "paid_amount": 300,
+ "paid_from": "_Test Receivable - _TC",
+ "paid_to": "_Test Cash - _TC",
+ }
+ )
pe.insert()
pe.submit()
si = frappe.copy_doc(test_records[0])
si.is_pos = 0
- si.append("advances", {
- "doctype": "Sales Invoice Advance",
- "reference_type": "Payment Entry",
- "reference_name": pe.name,
- "advance_amount": 300,
- "allocated_amount": 300,
- "remarks": pe.remarks
- })
+ si.append(
+ "advances",
+ {
+ "doctype": "Sales Invoice Advance",
+ "reference_type": "Payment Entry",
+ "reference_name": pe.name,
+ "advance_amount": 300,
+ "allocated_amount": 300,
+ "remarks": pe.remarks,
+ },
+ )
si.insert()
si.submit()
si.load_from_db()
- #check outstanding after advance allocation
- self.assertEqual(flt(si.outstanding_amount),
- flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")))
+ # check outstanding after advance allocation
+ self.assertEqual(
+ flt(si.outstanding_amount),
+ flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")),
+ )
- #added to avoid Document has been modified exception
+ # added to avoid Document has been modified exception
pe = frappe.get_doc("Payment Entry", pe.name)
pe.cancel()
si.load_from_db()
- #check outstanding after advance cancellation
- self.assertEqual(flt(si.outstanding_amount),
- flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount")))
+ # check outstanding after advance cancellation
+ self.assertEqual(
+ flt(si.outstanding_amount),
+ flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount")),
+ )
def test_multiple_uom_in_selling(self):
- frappe.db.sql("""delete from `tabItem Price`
- where price_list='_Test Price List' and item_code='_Test Item'""")
+ frappe.db.sql(
+ """delete from `tabItem Price`
+ where price_list='_Test Price List' and item_code='_Test Item'"""
+ )
item_price = frappe.new_doc("Item Price")
item_price.price_list = "_Test Price List"
item_price.item_code = "_Test Item"
@@ -1410,9 +1796,18 @@ class TestSalesInvoice(unittest.TestCase):
si.save()
expected_values = {
- "keys": ["price_list_rate", "stock_uom", "uom", "conversion_factor", "rate", "amount",
- "base_price_list_rate", "base_rate", "base_amount"],
- "_Test Item": [1000, "_Test UOM", "_Test UOM 1", 10.0, 1000, 1000, 1000, 1000, 1000]
+ "keys": [
+ "price_list_rate",
+ "stock_uom",
+ "uom",
+ "conversion_factor",
+ "rate",
+ "amount",
+ "base_price_list_rate",
+ "base_rate",
+ "base_amount",
+ ],
+ "_Test Item": [1000, "_Test UOM", "_Test UOM 1", 10.0, 1000, 1000, 1000, 1000, 1000],
}
# check if the conversion_factor and price_list_rate is calculated according to uom
@@ -1427,23 +1822,10 @@ class TestSalesInvoice(unittest.TestCase):
itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si)
expected_itemised_tax = {
- "_Test Item": {
- "Service Tax": {
- "tax_rate": 10.0,
- "tax_amount": 1000.0
- }
- },
- "_Test Item 2": {
- "Service Tax": {
- "tax_rate": 10.0,
- "tax_amount": 500.0
- }
- }
- }
- expected_itemised_taxable_amount = {
- "_Test Item": 10000.0,
- "_Test Item 2": 5000.0
+ "_Test Item": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0}},
+ "_Test Item 2": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0}},
}
+ expected_itemised_taxable_amount = {"_Test Item": 10000.0, "_Test Item 2": 5000.0}
self.assertEqual(itemised_tax, expected_itemised_tax)
self.assertEqual(itemised_taxable_amount, expected_itemised_taxable_amount)
@@ -1458,23 +1840,10 @@ class TestSalesInvoice(unittest.TestCase):
itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si)
expected_itemised_tax = {
- "_Test Item": {
- "Service Tax": {
- "tax_rate": 10.0,
- "tax_amount": 1000.0
- }
- },
- "_Test Item 2": {
- "Service Tax": {
- "tax_rate": 10.0,
- "tax_amount": 500.0
- }
- }
- }
- expected_itemised_taxable_amount = {
- "_Test Item": 10000.0,
- "_Test Item 2": 5000.0
+ "_Test Item": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0}},
+ "_Test Item 2": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0}},
}
+ expected_itemised_taxable_amount = {"_Test Item": 10000.0, "_Test Item 2": 5000.0}
self.assertEqual(itemised_tax, expected_itemised_tax)
self.assertEqual(itemised_taxable_amount, expected_itemised_taxable_amount)
@@ -1483,59 +1852,73 @@ class TestSalesInvoice(unittest.TestCase):
def create_si_to_test_tax_breakup(self):
si = create_sales_invoice(qty=100, rate=50, do_not_save=True)
- si.append("items", {
- "item_code": "_Test Item",
- "gst_hsn_code": "999800",
- "warehouse": "_Test Warehouse - _TC",
- "qty": 100,
- "rate": 50,
- "income_account": "Sales - _TC",
- "expense_account": "Cost of Goods Sold - _TC",
- "cost_center": "_Test Cost Center - _TC"
- })
- si.append("items", {
- "item_code": "_Test Item 2",
- "gst_hsn_code": "999800",
- "warehouse": "_Test Warehouse - _TC",
- "qty": 100,
- "rate": 50,
- "income_account": "Sales - _TC",
- "expense_account": "Cost of Goods Sold - _TC",
- "cost_center": "_Test Cost Center - _TC"
- })
+ si.append(
+ "items",
+ {
+ "item_code": "_Test Item",
+ "gst_hsn_code": "999800",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 100,
+ "rate": 50,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ )
+ si.append(
+ "items",
+ {
+ "item_code": "_Test Item 2",
+ "gst_hsn_code": "999800",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 100,
+ "rate": 50,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ )
- si.append("taxes", {
- "charge_type": "On Net Total",
- "account_head": "_Test Account Service Tax - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Service Tax",
- "rate": 10
- })
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 10,
+ },
+ )
si.insert()
return si
def test_company_monthly_sales(self):
- existing_current_month_sales = frappe.get_cached_value('Company', "_Test Company", "total_monthly_sales")
+ existing_current_month_sales = frappe.get_cached_value(
+ "Company", "_Test Company", "total_monthly_sales"
+ )
si = create_sales_invoice()
- current_month_sales = frappe.get_cached_value('Company', "_Test Company", "total_monthly_sales")
+ current_month_sales = frappe.get_cached_value("Company", "_Test Company", "total_monthly_sales")
self.assertEqual(current_month_sales, existing_current_month_sales + si.base_grand_total)
si.cancel()
- current_month_sales = frappe.get_cached_value('Company', "_Test Company", "total_monthly_sales")
+ current_month_sales = frappe.get_cached_value("Company", "_Test Company", "total_monthly_sales")
self.assertEqual(current_month_sales, existing_current_month_sales)
def test_rounding_adjustment(self):
si = create_sales_invoice(rate=24900, do_not_save=True)
for tax in ["Tax 1", "Tax2"]:
- si.append("taxes", {
- "charge_type": "On Net Total",
- "account_head": "_Test Account Service Tax - _TC",
- "description": tax,
- "rate": 14,
- "cost_center": "_Test Cost Center - _TC",
- "included_in_print_rate": 1
- })
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "description": tax,
+ "rate": 14,
+ "cost_center": "_Test Cost Center - _TC",
+ "included_in_print_rate": 1,
+ },
+ )
si.save()
si.submit()
self.assertEqual(si.net_total, 19453.13)
@@ -1543,16 +1926,23 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.total_taxes_and_charges, 5446.88)
self.assertEqual(si.rounding_adjustment, -0.01)
- expected_values = dict((d[0], d) for d in [
- [si.debit_to, 24900, 0.0],
- ["_Test Account Service Tax - _TC", 0.0, 5446.88],
- ["Sales - _TC", 0.0, 19453.13],
- ["Round Off - _TC", 0.01, 0.0]
- ])
+ expected_values = dict(
+ (d[0], d)
+ for d in [
+ [si.debit_to, 24900, 0.0],
+ ["_Test Account Service Tax - _TC", 0.0, 5446.88],
+ ["Sales - _TC", 0.0, 19453.13],
+ ["Round Off - _TC", 0.01, 0.0],
+ ]
+ )
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
- order by account asc""", si.name, as_dict=1)
+ order by account asc""",
+ si.name,
+ as_dict=1,
+ )
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
@@ -1562,24 +1952,30 @@ class TestSalesInvoice(unittest.TestCase):
def test_rounding_adjustment_2(self):
si = create_sales_invoice(rate=400, do_not_save=True)
for rate in [400, 600, 100]:
- si.append("items", {
- "item_code": "_Test Item",
- "gst_hsn_code": "999800",
- "warehouse": "_Test Warehouse - _TC",
- "qty": 1,
- "rate": rate,
- "income_account": "Sales - _TC",
- "cost_center": "_Test Cost Center - _TC"
- })
+ si.append(
+ "items",
+ {
+ "item_code": "_Test Item",
+ "gst_hsn_code": "999800",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 1,
+ "rate": rate,
+ "income_account": "Sales - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ )
for tax_account in ["_Test Account VAT - _TC", "_Test Account Service Tax - _TC"]:
- si.append("taxes", {
- "charge_type": "On Net Total",
- "account_head": tax_account,
- "description": tax_account,
- "rate": 9,
- "cost_center": "_Test Cost Center - _TC",
- "included_in_print_rate": 1
- })
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": tax_account,
+ "description": tax_account,
+ "rate": 9,
+ "cost_center": "_Test Cost Center - _TC",
+ "included_in_print_rate": 1,
+ },
+ )
si.save()
si.submit()
self.assertEqual(si.net_total, 1271.19)
@@ -1587,16 +1983,23 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.total_taxes_and_charges, 228.82)
self.assertEqual(si.rounding_adjustment, -0.01)
- expected_values = dict((d[0], d) for d in [
- [si.debit_to, 1500, 0.0],
- ["_Test Account Service Tax - _TC", 0.0, 114.41],
- ["_Test Account VAT - _TC", 0.0, 114.41],
- ["Sales - _TC", 0.0, 1271.18]
- ])
+ expected_values = dict(
+ (d[0], d)
+ for d in [
+ [si.debit_to, 1500, 0.0],
+ ["_Test Account Service Tax - _TC", 0.0, 114.41],
+ ["_Test Account VAT - _TC", 0.0, 114.41],
+ ["Sales - _TC", 0.0, 1271.18],
+ ]
+ )
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
- order by account asc""", si.name, as_dict=1)
+ order by account asc""",
+ si.name,
+ as_dict=1,
+ )
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
@@ -1604,27 +2007,44 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_rounding_adjustment_3(self):
+ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
+ create_dimension,
+ disable_dimension,
+ )
+
+ create_dimension()
+
si = create_sales_invoice(do_not_save=True)
si.items = []
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
- si.append("items", {
- "item_code": "_Test Item",
- "gst_hsn_code": "999800",
- "warehouse": "_Test Warehouse - _TC",
- "qty": d[1],
- "rate": d[0],
- "income_account": "Sales - _TC",
- "cost_center": "_Test Cost Center - _TC"
- })
+ si.append(
+ "items",
+ {
+ "item_code": "_Test Item",
+ "gst_hsn_code": "999800",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": d[1],
+ "rate": d[0],
+ "income_account": "Sales - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ )
for tax_account in ["_Test Account VAT - _TC", "_Test Account Service Tax - _TC"]:
- si.append("taxes", {
- "charge_type": "On Net Total",
- "account_head": tax_account,
- "description": tax_account,
- "rate": 6,
- "cost_center": "_Test Cost Center - _TC",
- "included_in_print_rate": 1
- })
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": tax_account,
+ "description": tax_account,
+ "rate": 6,
+ "cost_center": "_Test Cost Center - _TC",
+ "included_in_print_rate": 1,
+ },
+ )
+
+ si.cost_center = "_Test Cost Center 2 - _TC"
+ si.location = "Block 1"
+
si.save()
si.submit()
self.assertEqual(si.net_total, 4007.16)
@@ -1632,31 +2052,52 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.total_taxes_and_charges, 480.86)
self.assertEqual(si.rounding_adjustment, -0.02)
- expected_values = dict((d[0], d) for d in [
- [si.debit_to, 4488.0, 0.0],
- ["_Test Account Service Tax - _TC", 0.0, 240.43],
- ["_Test Account VAT - _TC", 0.0, 240.43],
- ["Sales - _TC", 0.0, 4007.15],
- ["Round Off - _TC", 0.01, 0]
- ])
+ expected_values = dict(
+ (d[0], d)
+ for d in [
+ [si.debit_to, 4488.0, 0.0],
+ ["_Test Account Service Tax - _TC", 0.0, 240.43],
+ ["_Test Account VAT - _TC", 0.0, 240.43],
+ ["Sales - _TC", 0.0, 4007.15],
+ ["Round Off - _TC", 0.01, 0],
+ ]
+ )
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
- order by account asc""", si.name, as_dict=1)
+ order by account asc""",
+ si.name,
+ as_dict=1,
+ )
debit_credit_diff = 0
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
- debit_credit_diff += (gle.debit - gle.credit)
+ debit_credit_diff += gle.debit - gle.credit
self.assertEqual(debit_credit_diff, 0)
+ round_off_gle = frappe.db.get_value(
+ "GL Entry",
+ {"voucher_type": "Sales Invoice", "voucher_no": si.name, "account": "Round Off - _TC"},
+ ["cost_center", "location"],
+ as_dict=1,
+ )
+
+ self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC")
+ self.assertEqual(round_off_gle.location, "Block 1")
+
+ disable_dimension()
+
def test_sales_invoice_with_shipping_rule(self):
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
- shipping_rule = create_shipping_rule(shipping_rule_type = "Selling", shipping_rule_name = "Shipping Rule - Sales Invoice Test")
+ shipping_rule = create_shipping_rule(
+ shipping_rule_type="Selling", shipping_rule_name="Shipping Rule - Sales Invoice Test"
+ )
si = frappe.copy_doc(test_records[2])
@@ -1669,29 +2110,32 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.total_taxes_and_charges, 468.85)
self.assertEqual(si.grand_total, 1718.85)
-
-
def test_create_invoice_without_terms(self):
si = create_sales_invoice(do_not_save=1)
- self.assertFalse(si.get('payment_schedule'))
+ self.assertFalse(si.get("payment_schedule"))
si.insert()
- self.assertTrue(si.get('payment_schedule'))
+ self.assertTrue(si.get("payment_schedule"))
def test_duplicate_due_date_in_terms(self):
si = create_sales_invoice(do_not_save=1)
- si.append('payment_schedule', dict(due_date='2017-01-01', invoice_portion=50.00, payment_amount=50))
- si.append('payment_schedule', dict(due_date='2017-01-01', invoice_portion=50.00, payment_amount=50))
+ si.append(
+ "payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50)
+ )
+ si.append(
+ "payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50)
+ )
self.assertRaises(frappe.ValidationError, si.insert)
def test_credit_note(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
- si = create_sales_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1)
+ si = create_sales_invoice(item_code="_Test Item", qty=(5 * -1), rate=500, is_return=1)
- outstanding_amount = get_outstanding_amount(si.doctype,
- si.name, "Debtors - _TC", si.customer, "Customer")
+ outstanding_amount = get_outstanding_amount(
+ si.doctype, si.name, "Debtors - _TC", si.customer, "Customer"
+ )
self.assertEqual(si.outstanding_amount, outstanding_amount)
@@ -1706,30 +2150,31 @@ class TestSalesInvoice(unittest.TestCase):
pe.insert()
pe.submit()
- si_doc = frappe.get_doc('Sales Invoice', si.name)
+ si_doc = frappe.get_doc("Sales Invoice", si.name)
self.assertEqual(si_doc.outstanding_amount, 0)
def test_sales_invoice_with_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+
cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
- si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC")
+ si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC")
self.assertEqual(si.cost_center, cost_center)
expected_values = {
- "Debtors - _TC": {
- "cost_center": cost_center
- },
- "Sales - _TC": {
- "cost_center": cost_center
- }
+ "Debtors - _TC": {"cost_center": cost_center},
+ "Sales - _TC": {"cost_center": cost_center},
}
- gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, cost_center, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
- order by account asc""", si.name, as_dict=1)
+ order by account asc""",
+ si.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -1739,16 +2184,20 @@ class TestSalesInvoice(unittest.TestCase):
def test_sales_invoice_with_project_link(self):
from erpnext.projects.doctype.project.test_project import make_project
- project = make_project({
- 'project_name': 'Sales Invoice Project',
- 'project_template_name': 'Test Project Template',
- 'start_date': '2020-01-01'
- })
- item_project = make_project({
- 'project_name': 'Sales Invoice Item Project',
- 'project_template_name': 'Test Project Template',
- 'start_date': '2019-06-01'
- })
+ project = make_project(
+ {
+ "project_name": "Sales Invoice Project",
+ "project_template_name": "Test Project Template",
+ "start_date": "2020-01-01",
+ }
+ )
+ item_project = make_project(
+ {
+ "project_name": "Sales Invoice Item Project",
+ "project_template_name": "Test Project Template",
+ "start_date": "2019-06-01",
+ }
+ )
sales_invoice = create_sales_invoice(do_not_save=1)
sales_invoice.items[0].project = item_project.name
@@ -1757,18 +2206,18 @@ class TestSalesInvoice(unittest.TestCase):
sales_invoice.submit()
expected_values = {
- "Debtors - _TC": {
- "project": project.name
- },
- "Sales - _TC": {
- "project": item_project.name
- }
+ "Debtors - _TC": {"project": project.name},
+ "Sales - _TC": {"project": item_project.name},
}
- gl_entries = frappe.db.sql("""select account, cost_center, project, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, cost_center, project, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
- order by account asc""", sales_invoice.name, as_dict=1)
+ order by account asc""",
+ sales_invoice.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -1777,21 +2226,21 @@ class TestSalesInvoice(unittest.TestCase):
def test_sales_invoice_without_cost_center(self):
cost_center = "_Test Cost Center - _TC"
- si = create_sales_invoice(debit_to="Debtors - _TC")
+ si = create_sales_invoice(debit_to="Debtors - _TC")
expected_values = {
- "Debtors - _TC": {
- "cost_center": None
- },
- "Sales - _TC": {
- "cost_center": cost_center
- }
+ "Debtors - _TC": {"cost_center": None},
+ "Sales - _TC": {"cost_center": cost_center},
}
- gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, cost_center, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
- order by account asc""", si.name, as_dict=1)
+ order by account asc""",
+ si.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -1799,8 +2248,11 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
def test_deferred_revenue(self):
- deferred_account = create_account(account_name="Deferred Revenue",
- parent_account="Current Liabilities - _TC", company="_Test Company")
+ deferred_account = create_account(
+ account_name="Deferred Revenue",
+ parent_account="Current Liabilities - _TC",
+ company="_Test Company",
+ )
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1
@@ -1816,14 +2268,16 @@ class TestSalesInvoice(unittest.TestCase):
si.save()
si.submit()
- pda1 = frappe.get_doc(dict(
- doctype='Process Deferred Accounting',
- posting_date=nowdate(),
- start_date="2019-01-01",
- end_date="2019-03-31",
- type="Income",
- company="_Test Company"
- ))
+ pda1 = frappe.get_doc(
+ dict(
+ doctype="Process Deferred Accounting",
+ posting_date=nowdate(),
+ start_date="2019-01-01",
+ end_date="2019-03-31",
+ type="Income",
+ company="_Test Company",
+ )
+ )
pda1.insert()
pda1.submit()
@@ -1834,17 +2288,28 @@ class TestSalesInvoice(unittest.TestCase):
[deferred_account, 43.08, 0.0, "2019-02-28"],
["Sales - _TC", 0.0, 43.08, "2019-02-28"],
[deferred_account, 23.07, 0.0, "2019-03-15"],
- ["Sales - _TC", 0.0, 23.07, "2019-03-15"]
+ ["Sales - _TC", 0.0, 23.07, "2019-03-15"],
]
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
- def test_fixed_deferred_revenue(self):
- deferred_account = create_account(account_name="Deferred Revenue",
- parent_account="Current Liabilities - _TC", company="_Test Company")
+ def test_deferred_revenue_missing_account(self):
+ si = create_sales_invoice(posting_date="2019-01-10", do_not_submit=True)
+ si.items[0].enable_deferred_revenue = 1
+ si.items[0].service_start_date = "2019-01-10"
+ si.items[0].service_end_date = "2019-03-15"
- acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings')
- acc_settings.book_deferred_entries_based_on = 'Months'
+ self.assertRaises(frappe.ValidationError, si.save)
+
+ def test_fixed_deferred_revenue(self):
+ deferred_account = create_account(
+ account_name="Deferred Revenue",
+ parent_account="Current Liabilities - _TC",
+ company="_Test Company",
+ )
+
+ acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
+ acc_settings.book_deferred_entries_based_on = "Months"
acc_settings.save()
item = create_item("_Test Item for Deferred Accounting")
@@ -1853,7 +2318,9 @@ class TestSalesInvoice(unittest.TestCase):
item.no_of_months = 12
item.save()
- si = create_sales_invoice(item=item.name, posting_date="2019-01-16", rate=50000, do_not_submit=True)
+ si = create_sales_invoice(
+ item=item.name, posting_date="2019-01-16", rate=50000, do_not_submit=True
+ )
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-16"
si.items[0].service_end_date = "2019-03-31"
@@ -1861,14 +2328,16 @@ class TestSalesInvoice(unittest.TestCase):
si.save()
si.submit()
- pda1 = frappe.get_doc(dict(
- doctype='Process Deferred Accounting',
- posting_date='2019-03-31',
- start_date="2019-01-01",
- end_date="2019-03-31",
- type="Income",
- company="_Test Company"
- ))
+ pda1 = frappe.get_doc(
+ dict(
+ doctype="Process Deferred Accounting",
+ posting_date="2019-03-31",
+ start_date="2019-01-01",
+ end_date="2019-03-31",
+ type="Income",
+ company="_Test Company",
+ )
+ )
pda1.insert()
pda1.submit()
@@ -1879,13 +2348,13 @@ class TestSalesInvoice(unittest.TestCase):
[deferred_account, 20000.0, 0.0, "2019-02-28"],
["Sales - _TC", 0.0, 20000.0, "2019-02-28"],
[deferred_account, 20000.0, 0.0, "2019-03-31"],
- ["Sales - _TC", 0.0, 20000.0, "2019-03-31"]
+ ["Sales - _TC", 0.0, 20000.0, "2019-03-31"],
]
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
- acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings')
- acc_settings.book_deferred_entries_based_on = 'Days'
+ acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
+ acc_settings.book_deferred_entries_based_on = "Days"
acc_settings.save()
def test_inter_company_transaction(self):
@@ -1894,45 +2363,47 @@ class TestSalesInvoice(unittest.TestCase):
create_internal_customer(
customer_name="_Test Internal Customer",
represents_company="_Test Company 1",
- allowed_to_interact_with="Wind Power LLC"
+ allowed_to_interact_with="Wind Power LLC",
)
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 = 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.append("companies", {"company": "_Test Company 1"})
supplier.insert()
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",
- do_not_save = 1
+ 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",
+ 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)
- target_doc.items[0].update({
- "expense_account": "Cost of Goods Sold - _TC1",
- "cost_center": "Main - _TC1",
- "warehouse": "Stores - _TC1"
- })
+ target_doc.items[0].update(
+ {
+ "expense_account": "Cost of Goods Sold - _TC1",
+ "cost_center": "Main - _TC1",
+ "warehouse": "Stores - _TC1",
+ }
+ )
target_doc.submit()
self.assertEqual(target_doc.company, "_Test Company 1")
@@ -1942,9 +2413,9 @@ class TestSalesInvoice(unittest.TestCase):
se = make_stock_entry(
item_code="138-CMS Shoe",
target="Finished Goods - _TC",
- company = "_Test Company",
+ company="_Test Company",
qty=1,
- basic_rate=500
+ basic_rate=500,
)
si = frappe.copy_doc(test_records[0])
@@ -1956,8 +2427,9 @@ class TestSalesInvoice(unittest.TestCase):
si.insert()
si.submit()
- sles = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": si.name},
- fields=["name", "actual_qty"])
+ sles = frappe.get_all(
+ "Stock Ledger Entry", filters={"voucher_no": si.name}, fields=["name", "actual_qty"]
+ )
# check if both SLEs are created
self.assertEqual(len(sles), 2)
@@ -1971,82 +2443,92 @@ class TestSalesInvoice(unittest.TestCase):
## Create internal transfer account
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
- account = create_account(account_name="Unrealized Profit",
- parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory")
+ account = create_account(
+ account_name="Unrealized Profit",
+ parent_account="Current Liabilities - TCP1",
+ company="_Test Company with perpetual inventory",
+ )
- frappe.db.set_value('Company', '_Test Company with perpetual inventory',
- 'unrealized_profit_loss_account', account)
+ frappe.db.set_value(
+ "Company", "_Test Company with perpetual inventory", "unrealized_profit_loss_account", account
+ )
- customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory",
- "_Test Company with perpetual inventory")
+ customer = create_internal_customer(
+ "_Test Internal Customer 2",
+ "_Test Company with perpetual inventory",
+ "_Test Company with perpetual inventory",
+ )
- create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory",
- "_Test Company with perpetual inventory")
+ create_internal_supplier(
+ "_Test Internal Supplier 2",
+ "_Test Company with perpetual inventory",
+ "_Test Company with perpetual inventory",
+ )
si = create_sales_invoice(
- company = "_Test Company with perpetual inventory",
- customer = customer,
- debit_to = "Debtors - TCP1",
- warehouse = "Stores - TCP1",
- income_account = "Sales - TCP1",
- expense_account = "Cost of Goods Sold - TCP1",
- cost_center = "Main - TCP1",
- currency = "INR",
- do_not_save = 1
+ company="_Test Company with perpetual inventory",
+ customer=customer,
+ debit_to="Debtors - TCP1",
+ warehouse="Stores - TCP1",
+ income_account="Sales - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1",
+ currency="INR",
+ do_not_save=1,
)
si.selling_price_list = "_Test Price List Rest of the World"
si.update_stock = 1
- si.items[0].target_warehouse = 'Work In Progress - TCP1'
+ si.items[0].target_warehouse = "Work In Progress - TCP1"
# Add stock to stores for succesful stock transfer
make_stock_entry(
- target="Stores - TCP1",
- company = "_Test Company with perpetual inventory",
- qty=1,
- basic_rate=100
+ target="Stores - TCP1", company="_Test Company with perpetual inventory", qty=1, basic_rate=100
)
add_taxes(si)
si.save()
rate = 0.0
- for d in si.get('items'):
- rate = get_incoming_rate({
- "item_code": d.item_code,
- "warehouse": d.warehouse,
- "posting_date": si.posting_date,
- "posting_time": si.posting_time,
- "qty": -1 * flt(d.get('stock_qty')),
- "serial_no": d.serial_no,
- "company": si.company,
- "voucher_type": 'Sales Invoice',
- "voucher_no": si.name,
- "allow_zero_valuation": d.get("allow_zero_valuation")
- }, raise_error_if_no_rate=False)
+ for d in si.get("items"):
+ rate = get_incoming_rate(
+ {
+ "item_code": d.item_code,
+ "warehouse": d.warehouse,
+ "posting_date": si.posting_date,
+ "posting_time": si.posting_time,
+ "qty": -1 * flt(d.get("stock_qty")),
+ "serial_no": d.serial_no,
+ "company": si.company,
+ "voucher_type": "Sales Invoice",
+ "voucher_no": si.name,
+ "allow_zero_valuation": d.get("allow_zero_valuation"),
+ },
+ raise_error_if_no_rate=False,
+ )
rate = flt(rate, 2)
si.submit()
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
- target_doc.company = '_Test Company with perpetual inventory'
- target_doc.items[0].warehouse = 'Finished Goods - TCP1'
+ target_doc.company = "_Test Company with perpetual inventory"
+ target_doc.items[0].warehouse = "Finished Goods - TCP1"
add_taxes(target_doc)
target_doc.save()
target_doc.submit()
- tax_amount = flt(rate * (12/100), 2)
+ tax_amount = flt(rate * (12 / 100), 2)
si_gl_entries = [
["_Test Account Excise Duty - TCP1", 0.0, tax_amount, nowdate()],
- ["Unrealized Profit - TCP1", tax_amount, 0.0, nowdate()]
+ ["Unrealized Profit - TCP1", tax_amount, 0.0, nowdate()],
]
check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1))
pi_gl_entries = [
- ["_Test Account Excise Duty - TCP1", tax_amount , 0.0, nowdate()],
- ["Unrealized Profit - TCP1", 0.0, tax_amount, nowdate()]
+ ["_Test Account Excise Duty - TCP1", tax_amount, 0.0, nowdate()],
+ ["Unrealized Profit - TCP1", 0.0, tax_amount, nowdate()],
]
# Sale and Purchase both should be at valuation rate
@@ -2062,43 +2544,46 @@ class TestSalesInvoice(unittest.TestCase):
data = get_ewb_data("Sales Invoice", [si.name])
- self.assertEqual(data['version'], '1.0.0421')
- self.assertEqual(data['billLists'][0]['fromGstin'], '27AAECE4835E1ZR')
- self.assertEqual(data['billLists'][0]['fromTrdName'], '_Test Company')
- self.assertEqual(data['billLists'][0]['toTrdName'], '_Test Customer')
- self.assertEqual(data['billLists'][0]['vehicleType'], 'R')
- self.assertEqual(data['billLists'][0]['totalValue'], 60000)
- self.assertEqual(data['billLists'][0]['cgstValue'], 5400)
- self.assertEqual(data['billLists'][0]['sgstValue'], 5400)
- self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234')
- self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000)
- self.assertEqual(data['billLists'][0]['actualFromStateCode'],7)
- self.assertEqual(data['billLists'][0]['fromStateCode'],27)
+ self.assertEqual(data["version"], "1.0.0421")
+ self.assertEqual(data["billLists"][0]["fromGstin"], "27AAECE4835E1ZR")
+ self.assertEqual(data["billLists"][0]["fromTrdName"], "_Test Company")
+ self.assertEqual(data["billLists"][0]["toTrdName"], "_Test Customer")
+ self.assertEqual(data["billLists"][0]["vehicleType"], "R")
+ self.assertEqual(data["billLists"][0]["totalValue"], 60000)
+ self.assertEqual(data["billLists"][0]["cgstValue"], 5400)
+ self.assertEqual(data["billLists"][0]["sgstValue"], 5400)
+ self.assertEqual(data["billLists"][0]["vehicleNo"], "KA12KA1234")
+ self.assertEqual(data["billLists"][0]["itemList"][0]["taxableAmount"], 60000)
+ self.assertEqual(data["billLists"][0]["actualFromStateCode"], 7)
+ self.assertEqual(data["billLists"][0]["fromStateCode"], 27)
def test_einvoice_submission_without_irn(self):
# init
- einvoice_settings = frappe.get_doc('E Invoice Settings')
+ einvoice_settings = frappe.get_doc("E Invoice Settings")
einvoice_settings.enable = 1
einvoice_settings.applicable_from = nowdate()
- einvoice_settings.append('credentials', {
- 'company': '_Test Company',
- 'gstin': '27AAECE4835E1ZR',
- 'username': 'test',
- 'password': 'test'
- })
+ einvoice_settings.append(
+ "credentials",
+ {
+ "company": "_Test Company",
+ "gstin": "27AAECE4835E1ZR",
+ "username": "test",
+ "password": "test",
+ },
+ )
einvoice_settings.save()
country = frappe.flags.country
- frappe.flags.country = 'India'
+ frappe.flags.country = "India"
si = make_sales_invoice_for_ewaybill()
self.assertRaises(frappe.ValidationError, si.submit)
- si.irn = 'test_irn'
+ si.irn = "test_irn"
si.submit()
# reset
- einvoice_settings = frappe.get_doc('E Invoice Settings')
+ einvoice_settings = frappe.get_doc("E Invoice Settings")
einvoice_settings.enable = 0
frappe.flags.country = country
@@ -2110,15 +2595,15 @@ class TestSalesInvoice(unittest.TestCase):
si.save()
einvoice = make_einvoice(si)
- self.assertTrue(einvoice['EwbDtls'])
+ self.assertTrue(einvoice["EwbDtls"])
validate_totals(einvoice)
- si.apply_discount_on = 'Net Total'
+ si.apply_discount_on = "Net Total"
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
- [d.set('included_in_print_rate', 1) for d in si.taxes]
+ [d.set("included_in_print_rate", 1) for d in si.taxes]
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
@@ -2126,29 +2611,39 @@ class TestSalesInvoice(unittest.TestCase):
def test_item_tax_net_range(self):
item = create_item("T Shirt")
- item.set('taxes', [])
- item.append("taxes", {
- "item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
- "minimum_net_rate": 0,
- "maximum_net_rate": 500
- })
+ item.set("taxes", [])
+ item.append(
+ "taxes",
+ {
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
+ "minimum_net_rate": 0,
+ "maximum_net_rate": 500,
+ },
+ )
- item.append("taxes", {
- "item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
- "minimum_net_rate": 501,
- "maximum_net_rate": 1000
- })
+ item.append(
+ "taxes",
+ {
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
+ "minimum_net_rate": 501,
+ "maximum_net_rate": 1000,
+ },
+ )
item.save()
- sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
- self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
+ sales_invoice = create_sales_invoice(item="T Shirt", rate=700, do_not_submit=True)
+ self.assertEqual(
+ sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC"
+ )
# Apply discount
- sales_invoice.apply_discount_on = 'Net Total'
+ sales_invoice.apply_discount_on = "Net Total"
sales_invoice.discount_amount = 300
sales_invoice.save()
- self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
+ self.assertEqual(
+ sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC"
+ )
def test_sales_invoice_with_discount_accounting_enabled(self):
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
@@ -2157,14 +2652,17 @@ class TestSalesInvoice(unittest.TestCase):
enable_discount_accounting()
- discount_account = create_account(account_name="Discount Account",
- parent_account="Indirect Expenses - _TC", company="_Test Company")
+ discount_account = create_account(
+ account_name="Discount Account",
+ parent_account="Indirect Expenses - _TC",
+ company="_Test Company",
+ )
si = create_sales_invoice(discount_account=discount_account, discount_percentage=10, rate=90)
expected_gle = [
["Debtors - _TC", 90.0, 0.0, nowdate()],
["Discount Account - _TC", 10.0, 0.0, nowdate()],
- ["Sales - _TC", 0.0, 100.0, nowdate()]
+ ["Sales - _TC", 0.0, 100.0, nowdate()],
]
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
@@ -2176,27 +2674,33 @@ class TestSalesInvoice(unittest.TestCase):
)
enable_discount_accounting()
- additional_discount_account = create_account(account_name="Discount Account",
- parent_account="Indirect Expenses - _TC", company="_Test Company")
+ additional_discount_account = create_account(
+ account_name="Discount Account",
+ parent_account="Indirect Expenses - _TC",
+ company="_Test Company",
+ )
- si = create_sales_invoice(parent_cost_center='Main - _TC', do_not_save=1)
+ si = create_sales_invoice(parent_cost_center="Main - _TC", do_not_save=1)
si.apply_discount_on = "Grand Total"
si.additional_discount_account = additional_discount_account
si.additional_discount_percentage = 20
- si.append("taxes", {
- "charge_type": "On Net Total",
- "account_head": "_Test Account VAT - _TC",
- "cost_center": "Main - _TC",
- "description": "Test",
- "rate": 10
- })
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account VAT - _TC",
+ "cost_center": "Main - _TC",
+ "description": "Test",
+ "rate": 10,
+ },
+ )
si.submit()
expected_gle = [
["_Test Account VAT - _TC", 0.0, 10.0, nowdate()],
["Debtors - _TC", 88, 0.0, nowdate()],
["Discount Account - _TC", 22.0, 0.0, nowdate()],
- ["Sales - _TC", 0.0, 100.0, nowdate()]
+ ["Sales - _TC", 0.0, 100.0, nowdate()],
]
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
@@ -2204,20 +2708,22 @@ class TestSalesInvoice(unittest.TestCase):
def test_asset_depreciation_on_sale_with_pro_rata(self):
"""
- Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
+ Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
"""
create_asset_data()
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1)
post_depreciation_entries(getdate("2021-09-30"))
- create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30"))
+ create_sales_invoice(
+ item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30")
+ )
asset.load_from_db()
expected_values = [
["2020-06-30", 1366.12, 1366.12],
["2021-06-30", 20000.0, 21366.12],
- ["2021-09-30", 5041.1, 26407.22]
+ ["2021-09-30", 5041.1, 26407.22],
]
for i, schedule in enumerate(asset.schedules):
@@ -2228,23 +2734,28 @@ class TestSalesInvoice(unittest.TestCase):
def test_asset_depreciation_on_sale_without_pro_rata(self):
"""
- Tests if an Asset set to depreciate yearly on Dec 31, that gets sold on Dec 31 after two years, created an additional depreciation entry on its date of sale.
+ Tests if an Asset set to depreciate yearly on Dec 31, that gets sold on Dec 31 after two years, created an additional depreciation entry on its date of sale.
"""
create_asset_data()
- asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1,
- available_for_use_date=getdate("2019-12-31"), total_number_of_depreciations=3,
- expected_value_after_useful_life=10000, depreciation_start_date=getdate("2020-12-31"), submit=1)
+ asset = create_asset(
+ item_code="Macbook Pro",
+ calculate_depreciation=1,
+ available_for_use_date=getdate("2019-12-31"),
+ total_number_of_depreciations=3,
+ expected_value_after_useful_life=10000,
+ depreciation_start_date=getdate("2020-12-31"),
+ submit=1,
+ )
post_depreciation_entries(getdate("2021-09-30"))
- create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-12-31"))
+ create_sales_invoice(
+ item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-12-31")
+ )
asset.load_from_db()
- expected_values = [
- ["2020-12-31", 30000, 30000],
- ["2021-12-31", 30000, 60000]
- ]
+ expected_values = [["2020-12-31", 30000, 30000], ["2021-12-31", 30000, 60000]]
for i, schedule in enumerate(asset.schedules):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
@@ -2259,7 +2770,9 @@ class TestSalesInvoice(unittest.TestCase):
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1)
post_depreciation_entries(getdate("2021-09-30"))
- si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30"))
+ si = create_sales_invoice(
+ item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30")
+ )
return_si = make_return_doc("Sales Invoice", si.name)
return_si.submit()
asset.load_from_db()
@@ -2269,8 +2782,8 @@ class TestSalesInvoice(unittest.TestCase):
["2021-06-30", 20000.0, 21366.12, True],
["2022-06-30", 20000.0, 41366.12, False],
["2023-06-30", 20000.0, 61366.12, False],
- ["2024-06-30", 20000.0, 81366.12, False],
- ["2025-06-06", 18633.88, 100000.0, False]
+ ["2024-06-30", 20000.0, 81366.12, False],
+ ["2025-06-06", 18633.88, 100000.0, False],
]
for i, schedule in enumerate(asset.schedules):
@@ -2295,30 +2808,34 @@ class TestSalesInvoice(unittest.TestCase):
party_link = create_party_link("Supplier", supplier, customer)
# enable common party accounting
- frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 1)
+ frappe.db.set_value("Accounts Settings", None, "enable_common_party_accounting", 1)
# create a sales invoice
si = create_sales_invoice(customer=customer, parent_cost_center="_Test Cost Center - _TC")
# check outstanding of sales invoice
si.reload()
- self.assertEqual(si.status, 'Paid')
+ self.assertEqual(si.status, "Paid")
self.assertEqual(flt(si.outstanding_amount), 0.0)
# check creation of journal entry
- jv = frappe.get_all('Journal Entry Account', {
- 'account': si.debit_to,
- 'party_type': 'Customer',
- 'party': si.customer,
- 'reference_type': si.doctype,
- 'reference_name': si.name
- }, pluck='credit_in_account_currency')
+ jv = frappe.get_all(
+ "Journal Entry Account",
+ {
+ "account": si.debit_to,
+ "party_type": "Customer",
+ "party": si.customer,
+ "reference_type": si.doctype,
+ "reference_name": si.name,
+ },
+ pluck="credit_in_account_currency",
+ )
self.assertTrue(jv)
self.assertEqual(jv[0], si.grand_total)
party_link.delete()
- frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 0)
+ frappe.db.set_value("Accounts Settings", None, "enable_common_party_accounting", 0)
def test_payment_statuses(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
@@ -2328,16 +2845,14 @@ class TestSalesInvoice(unittest.TestCase):
# Test Overdue
si = create_sales_invoice(do_not_submit=True)
si.payment_schedule = []
- si.append("payment_schedule", {
- "due_date": add_days(today, -5),
- "invoice_portion": 50,
- "payment_amount": si.grand_total / 2
- })
- si.append("payment_schedule", {
- "due_date": add_days(today, 5),
- "invoice_portion": 50,
- "payment_amount": si.grand_total / 2
- })
+ si.append(
+ "payment_schedule",
+ {"due_date": add_days(today, -5), "invoice_portion": 50, "payment_amount": si.grand_total / 2},
+ )
+ si.append(
+ "payment_schedule",
+ {"due_date": add_days(today, 5), "invoice_portion": 50, "payment_amount": si.grand_total / 2},
+ )
si.submit()
self.assertEqual(si.status, "Overdue")
@@ -2376,21 +2891,23 @@ class TestSalesInvoice(unittest.TestCase):
# Sales Invoice with Payment Schedule
si_with_payment_schedule = create_sales_invoice(do_not_submit=True)
- si_with_payment_schedule.extend("payment_schedule", [
- {
- "due_date": add_days(today, -5),
- "invoice_portion": 50,
- "payment_amount": si_with_payment_schedule.grand_total / 2
- },
- {
- "due_date": add_days(today, 5),
- "invoice_portion": 50,
- "payment_amount": si_with_payment_schedule.grand_total / 2
- }
- ])
+ si_with_payment_schedule.extend(
+ "payment_schedule",
+ [
+ {
+ "due_date": add_days(today, -5),
+ "invoice_portion": 50,
+ "payment_amount": si_with_payment_schedule.grand_total / 2,
+ },
+ {
+ "due_date": add_days(today, 5),
+ "invoice_portion": 50,
+ "payment_amount": si_with_payment_schedule.grand_total / 2,
+ },
+ ],
+ )
si_with_payment_schedule.submit()
-
for invoice in (si, si_with_payment_schedule):
invoice.db_set("status", "Unpaid")
update_invoice_status()
@@ -2402,24 +2919,27 @@ class TestSalesInvoice(unittest.TestCase):
invoice.reload()
self.assertEqual(invoice.status, "Overdue and Discounted")
-
def test_sales_commission(self):
si = frappe.copy_doc(test_records[2])
- frappe.db.set_value('Item', si.get('items')[0].item_code, 'grant_commission', 1)
- frappe.db.set_value('Item', si.get('items')[1].item_code, 'grant_commission', 0)
+ frappe.db.set_value("Item", si.get("items")[0].item_code, "grant_commission", 1)
+ frappe.db.set_value("Item", si.get("items")[1].item_code, "grant_commission", 0)
- item = copy.deepcopy(si.get('items')[0])
- item.update({
- "qty": 1,
- "rate": 500,
- })
+ item = copy.deepcopy(si.get("items")[0])
+ item.update(
+ {
+ "qty": 1,
+ "rate": 500,
+ }
+ )
- item = copy.deepcopy(si.get('items')[1])
- item.update({
- "qty": 1,
- "rate": 500,
- })
+ item = copy.deepcopy(si.get("items")[1])
+ item.update(
+ {
+ "qty": 1,
+ "rate": 500,
+ }
+ )
# Test valid values
for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)):
@@ -2435,7 +2955,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertRaises(frappe.ValidationError, si.save)
def test_sales_invoice_submission_post_account_freezing_date(self):
- frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1))
+ frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", add_days(getdate(), 1))
si = create_sales_invoice(do_not_save=True)
si.posting_date = add_days(getdate(), 1)
si.save()
@@ -2444,17 +2964,19 @@ class TestSalesInvoice(unittest.TestCase):
si.posting_date = getdate()
si.submit()
- frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
+ frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None)
def test_over_billing_case_against_delivery_note(self):
- '''
- Test a case where duplicating the item with qty = 1 in the invoice
- allows overbilling even if it is disabled
- '''
+ """
+ Test a case where duplicating the item with qty = 1 in the invoice
+ allows overbilling even if it is disabled
+ """
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
- over_billing_allowance = frappe.db.get_single_value('Accounts Settings', 'over_billing_allowance')
- frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', 0)
+ over_billing_allowance = frappe.db.get_single_value(
+ "Accounts Settings", "over_billing_allowance"
+ )
+ frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0)
dn = create_delivery_note()
dn.submit()
@@ -2462,7 +2984,7 @@ class TestSalesInvoice(unittest.TestCase):
si = make_sales_invoice(dn.name)
# make a copy of first item and add it to invoice
item_copy = frappe.copy_doc(si.items[0])
- si.append('items', item_copy)
+ si.append("items", item_copy)
si.save()
with self.assertRaises(frappe.ValidationError) as err:
@@ -2470,13 +2992,16 @@ class TestSalesInvoice(unittest.TestCase):
self.assertTrue("cannot overbill" in str(err.exception).lower())
- frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance)
+ frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", over_billing_allowance)
def test_multi_currency_deferred_revenue_via_journal_entry(self):
- deferred_account = create_account(account_name="Deferred Revenue",
- parent_account="Current Liabilities - _TC", company="_Test Company")
+ deferred_account = create_account(
+ account_name="Deferred Revenue",
+ parent_account="Current Liabilities - _TC",
+ company="_Test Company",
+ )
- acc_settings = frappe.get_single('Accounts Settings')
+ acc_settings = frappe.get_single("Accounts Settings")
acc_settings.book_deferred_entries_via_journal_entry = 1
acc_settings.submit_journal_entries = 1
acc_settings.save()
@@ -2486,12 +3011,19 @@ class TestSalesInvoice(unittest.TestCase):
item.deferred_revenue_account = deferred_account
item.save()
- si = create_sales_invoice(customer='_Test Customer USD', currency='USD',
- item=item.name, qty=1, rate=100, conversion_rate=60, do_not_save=True)
+ si = create_sales_invoice(
+ customer="_Test Customer USD",
+ currency="USD",
+ item=item.name,
+ qty=1,
+ rate=100,
+ conversion_rate=60,
+ do_not_save=True,
+ )
si.set_posting_time = 1
- si.posting_date = '2019-01-01'
- si.debit_to = '_Test Receivable USD - _TC'
+ si.posting_date = "2019-01-01"
+ si.debit_to = "_Test Receivable USD - _TC"
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-01"
si.items[0].service_end_date = "2019-03-30"
@@ -2499,16 +3031,18 @@ class TestSalesInvoice(unittest.TestCase):
si.save()
si.submit()
- frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
+ frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", getdate("2019-01-31"))
- pda1 = frappe.get_doc(dict(
- doctype='Process Deferred Accounting',
- posting_date=nowdate(),
- start_date="2019-01-01",
- end_date="2019-03-31",
- type="Income",
- company="_Test Company"
- ))
+ pda1 = frappe.get_doc(
+ dict(
+ doctype="Process Deferred Accounting",
+ posting_date=nowdate(),
+ start_date="2019-01-01",
+ end_date="2019-03-31",
+ type="Income",
+ company="_Test Company",
+ )
+ )
pda1.insert()
pda1.submit()
@@ -2519,13 +3053,17 @@ class TestSalesInvoice(unittest.TestCase):
["Sales - _TC", 0.0, 1887.64, "2019-02-28"],
[deferred_account, 1887.64, 0.0, "2019-02-28"],
["Sales - _TC", 0.0, 2022.47, "2019-03-15"],
- [deferred_account, 2022.47, 0.0, "2019-03-15"]
+ [deferred_account, 2022.47, 0.0, "2019-03-15"],
]
- gl_entries = gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
+ gl_entries = gl_entries = frappe.db.sql(
+ """select account, debit, credit, posting_date
from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s
- order by posting_date asc, account asc""", (si.items[0].name, si.posting_date), as_dict=1)
+ order by posting_date asc, account asc""",
+ (si.items[0].name, si.posting_date),
+ as_dict=1,
+ )
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
@@ -2533,125 +3071,196 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(expected_gle[i][2], gle.debit)
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
- acc_settings = frappe.get_single('Accounts Settings')
+ acc_settings = frappe.get_single("Accounts Settings")
acc_settings.book_deferred_entries_via_journal_entry = 0
- acc_settings.submit_journal_entriessubmit_journal_entries = 0
+ acc_settings.submit_journal_entries = 0
acc_settings.save()
- frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
+ frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None)
+
+ def test_standalone_serial_no_return(self):
+ si = create_sales_invoice(
+ item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1
+ )
+ si.reload()
+ self.assertTrue(si.items[0].serial_no)
+
+ def test_gain_loss_with_advance_entry(self):
+ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
+
+ unlink_enabled = frappe.db.get_value(
+ "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
+ )
+
+ frappe.db.set_value(
+ "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1
+ )
+
+ jv = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False)
+
+ jv.accounts[0].exchange_rate = 70
+ jv.accounts[0].credit_in_account_currency = 100
+ jv.accounts[0].party_type = "Customer"
+ jv.accounts[0].party = "_Test Customer USD"
+
+ jv.save()
+ jv.submit()
+
+ si = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=75,
+ do_not_save=1,
+ rate=100,
+ )
+
+ si.append(
+ "advances",
+ {
+ "reference_type": "Journal Entry",
+ "reference_name": jv.name,
+ "reference_row": jv.accounts[0].name,
+ "advance_amount": 100,
+ "allocated_amount": 100,
+ "ref_exchange_rate": 70,
+ },
+ )
+ si.save()
+ si.submit()
+
+ expected_gle = [
+ ["_Test Receivable USD - _TC", 7500.0, 500],
+ ["Exchange Gain/Loss - _TC", 500.0, 0.0],
+ ["Sales - _TC", 0.0, 7500.0],
+ ]
+
+ check_gl_entries(self, si.name, expected_gle, nowdate())
+
+ frappe.db.set_value(
+ "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
+ )
+
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
- si.naming_series = 'INV-2020-.#####'
+ si.naming_series = "INV-2020-.#####"
si.items = []
- si.append("items", {
- "item_code": "_Test Item",
- "uom": "Nos",
- "warehouse": "_Test Warehouse - _TC",
- "qty": 2000,
- "rate": 12,
- "income_account": "Sales - _TC",
- "expense_account": "Cost of Goods Sold - _TC",
- "cost_center": "_Test Cost Center - _TC",
- })
+ si.append(
+ "items",
+ {
+ "item_code": "_Test Item",
+ "uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 2000,
+ "rate": 12,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ )
- si.append("items", {
- "item_code": "_Test Item 2",
- "uom": "Nos",
- "warehouse": "_Test Warehouse - _TC",
- "qty": 420,
- "rate": 15,
- "income_account": "Sales - _TC",
- "expense_account": "Cost of Goods Sold - _TC",
- "cost_center": "_Test Cost Center - _TC",
- })
+ si.append(
+ "items",
+ {
+ "item_code": "_Test Item 2",
+ "uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 420,
+ "rate": 15,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ )
return si
def make_test_address_for_ewaybill():
- if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
- address = frappe.get_doc({
- "address_line1": "_Test Address Line 1",
- "address_title": "_Test Address for Eway bill",
- "address_type": "Billing",
- "city": "_Test City",
- "state": "Test State",
- "country": "India",
- "doctype": "Address",
- "is_primary_address": 1,
- "phone": "+910000000000",
- "gstin": "27AAECE4835E1ZR",
- "gst_state": "Maharashtra",
- "gst_state_number": "27",
- "pincode": "401108"
- }).insert()
+ if not frappe.db.exists("Address", "_Test Address for Eway bill-Billing"):
+ address = frappe.get_doc(
+ {
+ "address_line1": "_Test Address Line 1",
+ "address_title": "_Test Address for Eway bill",
+ "address_type": "Billing",
+ "city": "_Test City",
+ "state": "Test State",
+ "country": "India",
+ "doctype": "Address",
+ "is_primary_address": 1,
+ "phone": "+910000000000",
+ "gstin": "27AAECE4835E1ZR",
+ "gst_state": "Maharashtra",
+ "gst_state_number": "27",
+ "pincode": "401108",
+ }
+ ).insert()
- address.append("links", {
- "link_doctype": "Company",
- "link_name": "_Test Company"
- })
+ address.append("links", {"link_doctype": "Company", "link_name": "_Test Company"})
address.save()
- if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
- address = frappe.get_doc({
- "address_line1": "_Test Address Line 1",
- "address_title": "_Test Customer-Address for Eway bill",
- "address_type": "Shipping",
- "city": "_Test City",
- "state": "Test State",
- "country": "India",
- "doctype": "Address",
- "is_primary_address": 1,
- "phone": "+910000000000",
- "gstin": "27AACCM7806M1Z3",
- "gst_state": "Maharashtra",
- "gst_state_number": "27",
- "pincode": "410038"
- }).insert()
+ if not frappe.db.exists("Address", "_Test Customer-Address for Eway bill-Shipping"):
+ address = frappe.get_doc(
+ {
+ "address_line1": "_Test Address Line 1",
+ "address_title": "_Test Customer-Address for Eway bill",
+ "address_type": "Shipping",
+ "city": "_Test City",
+ "state": "Test State",
+ "country": "India",
+ "doctype": "Address",
+ "is_primary_address": 1,
+ "phone": "+910000000000",
+ "gstin": "27AACCM7806M1Z3",
+ "gst_state": "Maharashtra",
+ "gst_state_number": "27",
+ "pincode": "410038",
+ }
+ ).insert()
- address.append("links", {
- "link_doctype": "Customer",
- "link_name": "_Test Customer"
- })
+ address.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"})
address.save()
- if not frappe.db.exists('Address', '_Test Dispatch-Address for Eway bill-Shipping'):
- address = frappe.get_doc({
- "address_line1": "_Test Dispatch Address Line 1",
- "address_title": "_Test Dispatch-Address for Eway bill",
- "address_type": "Shipping",
- "city": "_Test City",
- "state": "Test State",
- "country": "India",
- "doctype": "Address",
- "is_primary_address": 0,
- "phone": "+910000000000",
- "gstin": "07AAACC1206D1ZI",
- "gst_state": "Delhi",
- "gst_state_number": "07",
- "pincode": "1100101"
- }).insert()
+ if not frappe.db.exists("Address", "_Test Dispatch-Address for Eway bill-Shipping"):
+ address = frappe.get_doc(
+ {
+ "address_line1": "_Test Dispatch Address Line 1",
+ "address_title": "_Test Dispatch-Address for Eway bill",
+ "address_type": "Shipping",
+ "city": "_Test City",
+ "state": "Test State",
+ "country": "India",
+ "doctype": "Address",
+ "is_primary_address": 0,
+ "phone": "+910000000000",
+ "gstin": "07AAACC1206D1ZI",
+ "gst_state": "Delhi",
+ "gst_state_number": "07",
+ "pincode": "1100101",
+ }
+ ).insert()
- address.append("links", {
- "link_doctype": "Company",
- "link_name": "_Test Company"
- })
+ address.append("links", {"link_doctype": "Company", "link_name": "_Test Company"})
address.save()
+
def make_test_transporter_for_ewaybill():
- if not frappe.db.exists('Supplier', '_Test Transporter'):
- frappe.get_doc({
- "doctype": "Supplier",
- "supplier_name": "_Test Transporter",
- "country": "India",
- "supplier_group": "_Test Supplier Group",
- "supplier_type": "Company",
- "is_transporter": 1
- }).insert()
+ if not frappe.db.exists("Supplier", "_Test Transporter"):
+ frappe.get_doc(
+ {
+ "doctype": "Supplier",
+ "supplier_name": "_Test Transporter",
+ "country": "India",
+ "supplier_group": "_Test Supplier Group",
+ "supplier_type": "Company",
+ "is_transporter": 1,
+ }
+ ).insert()
+
def make_sales_invoice_for_ewaybill():
make_test_address_for_ewaybill()
@@ -2662,20 +3271,23 @@ def make_sales_invoice_for_ewaybill():
gst_account = frappe.get_all(
"GST Account",
fields=["cgst_account", "sgst_account", "igst_account"],
- filters = {"company": "_Test Company"}
+ filters={"company": "_Test Company"},
)
if not gst_account:
- gst_settings.append("gst_accounts", {
- "company": "_Test Company",
- "cgst_account": "Output Tax CGST - _TC",
- "sgst_account": "Output Tax SGST - _TC",
- "igst_account": "Output Tax IGST - _TC",
- })
+ gst_settings.append(
+ "gst_accounts",
+ {
+ "company": "_Test Company",
+ "cgst_account": "Output Tax CGST - _TC",
+ "sgst_account": "Output Tax SGST - _TC",
+ "igst_account": "Output Tax IGST - _TC",
+ },
+ )
gst_settings.save()
- si = create_sales_invoice(do_not_save=1, rate='60000')
+ si = create_sales_invoice(do_not_save=1, rate="60000")
si.distance = 2000
si.company_address = "_Test Address for Eway bill-Billing"
@@ -2683,32 +3295,43 @@ def make_sales_invoice_for_ewaybill():
si.dispatch_address_name = "_Test Dispatch-Address for Eway bill-Shipping"
si.vehicle_no = "KA12KA1234"
si.gst_category = "Registered Regular"
- si.mode_of_transport = 'Road'
- si.transporter = '_Test Transporter'
+ si.mode_of_transport = "Road"
+ si.transporter = "_Test Transporter"
- si.append("taxes", {
- "charge_type": "On Net Total",
- "account_head": "Output Tax CGST - _TC",
- "cost_center": "Main - _TC",
- "description": "CGST @ 9.0",
- "rate": 9
- })
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "Output Tax CGST - _TC",
+ "cost_center": "Main - _TC",
+ "description": "CGST @ 9.0",
+ "rate": 9,
+ },
+ )
- si.append("taxes", {
- "charge_type": "On Net Total",
- "account_head": "Output Tax SGST - _TC",
- "cost_center": "Main - _TC",
- "description": "SGST @ 9.0",
- "rate": 9
- })
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "Output Tax SGST - _TC",
+ "cost_center": "Main - _TC",
+ "description": "SGST @ 9.0",
+ "rate": 9,
+ },
+ )
return si
+
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
- gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit, posting_date
from `tabGL Entry`
where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s
- order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1)
+ order by posting_date asc, account asc""",
+ (voucher_no, posting_date),
+ as_dict=1,
+ )
for i, gle in enumerate(gl_entries):
doc.assertEqual(expected_gle[i][0], gle.account)
@@ -2716,6 +3339,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
doc.assertEqual(expected_gle[i][2], gle.credit)
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
+
def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice")
args = frappe._dict(args)
@@ -2730,32 +3354,35 @@ def create_sales_invoice(**args):
si.is_pos = args.is_pos
si.is_return = args.is_return
si.return_against = args.return_against
- si.currency=args.currency or "INR"
+ si.currency = args.currency or "INR"
si.conversion_rate = args.conversion_rate or 1
si.naming_series = args.naming_series or "T-SINV-"
si.cost_center = args.parent_cost_center
- si.append("items", {
- "item_code": args.item or args.item_code or "_Test Item",
- "item_name": args.item_name or "_Test Item",
- "description": args.description or "_Test Item",
- "gst_hsn_code": "999800",
- "warehouse": args.warehouse or "_Test Warehouse - _TC",
- "qty": args.qty or 1,
- "uom": args.uom or "Nos",
- "stock_uom": args.uom or "Nos",
- "rate": args.rate if args.get("rate") is not None else 100,
- "price_list_rate": args.price_list_rate if args.get("price_list_rate") is not None else 100,
- "income_account": args.income_account or "Sales - _TC",
- "expense_account": args.expense_account or "Cost of Goods Sold - _TC",
- "asset": args.asset or None,
- "discount_account": args.discount_account or None,
- "discount_amount": args.discount_amount or 0,
- "cost_center": args.cost_center or "_Test Cost Center - _TC",
- "serial_no": args.serial_no,
- "conversion_factor": 1,
- "incoming_rate": args.incoming_rate or 0
- })
+ si.append(
+ "items",
+ {
+ "item_code": args.item or args.item_code or "_Test Item",
+ "item_name": args.item_name or "_Test Item",
+ "description": args.description or "_Test Item",
+ "gst_hsn_code": "999800",
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "qty": args.qty or 1,
+ "uom": args.uom or "Nos",
+ "stock_uom": args.uom or "Nos",
+ "rate": args.rate if args.get("rate") is not None else 100,
+ "price_list_rate": args.price_list_rate if args.get("price_list_rate") is not None else 100,
+ "income_account": args.income_account or "Sales - _TC",
+ "expense_account": args.expense_account or "Cost of Goods Sold - _TC",
+ "asset": args.asset or None,
+ "discount_account": args.discount_account or None,
+ "discount_amount": args.discount_amount or 0,
+ "cost_center": args.cost_center or "_Test Cost Center - _TC",
+ "serial_no": args.serial_no,
+ "conversion_factor": 1,
+ "incoming_rate": args.incoming_rate or 0,
+ },
+ )
if not args.do_not_save:
si.insert()
@@ -2768,6 +3395,7 @@ def create_sales_invoice(**args):
return si
+
def create_sales_invoice_against_cost_center(**args):
si = frappe.new_doc("Sales Invoice")
args = frappe._dict(args)
@@ -2783,20 +3411,23 @@ def create_sales_invoice_against_cost_center(**args):
si.is_pos = args.is_pos
si.is_return = args.is_return
si.return_against = args.return_against
- si.currency=args.currency or "INR"
+ si.currency = args.currency or "INR"
si.conversion_rate = args.conversion_rate or 1
- si.append("items", {
- "item_code": args.item or args.item_code or "_Test Item",
- "gst_hsn_code": "999800",
- "warehouse": args.warehouse or "_Test Warehouse - _TC",
- "qty": args.qty or 1,
- "rate": args.rate or 100,
- "income_account": "Sales - _TC",
- "expense_account": "Cost of Goods Sold - _TC",
- "cost_center": args.cost_center or "_Test Cost Center - _TC",
- "serial_no": args.serial_no
- })
+ si.append(
+ "items",
+ {
+ "item_code": args.item or args.item_code or "_Test Item",
+ "gst_hsn_code": "999800",
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "qty": args.qty or 1,
+ "rate": args.rate or 100,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": args.cost_center or "_Test Cost Center - _TC",
+ "serial_no": args.serial_no,
+ },
+ )
if not args.do_not_save:
si.insert()
@@ -2811,59 +3442,69 @@ def create_sales_invoice_against_cost_center(**args):
test_dependencies = ["Journal Entry", "Contact", "Address"]
-test_records = frappe.get_test_records('Sales Invoice')
+test_records = frappe.get_test_records("Sales Invoice")
+
def get_outstanding_amount(against_voucher_type, against_voucher, account, party, party_type):
- bal = flt(frappe.db.sql("""
+ bal = flt(
+ frappe.db.sql(
+ """
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry`
where against_voucher_type=%s and against_voucher=%s
and account = %s and party = %s and party_type = %s""",
- (against_voucher_type, against_voucher, account, party, party_type))[0][0] or 0.0)
+ (against_voucher_type, against_voucher, account, party, party_type),
+ )[0][0]
+ or 0.0
+ )
- if against_voucher_type == 'Purchase Invoice':
+ if against_voucher_type == "Purchase Invoice":
bal = bal * -1
return bal
+
def get_taxes_and_charges():
- return [{
- "account_head": "_Test Account Excise Duty - TCP1",
- "charge_type": "On Net Total",
- "cost_center": "Main - TCP1",
- "description": "Excise Duty",
- "doctype": "Sales Taxes and Charges",
- "idx": 1,
- "included_in_print_rate": 1,
- "parentfield": "taxes",
- "rate": 12
- },
- {
- "account_head": "_Test Account Education Cess - TCP1",
- "charge_type": "On Previous Row Amount",
- "cost_center": "Main - TCP1",
- "description": "Education Cess",
- "doctype": "Sales Taxes and Charges",
- "idx": 2,
- "included_in_print_rate": 1,
- "parentfield": "taxes",
- "rate": 2,
- "row_id": 1
- }]
+ return [
+ {
+ "account_head": "_Test Account Excise Duty - TCP1",
+ "charge_type": "On Net Total",
+ "cost_center": "Main - TCP1",
+ "description": "Excise Duty",
+ "doctype": "Sales Taxes and Charges",
+ "idx": 1,
+ "included_in_print_rate": 1,
+ "parentfield": "taxes",
+ "rate": 12,
+ },
+ {
+ "account_head": "_Test Account Education Cess - TCP1",
+ "charge_type": "On Previous Row Amount",
+ "cost_center": "Main - TCP1",
+ "description": "Education Cess",
+ "doctype": "Sales Taxes and Charges",
+ "idx": 2,
+ "included_in_print_rate": 1,
+ "parentfield": "taxes",
+ "rate": 2,
+ "row_id": 1,
+ },
+ ]
+
def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with):
if not frappe.db.exists("Supplier", supplier_name):
- supplier = frappe.get_doc({
- "supplier_group": "_Test Supplier Group",
- "supplier_name": supplier_name,
- "doctype": "Supplier",
- "is_internal_supplier": 1,
- "represents_company": represents_company
- })
+ supplier = frappe.get_doc(
+ {
+ "supplier_group": "_Test Supplier Group",
+ "supplier_name": supplier_name,
+ "doctype": "Supplier",
+ "is_internal_supplier": 1,
+ "represents_company": represents_company,
+ }
+ )
- supplier.append("companies", {
- "company": allowed_to_interact_with
- })
+ supplier.append("companies", {"company": allowed_to_interact_with})
supplier.insert()
supplier_name = supplier.name
@@ -2872,11 +3513,15 @@ def create_internal_supplier(supplier_name, represents_company, allowed_to_inter
return supplier_name
+
def add_taxes(doc):
- doc.append('taxes', {
- 'account_head': '_Test Account Excise Duty - TCP1',
- "charge_type": "On Net Total",
- "cost_center": "Main - TCP1",
- "description": "Excise Duty",
- "rate": 12
- })
+ doc.append(
+ "taxes",
+ {
+ "account_head": "_Test Account Excise Duty - TCP1",
+ "charge_type": "On Net Total",
+ "cost_center": "Main - TCP1",
+ "description": "Excise Duty",
+ "rate": 12,
+ },
+ )
diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
index 8043a1b66f2..d9009bae4c0 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
@@ -21,13 +21,14 @@ class SalesTaxesandChargesTemplate(Document):
def autoname(self):
if self.company and self.title:
- abbr = frappe.get_cached_value('Company', self.company, 'abbr')
- self.name = '{0} - {1}'.format(self.title, abbr)
+ abbr = frappe.get_cached_value("Company", self.company, "abbr")
+ self.name = "{0} - {1}".format(self.title, abbr)
def set_missing_values(self):
for data in self.taxes:
- if data.charge_type == 'On Net Total' and flt(data.rate) == 0.0:
- data.rate = frappe.db.get_value('Account', data.account_head, 'tax_rate')
+ if data.charge_type == "On Net Total" and flt(data.rate) == 0.0:
+ data.rate = frappe.db.get_value("Account", data.account_head, "tax_rate")
+
def valdiate_taxes_and_charges_template(doc):
# default should not be disabled
@@ -35,9 +36,13 @@ def valdiate_taxes_and_charges_template(doc):
# doc.is_default = 1
if doc.is_default == 1:
- frappe.db.sql("""update `tab{0}` set is_default = 0
- where is_default = 1 and name != %s and company = %s""".format(doc.doctype),
- (doc.name, doc.company))
+ frappe.db.sql(
+ """update `tab{0}` set is_default = 0
+ where is_default = 1 and name != %s and company = %s""".format(
+ doc.doctype
+ ),
+ (doc.name, doc.company),
+ )
validate_disabled(doc)
@@ -50,13 +55,27 @@ def valdiate_taxes_and_charges_template(doc):
validate_cost_center(tax, doc)
validate_inclusive_tax(tax, doc)
+
def validate_disabled(doc):
if doc.is_default and doc.disabled:
frappe.throw(_("Disabled template must not be default template"))
+
def validate_for_tax_category(doc):
if not doc.tax_category:
return
- if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}):
- frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category)))
+ if frappe.db.exists(
+ doc.doctype,
+ {
+ "company": doc.company,
+ "tax_category": doc.tax_category,
+ "disabled": 0,
+ "name": ["!=", doc.name],
+ },
+ ):
+ frappe.throw(
+ _(
+ "A template with tax category {0} already exists. Only one template is allowed with each tax category"
+ ).format(frappe.bold(doc.tax_category))
+ )
diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py
index 5b9fbafed8d..6432acaae93 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py
@@ -1,23 +1,16 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'taxes_and_charges',
- 'non_standard_fieldnames': {
- 'Tax Rule': 'sales_tax_template',
- 'Subscription': 'sales_tax_template',
- 'Restaurant': 'default_tax_template'
+ "fieldname": "taxes_and_charges",
+ "non_standard_fieldnames": {
+ "Tax Rule": "sales_tax_template",
+ "Subscription": "sales_tax_template",
+ "Restaurant": "default_tax_template",
},
- 'transactions': [
- {
- 'label': _('Transactions'),
- 'items': ['Sales Invoice', 'Sales Order', 'Delivery Note']
- },
- {
- 'label': _('References'),
- 'items': ['POS Profile', 'Subscription', 'Restaurant', 'Tax Rule']
- }
- ]
+ "transactions": [
+ {"label": _("Transactions"), "items": ["Sales Invoice", "Sales Order", "Delivery Note"]},
+ {"label": _("References"), "items": ["POS Profile", "Subscription", "Restaurant", "Tax Rule"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.py
index 7b13c6c6925..972b773501a 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.py
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.py
@@ -5,7 +5,8 @@ import unittest
import frappe
-test_records = frappe.get_test_records('Sales Taxes and Charges Template')
+test_records = frappe.get_test_records("Sales Taxes and Charges Template")
+
class TestSalesTaxesandChargesTemplate(unittest.TestCase):
pass
diff --git a/erpnext/accounts/doctype/share_transfer/share_transfer.py b/erpnext/accounts/doctype/share_transfer/share_transfer.py
index b543ad8204d..4f49843c1eb 100644
--- a/erpnext/accounts/doctype/share_transfer/share_transfer.py
+++ b/erpnext/accounts/doctype/share_transfer/share_transfer.py
@@ -10,95 +10,115 @@ from frappe.model.naming import make_autoname
from frappe.utils import nowdate
-class ShareDontExists(ValidationError): pass
+class ShareDontExists(ValidationError):
+ pass
+
class ShareTransfer(Document):
def on_submit(self):
- if self.transfer_type == 'Issue':
+ if self.transfer_type == "Issue":
shareholder = self.get_company_shareholder()
- shareholder.append('share_balance', {
- 'share_type': self.share_type,
- 'from_no': self.from_no,
- 'to_no': self.to_no,
- 'rate': self.rate,
- 'amount': self.amount,
- 'no_of_shares': self.no_of_shares,
- 'is_company': 1,
- 'current_state': 'Issued'
- })
+ shareholder.append(
+ "share_balance",
+ {
+ "share_type": self.share_type,
+ "from_no": self.from_no,
+ "to_no": self.to_no,
+ "rate": self.rate,
+ "amount": self.amount,
+ "no_of_shares": self.no_of_shares,
+ "is_company": 1,
+ "current_state": "Issued",
+ },
+ )
shareholder.save()
doc = self.get_shareholder_doc(self.to_shareholder)
- doc.append('share_balance', {
- 'share_type': self.share_type,
- 'from_no': self.from_no,
- 'to_no': self.to_no,
- 'rate': self.rate,
- 'amount': self.amount,
- 'no_of_shares': self.no_of_shares
- })
+ doc.append(
+ "share_balance",
+ {
+ "share_type": self.share_type,
+ "from_no": self.from_no,
+ "to_no": self.to_no,
+ "rate": self.rate,
+ "amount": self.amount,
+ "no_of_shares": self.no_of_shares,
+ },
+ )
doc.save()
- elif self.transfer_type == 'Purchase':
+ elif self.transfer_type == "Purchase":
self.remove_shares(self.from_shareholder)
self.remove_shares(self.get_company_shareholder().name)
- elif self.transfer_type == 'Transfer':
+ elif self.transfer_type == "Transfer":
self.remove_shares(self.from_shareholder)
doc = self.get_shareholder_doc(self.to_shareholder)
- doc.append('share_balance', {
- 'share_type': self.share_type,
- 'from_no': self.from_no,
- 'to_no': self.to_no,
- 'rate': self.rate,
- 'amount': self.amount,
- 'no_of_shares': self.no_of_shares
- })
+ doc.append(
+ "share_balance",
+ {
+ "share_type": self.share_type,
+ "from_no": self.from_no,
+ "to_no": self.to_no,
+ "rate": self.rate,
+ "amount": self.amount,
+ "no_of_shares": self.no_of_shares,
+ },
+ )
doc.save()
def on_cancel(self):
- if self.transfer_type == 'Issue':
+ if self.transfer_type == "Issue":
compnay_shareholder = self.get_company_shareholder()
self.remove_shares(compnay_shareholder.name)
self.remove_shares(self.to_shareholder)
- elif self.transfer_type == 'Purchase':
+ elif self.transfer_type == "Purchase":
compnay_shareholder = self.get_company_shareholder()
from_shareholder = self.get_shareholder_doc(self.from_shareholder)
- from_shareholder.append('share_balance', {
- 'share_type': self.share_type,
- 'from_no': self.from_no,
- 'to_no': self.to_no,
- 'rate': self.rate,
- 'amount': self.amount,
- 'no_of_shares': self.no_of_shares
- })
+ from_shareholder.append(
+ "share_balance",
+ {
+ "share_type": self.share_type,
+ "from_no": self.from_no,
+ "to_no": self.to_no,
+ "rate": self.rate,
+ "amount": self.amount,
+ "no_of_shares": self.no_of_shares,
+ },
+ )
from_shareholder.save()
- compnay_shareholder.append('share_balance', {
- 'share_type': self.share_type,
- 'from_no': self.from_no,
- 'to_no': self.to_no,
- 'rate': self.rate,
- 'amount': self.amount,
- 'no_of_shares': self.no_of_shares
- })
+ compnay_shareholder.append(
+ "share_balance",
+ {
+ "share_type": self.share_type,
+ "from_no": self.from_no,
+ "to_no": self.to_no,
+ "rate": self.rate,
+ "amount": self.amount,
+ "no_of_shares": self.no_of_shares,
+ },
+ )
compnay_shareholder.save()
- elif self.transfer_type == 'Transfer':
+ elif self.transfer_type == "Transfer":
self.remove_shares(self.to_shareholder)
from_shareholder = self.get_shareholder_doc(self.from_shareholder)
- from_shareholder.append('share_balance', {
- 'share_type': self.share_type,
- 'from_no': self.from_no,
- 'to_no': self.to_no,
- 'rate': self.rate,
- 'amount': self.amount,
- 'no_of_shares': self.no_of_shares
- })
+ from_shareholder.append(
+ "share_balance",
+ {
+ "share_type": self.share_type,
+ "from_no": self.from_no,
+ "to_no": self.to_no,
+ "rate": self.rate,
+ "amount": self.amount,
+ "no_of_shares": self.no_of_shares,
+ },
+ )
from_shareholder.save()
def validate(self):
@@ -106,90 +126,96 @@ class ShareTransfer(Document):
self.basic_validations()
self.folio_no_validation()
- if self.transfer_type == 'Issue':
+ if self.transfer_type == "Issue":
# validate share doesn't exist in company
ret_val = self.share_exists(self.get_company_shareholder().name)
- if ret_val in ('Complete', 'Partial'):
- frappe.throw(_('The shares already exist'), frappe.DuplicateEntryError)
+ if ret_val in ("Complete", "Partial"):
+ frappe.throw(_("The shares already exist"), frappe.DuplicateEntryError)
else:
# validate share exists with from_shareholder
ret_val = self.share_exists(self.from_shareholder)
- if ret_val in ('Outside', 'Partial'):
- frappe.throw(_("The shares don't exist with the {0}")
- .format(self.from_shareholder), ShareDontExists)
+ if ret_val in ("Outside", "Partial"):
+ frappe.throw(
+ _("The shares don't exist with the {0}").format(self.from_shareholder), ShareDontExists
+ )
def basic_validations(self):
- if self.transfer_type == 'Purchase':
- self.to_shareholder = ''
+ if self.transfer_type == "Purchase":
+ self.to_shareholder = ""
if not self.from_shareholder:
- frappe.throw(_('The field From Shareholder cannot be blank'))
+ frappe.throw(_("The field From Shareholder cannot be blank"))
if not self.from_folio_no:
self.to_folio_no = self.autoname_folio(self.to_shareholder)
if not self.asset_account:
- frappe.throw(_('The field Asset Account cannot be blank'))
- elif (self.transfer_type == 'Issue'):
- self.from_shareholder = ''
+ frappe.throw(_("The field Asset Account cannot be blank"))
+ elif self.transfer_type == "Issue":
+ self.from_shareholder = ""
if not self.to_shareholder:
- frappe.throw(_('The field To Shareholder cannot be blank'))
+ frappe.throw(_("The field To Shareholder cannot be blank"))
if not self.to_folio_no:
self.to_folio_no = self.autoname_folio(self.to_shareholder)
if not self.asset_account:
- frappe.throw(_('The field Asset Account cannot be blank'))
+ frappe.throw(_("The field Asset Account cannot be blank"))
else:
if not self.from_shareholder or not self.to_shareholder:
- frappe.throw(_('The fields From Shareholder and To Shareholder cannot be blank'))
+ frappe.throw(_("The fields From Shareholder and To Shareholder cannot be blank"))
if not self.to_folio_no:
self.to_folio_no = self.autoname_folio(self.to_shareholder)
if not self.equity_or_liability_account:
- frappe.throw(_('The field Equity/Liability Account cannot be blank'))
+ frappe.throw(_("The field Equity/Liability Account cannot be blank"))
if self.from_shareholder == self.to_shareholder:
- frappe.throw(_('The seller and the buyer cannot be the same'))
+ frappe.throw(_("The seller and the buyer cannot be the same"))
if self.no_of_shares != self.to_no - self.from_no + 1:
- frappe.throw(_('The number of shares and the share numbers are inconsistent'))
+ frappe.throw(_("The number of shares and the share numbers are inconsistent"))
if not self.amount:
self.amount = self.rate * self.no_of_shares
if self.amount != self.rate * self.no_of_shares:
- frappe.throw(_('There are inconsistencies between the rate, no of shares and the amount calculated'))
+ frappe.throw(
+ _("There are inconsistencies between the rate, no of shares and the amount calculated")
+ )
def share_exists(self, shareholder):
doc = self.get_shareholder_doc(shareholder)
for entry in doc.share_balance:
- if entry.share_type != self.share_type or \
- entry.from_no > self.to_no or \
- entry.to_no < self.from_no:
- continue # since query lies outside bounds
- elif entry.from_no <= self.from_no and entry.to_no >= self.to_no: #both inside
- return 'Complete' # absolute truth!
+ if (
+ entry.share_type != self.share_type or entry.from_no > self.to_no or entry.to_no < self.from_no
+ ):
+ continue # since query lies outside bounds
+ elif entry.from_no <= self.from_no and entry.to_no >= self.to_no: # both inside
+ return "Complete" # absolute truth!
elif entry.from_no <= self.from_no <= self.to_no:
- return 'Partial'
+ return "Partial"
elif entry.from_no <= self.to_no <= entry.to_no:
- return 'Partial'
+ return "Partial"
- return 'Outside'
+ return "Outside"
def folio_no_validation(self):
- shareholder_fields = ['from_shareholder', 'to_shareholder']
+ shareholder_fields = ["from_shareholder", "to_shareholder"]
for shareholder_field in shareholder_fields:
shareholder_name = self.get(shareholder_field)
if not shareholder_name:
continue
doc = self.get_shareholder_doc(shareholder_name)
if doc.company != self.company:
- frappe.throw(_('The shareholder does not belong to this company'))
+ frappe.throw(_("The shareholder does not belong to this company"))
if not doc.folio_no:
- doc.folio_no = self.from_folio_no \
- if (shareholder_field == 'from_shareholder') else self.to_folio_no
+ doc.folio_no = (
+ self.from_folio_no if (shareholder_field == "from_shareholder") else self.to_folio_no
+ )
doc.save()
else:
- if doc.folio_no and doc.folio_no != (self.from_folio_no if (shareholder_field == 'from_shareholder') else self.to_folio_no):
- frappe.throw(_('The folio numbers are not matching'))
+ if doc.folio_no and doc.folio_no != (
+ self.from_folio_no if (shareholder_field == "from_shareholder") else self.to_folio_no
+ ):
+ frappe.throw(_("The folio numbers are not matching"))
def autoname_folio(self, shareholder, is_company=False):
if is_company:
doc = self.get_company_shareholder()
else:
doc = self.get_shareholder_doc(shareholder)
- doc.folio_no = make_autoname('FN.#####')
+ doc.folio_no = make_autoname("FN.#####")
doc.save()
return doc.folio_no
@@ -197,106 +223,120 @@ class ShareTransfer(Document):
# query = {'from_no': share_starting_no, 'to_no': share_ending_no}
# Shares exist for sure
# Iterate over all entries and modify entry if in entry
- doc = frappe.get_doc('Shareholder', shareholder)
+ doc = frappe.get_doc("Shareholder", shareholder)
current_entries = doc.share_balance
new_entries = []
for entry in current_entries:
# use spaceage logic here
- if entry.share_type != self.share_type or \
- entry.from_no > self.to_no or \
- entry.to_no < self.from_no:
+ if (
+ entry.share_type != self.share_type or entry.from_no > self.to_no or entry.to_no < self.from_no
+ ):
new_entries.append(entry)
- continue # since query lies outside bounds
+ continue # since query lies outside bounds
elif entry.from_no <= self.from_no and entry.to_no >= self.to_no:
- #split
+ # split
if entry.from_no == self.from_no:
if entry.to_no == self.to_no:
- pass #nothing to append
+ pass # nothing to append
else:
- new_entries.append(self.return_share_balance_entry(self.to_no+1, entry.to_no, entry.rate))
+ new_entries.append(self.return_share_balance_entry(self.to_no + 1, entry.to_no, entry.rate))
else:
if entry.to_no == self.to_no:
- new_entries.append(self.return_share_balance_entry(entry.from_no, self.from_no-1, entry.rate))
+ new_entries.append(
+ self.return_share_balance_entry(entry.from_no, self.from_no - 1, entry.rate)
+ )
else:
- new_entries.append(self.return_share_balance_entry(entry.from_no, self.from_no-1, entry.rate))
- new_entries.append(self.return_share_balance_entry(self.to_no+1, entry.to_no, entry.rate))
+ new_entries.append(
+ self.return_share_balance_entry(entry.from_no, self.from_no - 1, entry.rate)
+ )
+ new_entries.append(self.return_share_balance_entry(self.to_no + 1, entry.to_no, entry.rate))
elif entry.from_no >= self.from_no and entry.to_no <= self.to_no:
# split and check
- pass #nothing to append
+ pass # nothing to append
elif self.from_no <= entry.from_no <= self.to_no and entry.to_no >= self.to_no:
- new_entries.append(self.return_share_balance_entry(self.to_no+1, entry.to_no, entry.rate))
+ new_entries.append(self.return_share_balance_entry(self.to_no + 1, entry.to_no, entry.rate))
elif self.from_no <= entry.to_no <= self.to_no and entry.from_no <= self.from_no:
- new_entries.append(self.return_share_balance_entry(entry.from_no, self.from_no-1, entry.rate))
+ new_entries.append(
+ self.return_share_balance_entry(entry.from_no, self.from_no - 1, entry.rate)
+ )
else:
new_entries.append(entry)
doc.share_balance = []
for entry in new_entries:
- doc.append('share_balance', entry)
+ doc.append("share_balance", entry)
doc.save()
def return_share_balance_entry(self, from_no, to_no, rate):
# return an entry as a dict
return {
- 'share_type' : self.share_type,
- 'from_no' : from_no,
- 'to_no' : to_no,
- 'rate' : rate,
- 'amount' : self.rate * (to_no - from_no + 1),
- 'no_of_shares' : to_no - from_no + 1
+ "share_type": self.share_type,
+ "from_no": from_no,
+ "to_no": to_no,
+ "rate": rate,
+ "amount": self.rate * (to_no - from_no + 1),
+ "no_of_shares": to_no - from_no + 1,
}
def get_shareholder_doc(self, shareholder):
# Get Shareholder doc based on the Shareholder name
if shareholder:
- query_filters = {'name': shareholder}
+ query_filters = {"name": shareholder}
- name = frappe.db.get_value('Shareholder', {'name': shareholder}, 'name')
+ name = frappe.db.get_value("Shareholder", {"name": shareholder}, "name")
- return frappe.get_doc('Shareholder', name)
+ return frappe.get_doc("Shareholder", name)
def get_company_shareholder(self):
# Get company doc or create one if not present
- company_shareholder = frappe.db.get_value('Shareholder',
- {
- 'company': self.company,
- 'is_company': 1
- }, 'name')
+ company_shareholder = frappe.db.get_value(
+ "Shareholder", {"company": self.company, "is_company": 1}, "name"
+ )
if company_shareholder:
- return frappe.get_doc('Shareholder', company_shareholder)
+ return frappe.get_doc("Shareholder", company_shareholder)
else:
- shareholder = frappe.get_doc({
- 'doctype': 'Shareholder',
- 'title': self.company,
- 'company': self.company,
- 'is_company': 1
- })
+ shareholder = frappe.get_doc(
+ {"doctype": "Shareholder", "title": self.company, "company": self.company, "is_company": 1}
+ )
shareholder.insert()
return shareholder
+
@frappe.whitelist()
-def make_jv_entry( company, account, amount, payment_account,\
- credit_applicant_type, credit_applicant, debit_applicant_type, debit_applicant):
- journal_entry = frappe.new_doc('Journal Entry')
- journal_entry.voucher_type = 'Journal Entry'
+def make_jv_entry(
+ company,
+ account,
+ amount,
+ payment_account,
+ credit_applicant_type,
+ credit_applicant,
+ debit_applicant_type,
+ debit_applicant,
+):
+ journal_entry = frappe.new_doc("Journal Entry")
+ journal_entry.voucher_type = "Journal Entry"
journal_entry.company = company
journal_entry.posting_date = nowdate()
account_amt_list = []
- account_amt_list.append({
- "account": account,
- "debit_in_account_currency": amount,
- "party_type": debit_applicant_type,
- "party": debit_applicant,
- })
- account_amt_list.append({
- "account": payment_account,
- "credit_in_account_currency": amount,
- "party_type": credit_applicant_type,
- "party": credit_applicant,
- })
+ account_amt_list.append(
+ {
+ "account": account,
+ "debit_in_account_currency": amount,
+ "party_type": debit_applicant_type,
+ "party": debit_applicant,
+ }
+ )
+ account_amt_list.append(
+ {
+ "account": payment_account,
+ "credit_in_account_currency": amount,
+ "party_type": credit_applicant_type,
+ "party": credit_applicant,
+ }
+ )
journal_entry.set("accounts", account_amt_list)
return journal_entry.as_dict()
diff --git a/erpnext/accounts/doctype/share_transfer/test_share_transfer.py b/erpnext/accounts/doctype/share_transfer/test_share_transfer.py
index bc3a52167db..97310743605 100644
--- a/erpnext/accounts/doctype/share_transfer/test_share_transfer.py
+++ b/erpnext/accounts/doctype/share_transfer/test_share_transfer.py
@@ -9,6 +9,7 @@ from erpnext.accounts.doctype.share_transfer.share_transfer import ShareDontExis
test_dependencies = ["Share Type", "Shareholder"]
+
class TestShareTransfer(unittest.TestCase):
def setUp(self):
frappe.db.sql("delete from `tabShare Transfer`")
@@ -26,7 +27,7 @@ class TestShareTransfer(unittest.TestCase):
"rate": 10,
"company": "_Test Company",
"asset_account": "Cash - _TC",
- "equity_or_liability_account": "Creditors - _TC"
+ "equity_or_liability_account": "Creditors - _TC",
},
{
"doctype": "Share Transfer",
@@ -40,7 +41,7 @@ class TestShareTransfer(unittest.TestCase):
"no_of_shares": 100,
"rate": 15,
"company": "_Test Company",
- "equity_or_liability_account": "Creditors - _TC"
+ "equity_or_liability_account": "Creditors - _TC",
},
{
"doctype": "Share Transfer",
@@ -54,7 +55,7 @@ class TestShareTransfer(unittest.TestCase):
"no_of_shares": 300,
"rate": 20,
"company": "_Test Company",
- "equity_or_liability_account": "Creditors - _TC"
+ "equity_or_liability_account": "Creditors - _TC",
},
{
"doctype": "Share Transfer",
@@ -68,7 +69,7 @@ class TestShareTransfer(unittest.TestCase):
"no_of_shares": 200,
"rate": 15,
"company": "_Test Company",
- "equity_or_liability_account": "Creditors - _TC"
+ "equity_or_liability_account": "Creditors - _TC",
},
{
"doctype": "Share Transfer",
@@ -82,42 +83,46 @@ class TestShareTransfer(unittest.TestCase):
"rate": 25,
"company": "_Test Company",
"asset_account": "Cash - _TC",
- "equity_or_liability_account": "Creditors - _TC"
- }
+ "equity_or_liability_account": "Creditors - _TC",
+ },
]
for d in share_transfers:
st = frappe.get_doc(d)
st.submit()
def test_invalid_share_transfer(self):
- doc = frappe.get_doc({
- "doctype": "Share Transfer",
- "transfer_type": "Transfer",
- "date": "2018-01-05",
- "from_shareholder": "SH-00003",
- "to_shareholder": "SH-00002",
- "share_type": "Equity",
- "from_no": 1,
- "to_no": 100,
- "no_of_shares": 100,
- "rate": 15,
- "company": "_Test Company",
- "equity_or_liability_account": "Creditors - _TC"
- })
+ doc = frappe.get_doc(
+ {
+ "doctype": "Share Transfer",
+ "transfer_type": "Transfer",
+ "date": "2018-01-05",
+ "from_shareholder": "SH-00003",
+ "to_shareholder": "SH-00002",
+ "share_type": "Equity",
+ "from_no": 1,
+ "to_no": 100,
+ "no_of_shares": 100,
+ "rate": 15,
+ "company": "_Test Company",
+ "equity_or_liability_account": "Creditors - _TC",
+ }
+ )
self.assertRaises(ShareDontExists, doc.insert)
- doc = frappe.get_doc({
- "doctype": "Share Transfer",
- "transfer_type": "Purchase",
- "date": "2018-01-02",
- "from_shareholder": "SH-00001",
- "share_type": "Equity",
- "from_no": 1,
- "to_no": 200,
- "no_of_shares": 200,
- "rate": 15,
- "company": "_Test Company",
- "asset_account": "Cash - _TC",
- "equity_or_liability_account": "Creditors - _TC"
- })
+ doc = frappe.get_doc(
+ {
+ "doctype": "Share Transfer",
+ "transfer_type": "Purchase",
+ "date": "2018-01-02",
+ "from_shareholder": "SH-00001",
+ "share_type": "Equity",
+ "from_no": 1,
+ "to_no": 200,
+ "no_of_shares": 200,
+ "rate": 15,
+ "company": "_Test Company",
+ "asset_account": "Cash - _TC",
+ "equity_or_liability_account": "Creditors - _TC",
+ }
+ )
self.assertRaises(ShareDontExists, doc.insert)
diff --git a/erpnext/accounts/doctype/share_type/share_type_dashboard.py b/erpnext/accounts/doctype/share_type/share_type_dashboard.py
index fdb417ed6d0..19604b332a3 100644
--- a/erpnext/accounts/doctype/share_type/share_type_dashboard.py
+++ b/erpnext/accounts/doctype/share_type/share_type_dashboard.py
@@ -1,14 +1,8 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'share_type',
- 'transactions': [
- {
- 'label': _('References'),
- 'items': ['Share Transfer', 'Shareholder']
- }
- ]
+ "fieldname": "share_type",
+ "transactions": [{"label": _("References"), "items": ["Share Transfer", "Shareholder"]}],
}
diff --git a/erpnext/accounts/doctype/shareholder/shareholder.py b/erpnext/accounts/doctype/shareholder/shareholder.py
index 8a0fa85a692..b0e2493f7a6 100644
--- a/erpnext/accounts/doctype/shareholder/shareholder.py
+++ b/erpnext/accounts/doctype/shareholder/shareholder.py
@@ -15,7 +15,7 @@ class Shareholder(Document):
load_address_and_contact(self)
def on_trash(self):
- delete_contact_and_address('Shareholder', self.name)
+ delete_contact_and_address("Shareholder", self.name)
def before_save(self):
for entry in self.share_balance:
diff --git a/erpnext/accounts/doctype/shareholder/shareholder_dashboard.py b/erpnext/accounts/doctype/shareholder/shareholder_dashboard.py
index 44d5ec684fb..fa9d431c19e 100644
--- a/erpnext/accounts/doctype/shareholder/shareholder_dashboard.py
+++ b/erpnext/accounts/doctype/shareholder/shareholder_dashboard.py
@@ -1,14 +1,6 @@
-
-
def get_data():
return {
- 'fieldname': 'shareholder',
- 'non_standard_fieldnames': {
- 'Share Transfer': 'to_shareholder'
- },
- 'transactions': [
- {
- 'items': ['Share Transfer']
- }
- ]
+ "fieldname": "shareholder",
+ "non_standard_fieldnames": {"Share Transfer": "to_shareholder"},
+ "transactions": [{"items": ["Share Transfer"]}],
}
diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py
index 792e7d21a78..1d79503a05e 100644
--- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py
+++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py
@@ -12,9 +12,17 @@ from frappe.utils import flt, fmt_money
import erpnext
-class OverlappingConditionError(frappe.ValidationError): pass
-class FromGreaterThanToError(frappe.ValidationError): pass
-class ManyBlankToValuesError(frappe.ValidationError): pass
+class OverlappingConditionError(frappe.ValidationError):
+ pass
+
+
+class FromGreaterThanToError(frappe.ValidationError):
+ pass
+
+
+class ManyBlankToValuesError(frappe.ValidationError):
+ pass
+
class ShippingRule(Document):
def validate(self):
@@ -35,15 +43,19 @@ class ShippingRule(Document):
if not d.to_value:
zero_to_values.append(d)
elif d.from_value >= d.to_value:
- throw(_("From value must be less than to value in row {0}").format(d.idx), FromGreaterThanToError)
+ throw(
+ _("From value must be less than to value in row {0}").format(d.idx), FromGreaterThanToError
+ )
# check if more than two or more rows has To Value = 0
if len(zero_to_values) >= 2:
- throw(_('There can only be one Shipping Rule Condition with 0 or blank value for "To Value"'),
- ManyBlankToValuesError)
+ throw(
+ _('There can only be one Shipping Rule Condition with 0 or blank value for "To Value"'),
+ ManyBlankToValuesError,
+ )
def apply(self, doc):
- '''Apply shipping rule on given doc. Called from accounts controller'''
+ """Apply shipping rule on given doc. Called from accounts controller"""
shipping_amount = 0.0
by_value = False
@@ -52,15 +64,15 @@ class ShippingRule(Document):
# validate country only if there is address
self.validate_countries(doc)
- if self.calculate_based_on == 'Net Total':
+ if self.calculate_based_on == "Net Total":
value = doc.base_net_total
by_value = True
- elif self.calculate_based_on == 'Net Weight':
+ elif self.calculate_based_on == "Net Weight":
value = doc.total_net_weight
by_value = True
- elif self.calculate_based_on == 'Fixed':
+ elif self.calculate_based_on == "Fixed":
shipping_amount = self.shipping_amount
# shipping amount by value, apply conditions
@@ -71,12 +83,13 @@ class ShippingRule(Document):
if doc.currency != doc.company_currency:
shipping_amount = flt(shipping_amount / doc.conversion_rate, 2)
- if shipping_amount:
- self.add_shipping_rule_to_tax_table(doc, shipping_amount)
+ self.add_shipping_rule_to_tax_table(doc, shipping_amount)
def get_shipping_amount_from_rules(self, value):
for condition in self.get("conditions"):
- if not condition.to_value or (flt(condition.from_value) <= flt(value) <= flt(condition.to_value)):
+ if not condition.to_value or (
+ flt(condition.from_value) <= flt(value) <= flt(condition.to_value)
+ ):
return condition.shipping_amount
return 0.0
@@ -84,27 +97,31 @@ class ShippingRule(Document):
def validate_countries(self, doc):
# validate applicable countries
if self.countries:
- shipping_country = doc.get_shipping_address().get('country')
+ shipping_country = doc.get_shipping_address().get("country")
if not shipping_country:
- frappe.throw(_('Shipping Address does not have country, which is required for this Shipping Rule'))
+ frappe.throw(
+ _("Shipping Address does not have country, which is required for this Shipping Rule")
+ )
if shipping_country not in [d.country for d in self.countries]:
- frappe.throw(_('Shipping rule not applicable for country {0} in Shipping Address').format(shipping_country))
+ frappe.throw(
+ _("Shipping rule not applicable for country {0} in Shipping Address").format(shipping_country)
+ )
def add_shipping_rule_to_tax_table(self, doc, shipping_amount):
shipping_charge = {
"charge_type": "Actual",
"account_head": self.account,
- "cost_center": self.cost_center
+ "cost_center": self.cost_center,
}
if self.shipping_rule_type == "Selling":
# check if not applied on purchase
- if not doc.meta.get_field('taxes').options == 'Sales Taxes and Charges':
- frappe.throw(_('Shipping rule only applicable for Selling'))
+ if not doc.meta.get_field("taxes").options == "Sales Taxes and Charges":
+ frappe.throw(_("Shipping rule only applicable for Selling"))
shipping_charge["doctype"] = "Sales Taxes and Charges"
else:
# check if not applied on sales
- if not doc.meta.get_field('taxes').options == 'Purchase Taxes and Charges':
- frappe.throw(_('Shipping rule only applicable for Buying'))
+ if not doc.meta.get_field("taxes").options == "Purchase Taxes and Charges":
+ frappe.throw(_("Shipping rule only applicable for Buying"))
shipping_charge["doctype"] = "Purchase Taxes and Charges"
shipping_charge["category"] = "Valuation and Total"
@@ -128,19 +145,19 @@ class ShippingRule(Document):
def validate_overlapping_shipping_rule_conditions(self):
def overlap_exists_between(num_range1, num_range2):
"""
- num_range1 and num_range2 are two ranges
- ranges are represented as a tuple e.g. range 100 to 300 is represented as (100, 300)
- if condition num_range1 = 100 to 300
- then condition num_range2 can only be like 50 to 99 or 301 to 400
- hence, non-overlapping condition = (x1 <= x2 < y1 <= y2) or (y1 <= y2 < x1 <= x2)
+ num_range1 and num_range2 are two ranges
+ ranges are represented as a tuple e.g. range 100 to 300 is represented as (100, 300)
+ if condition num_range1 = 100 to 300
+ then condition num_range2 can only be like 50 to 99 or 301 to 400
+ hence, non-overlapping condition = (x1 <= x2 < y1 <= y2) or (y1 <= y2 < x1 <= x2)
"""
(x1, x2), (y1, y2) = num_range1, num_range2
separate = (x1 <= x2 <= y1 <= y2) or (y1 <= y2 <= x1 <= x2)
- return (not separate)
+ return not separate
overlaps = []
for i in range(0, len(self.conditions)):
- for j in range(i+1, len(self.conditions)):
+ for j in range(i + 1, len(self.conditions)):
d1, d2 = self.conditions[i], self.conditions[j]
if d1.as_dict() != d2.as_dict():
# in our case, to_value can be zero, hence pass the from_value if so
@@ -154,7 +171,12 @@ class ShippingRule(Document):
msgprint(_("Overlapping conditions found between:"))
messages = []
for d1, d2 in overlaps:
- messages.append("%s-%s = %s " % (d1.from_value, d1.to_value, fmt_money(d1.shipping_amount, currency=company_currency)) +
- _("and") + " %s-%s = %s" % (d2.from_value, d2.to_value, fmt_money(d2.shipping_amount, currency=company_currency)))
+ messages.append(
+ "%s-%s = %s "
+ % (d1.from_value, d1.to_value, fmt_money(d1.shipping_amount, currency=company_currency))
+ + _("and")
+ + " %s-%s = %s"
+ % (d2.from_value, d2.to_value, fmt_money(d2.shipping_amount, currency=company_currency))
+ )
msgprint("\n".join(messages), raise_exception=OverlappingConditionError)
diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule_dashboard.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule_dashboard.py
index ef2a053227b..60ce120c54f 100644
--- a/erpnext/accounts/doctype/shipping_rule/shipping_rule_dashboard.py
+++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule_dashboard.py
@@ -1,25 +1,13 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'shipping_rule',
- 'non_standard_fieldnames': {
- 'Payment Entry': 'party_name'
- },
- 'transactions': [
- {
- 'label': _('Pre Sales'),
- 'items': ['Quotation', 'Supplier Quotation']
- },
- {
- 'label': _('Sales'),
- 'items': ['Sales Order', 'Delivery Note', 'Sales Invoice']
- },
- {
- 'label': _('Purchase'),
- 'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt']
- }
- ]
+ "fieldname": "shipping_rule",
+ "non_standard_fieldnames": {"Payment Entry": "party_name"},
+ "transactions": [
+ {"label": _("Pre Sales"), "items": ["Quotation", "Supplier Quotation"]},
+ {"label": _("Sales"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]},
+ {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.py
index c06dae09701..a24e834c572 100644
--- a/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.py
+++ b/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.py
@@ -11,18 +11,19 @@ from erpnext.accounts.doctype.shipping_rule.shipping_rule import (
OverlappingConditionError,
)
-test_records = frappe.get_test_records('Shipping Rule')
+test_records = frappe.get_test_records("Shipping Rule")
+
class TestShippingRule(unittest.TestCase):
def test_from_greater_than_to(self):
shipping_rule = frappe.copy_doc(test_records[0])
- shipping_rule.name = test_records[0].get('name')
+ shipping_rule.name = test_records[0].get("name")
shipping_rule.get("conditions")[0].from_value = 101
self.assertRaises(FromGreaterThanToError, shipping_rule.insert)
def test_many_zero_to_values(self):
shipping_rule = frappe.copy_doc(test_records[0])
- shipping_rule.name = test_records[0].get('name')
+ shipping_rule.name = test_records[0].get("name")
shipping_rule.get("conditions")[0].to_value = 0
self.assertRaises(ManyBlankToValuesError, shipping_rule.insert)
@@ -35,48 +36,58 @@ class TestShippingRule(unittest.TestCase):
((50, 150), (50, 150)),
]:
shipping_rule = frappe.copy_doc(test_records[0])
- shipping_rule.name = test_records[0].get('name')
+ shipping_rule.name = test_records[0].get("name")
shipping_rule.get("conditions")[0].from_value = range_a[0]
shipping_rule.get("conditions")[0].to_value = range_a[1]
shipping_rule.get("conditions")[1].from_value = range_b[0]
shipping_rule.get("conditions")[1].to_value = range_b[1]
self.assertRaises(OverlappingConditionError, shipping_rule.insert)
+
def create_shipping_rule(shipping_rule_type, shipping_rule_name):
if frappe.db.exists("Shipping Rule", shipping_rule_name):
return frappe.get_doc("Shipping Rule", shipping_rule_name)
sr = frappe.new_doc("Shipping Rule")
- sr.account = "_Test Account Shipping Charges - _TC"
- sr.calculate_based_on = "Net Total"
+ sr.account = "_Test Account Shipping Charges - _TC"
+ sr.calculate_based_on = "Net Total"
sr.company = "_Test Company"
sr.cost_center = "_Test Cost Center - _TC"
sr.label = shipping_rule_name
sr.name = shipping_rule_name
sr.shipping_rule_type = shipping_rule_type
- sr.append("conditions", {
+ sr.append(
+ "conditions",
+ {
"doctype": "Shipping Rule Condition",
"from_value": 0,
"parentfield": "conditions",
"shipping_amount": 50.0,
- "to_value": 100
- })
- sr.append("conditions", {
+ "to_value": 100,
+ },
+ )
+ sr.append(
+ "conditions",
+ {
"doctype": "Shipping Rule Condition",
"from_value": 101,
"parentfield": "conditions",
"shipping_amount": 100.0,
- "to_value": 200
- })
- sr.append("conditions", {
+ "to_value": 200,
+ },
+ )
+ sr.append(
+ "conditions",
+ {
"doctype": "Shipping Rule Condition",
"from_value": 201,
"parentfield": "conditions",
"shipping_amount": 200.0,
- "to_value": 2000
- })
+ "to_value": 2000,
+ },
+ )
sr.insert(ignore_permissions=True)
sr.submit()
return sr
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 744b99807fc..62ffc79fee8 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -1,4 +1,3 @@
-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
@@ -61,7 +60,11 @@ class Subscription(Document):
"""
_current_invoice_start = None
- if self.is_new_subscription() and self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
+ if (
+ self.is_new_subscription()
+ and self.trial_period_end
+ and getdate(self.trial_period_end) > getdate(self.start_date)
+ ):
_current_invoice_start = add_days(self.trial_period_end, 1)
elif self.trial_period_start and self.is_trialling():
_current_invoice_start = self.trial_period_start
@@ -103,7 +106,7 @@ class Subscription(Document):
if self.follow_calendar_months:
billing_info = self.get_billing_cycle_and_interval()
- billing_interval_count = billing_info[0]['billing_interval_count']
+ billing_interval_count = billing_info[0]["billing_interval_count"]
calendar_months = get_calendar_months(billing_interval_count)
calendar_month = 0
current_invoice_end_month = getdate(_current_invoice_end).month
@@ -113,12 +116,13 @@ class Subscription(Document):
if month <= current_invoice_end_month:
calendar_month = month
- if cint(calendar_month - billing_interval_count) <= 0 and \
- getdate(date).month != 1:
+ if cint(calendar_month - billing_interval_count) <= 0 and getdate(date).month != 1:
calendar_month = 12
current_invoice_end_year -= 1
- _current_invoice_end = get_last_day(cstr(current_invoice_end_year) + '-' + cstr(calendar_month) + '-01')
+ _current_invoice_end = get_last_day(
+ cstr(current_invoice_end_year) + "-" + cstr(calendar_month) + "-01"
+ )
if self.end_date and getdate(_current_invoice_end) > getdate(self.end_date):
_current_invoice_end = self.end_date
@@ -132,7 +136,7 @@ class Subscription(Document):
same billing interval
"""
if billing_cycle_data and len(billing_cycle_data) != 1:
- frappe.throw(_('You can only have Plans with the same billing cycle in a Subscription'))
+ frappe.throw(_("You can only have Plans with the same billing cycle in a Subscription"))
def get_billing_cycle_and_interval(self):
"""
@@ -142,10 +146,11 @@ class Subscription(Document):
"""
plan_names = [plan.plan for plan in self.plans]
billing_info = frappe.db.sql(
- 'select distinct `billing_interval`, `billing_interval_count` '
- 'from `tabSubscription Plan` '
- 'where name in %s',
- (plan_names,), as_dict=1
+ "select distinct `billing_interval`, `billing_interval_count` "
+ "from `tabSubscription Plan` "
+ "where name in %s",
+ (plan_names,),
+ as_dict=1,
)
return billing_info
@@ -162,19 +167,19 @@ class Subscription(Document):
if billing_info:
data = dict()
- interval = billing_info[0]['billing_interval']
- interval_count = billing_info[0]['billing_interval_count']
- if interval not in ['Day', 'Week']:
- data['days'] = -1
- if interval == 'Day':
- data['days'] = interval_count - 1
- elif interval == 'Month':
- data['months'] = interval_count
- elif interval == 'Year':
- data['years'] = interval_count
+ interval = billing_info[0]["billing_interval"]
+ interval_count = billing_info[0]["billing_interval_count"]
+ if interval not in ["Day", "Week"]:
+ data["days"] = -1
+ if interval == "Day":
+ data["days"] = interval_count - 1
+ elif interval == "Month":
+ data["months"] = interval_count
+ elif interval == "Year":
+ data["years"] = interval_count
# todo: test week
- elif interval == 'Week':
- data['days'] = interval_count * 7 - 1
+ elif interval == "Week":
+ data["days"] = interval_count * 7 - 1
return data
@@ -185,27 +190,27 @@ class Subscription(Document):
Used when the `Subscription` needs to decide what to do after the current generated
invoice is past it's due date and grace period.
"""
- subscription_settings = frappe.get_single('Subscription Settings')
- if self.status == 'Past Due Date' and self.is_past_grace_period():
- self.status = 'Cancelled' if cint(subscription_settings.cancel_after_grace) else 'Unpaid'
+ subscription_settings = frappe.get_single("Subscription Settings")
+ if self.status == "Past Due Date" and self.is_past_grace_period():
+ self.status = "Cancelled" if cint(subscription_settings.cancel_after_grace) else "Unpaid"
def set_subscription_status(self):
"""
Sets the status of the `Subscription`
"""
if self.is_trialling():
- self.status = 'Trialling'
- elif self.status == 'Active' and self.end_date and getdate() > getdate(self.end_date):
- self.status = 'Completed'
+ self.status = "Trialling"
+ elif self.status == "Active" and self.end_date and getdate() > getdate(self.end_date):
+ self.status = "Completed"
elif self.is_past_grace_period():
- subscription_settings = frappe.get_single('Subscription Settings')
- self.status = 'Cancelled' if cint(subscription_settings.cancel_after_grace) else 'Unpaid'
+ subscription_settings = frappe.get_single("Subscription Settings")
+ self.status = "Cancelled" if cint(subscription_settings.cancel_after_grace) else "Unpaid"
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
- self.status = 'Past Due Date'
+ self.status = "Past Due Date"
elif not self.has_outstanding_invoice():
- self.status = 'Active'
+ self.status = "Active"
elif self.is_new_subscription():
- self.status = 'Active'
+ self.status = "Active"
self.save()
def is_trialling(self):
@@ -232,7 +237,7 @@ class Subscription(Document):
"""
current_invoice = self.get_current_invoice()
if self.current_invoice_is_past_due(current_invoice):
- subscription_settings = frappe.get_single('Subscription Settings')
+ subscription_settings = frappe.get_single("Subscription Settings")
grace_period = cint(subscription_settings.grace_period)
return getdate() > add_days(current_invoice.due_date, grace_period)
@@ -253,15 +258,15 @@ class Subscription(Document):
"""
Returns the most recent generated invoice.
"""
- doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
+ doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
if len(self.invoices):
current = self.invoices[-1]
- if frappe.db.exists(doctype, current.get('invoice')):
- doc = frappe.get_doc(doctype, current.get('invoice'))
+ if frappe.db.exists(doctype, current.get("invoice")):
+ doc = frappe.get_doc(doctype, current.get("invoice"))
return doc
else:
- frappe.throw(_('Invoice {0} no longer exists').format(current.get('invoice')))
+ frappe.throw(_("Invoice {0} no longer exists").format(current.get("invoice")))
def is_new_subscription(self):
"""
@@ -274,7 +279,7 @@ class Subscription(Document):
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
self.validate_end_date()
self.validate_to_follow_calendar_months()
- self.cost_center = erpnext.get_default_cost_center(self.get('company'))
+ self.cost_center = erpnext.get_default_cost_center(self.get("company"))
def validate_trial_period(self):
"""
@@ -282,30 +287,34 @@ class Subscription(Document):
"""
if self.trial_period_start and self.trial_period_end:
if getdate(self.trial_period_end) < getdate(self.trial_period_start):
- frappe.throw(_('Trial Period End Date Cannot be before Trial Period Start Date'))
+ frappe.throw(_("Trial Period End Date Cannot be before Trial Period Start Date"))
if self.trial_period_start and not self.trial_period_end:
- frappe.throw(_('Both Trial Period Start Date and Trial Period End Date must be set'))
+ frappe.throw(_("Both Trial Period Start Date and Trial Period End Date must be set"))
if self.trial_period_start and getdate(self.trial_period_start) > getdate(self.start_date):
- frappe.throw(_('Trial Period Start date cannot be after Subscription Start Date'))
+ frappe.throw(_("Trial Period Start date cannot be after Subscription Start Date"))
def validate_end_date(self):
billing_cycle_info = self.get_billing_cycle_data()
end_date = add_to_date(self.start_date, **billing_cycle_info)
if self.end_date and getdate(self.end_date) <= getdate(end_date):
- frappe.throw(_('Subscription End Date must be after {0} as per the subscription plan').format(end_date))
+ frappe.throw(
+ _("Subscription End Date must be after {0} as per the subscription plan").format(end_date)
+ )
def validate_to_follow_calendar_months(self):
if self.follow_calendar_months:
billing_info = self.get_billing_cycle_and_interval()
if not self.end_date:
- frappe.throw(_('Subscription End Date is mandatory to follow calendar months'))
+ frappe.throw(_("Subscription End Date is mandatory to follow calendar months"))
- if billing_info[0]['billing_interval'] != 'Month':
- frappe.throw(_('Billing Interval in Subscription Plan must be Month to follow calendar months'))
+ if billing_info[0]["billing_interval"] != "Month":
+ frappe.throw(
+ _("Billing Interval in Subscription Plan must be Month to follow calendar months")
+ )
def after_insert(self):
# todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype?
@@ -317,13 +326,10 @@ class Subscription(Document):
saves the `Subscription`.
"""
- doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
+ doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
invoice = self.create_invoice(prorate)
- self.append('invoices', {
- 'document_type': doctype,
- 'invoice': invoice.name
- })
+ self.append("invoices", {"document_type": doctype, "invoice": invoice.name})
self.save()
@@ -333,55 +339,58 @@ class Subscription(Document):
"""
Creates a `Invoice`, submits it and returns it
"""
- doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
+ doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
invoice = frappe.new_doc(doctype)
# For backward compatibility
# Earlier subscription didn't had any company field
- company = self.get('company') or get_default_company()
+ company = self.get("company") or get_default_company()
if not company:
- frappe.throw(_("Company is mandatory was generating invoice. Please set default company in Global Defaults"))
+ frappe.throw(
+ _("Company is mandatory was generating invoice. Please set default company in Global Defaults")
+ )
invoice.company = company
invoice.set_posting_time = 1
- invoice.posting_date = self.current_invoice_start if self.generate_invoice_at_period_start \
+ invoice.posting_date = (
+ self.current_invoice_start
+ if self.generate_invoice_at_period_start
else self.current_invoice_end
+ )
invoice.cost_center = self.cost_center
- if doctype == 'Sales Invoice':
+ if doctype == "Sales Invoice":
invoice.customer = self.party
else:
invoice.supplier = self.party
- if frappe.db.get_value('Supplier', self.party, 'tax_withholding_category'):
+ if frappe.db.get_value("Supplier", self.party, "tax_withholding_category"):
invoice.apply_tds = 1
- ### Add party currency to invoice
+ # Add party currency to invoice
invoice.currency = get_party_account_currency(self.party_type, self.party, self.company)
- ## Add dimensions in invoice for subscription:
+ # Add dimensions in invoice for subscription:
accounting_dimensions = get_accounting_dimensions()
for dimension in accounting_dimensions:
if self.get(dimension):
- invoice.update({
- dimension: self.get(dimension)
- })
+ invoice.update({dimension: self.get(dimension)})
# Subscription is better suited for service items. I won't update `update_stock`
# for that reason
items_list = self.get_items_from_plans(self.plans, prorate)
for item in items_list:
- item['cost_center'] = self.cost_center
- invoice.append('items', item)
+ item["cost_center"] = self.cost_center
+ invoice.append("items", item)
# Taxes
- tax_template = ''
+ tax_template = ""
- if doctype == 'Sales Invoice' and self.sales_tax_template:
+ if doctype == "Sales Invoice" and self.sales_tax_template:
tax_template = self.sales_tax_template
- if doctype == 'Purchase Invoice' and self.purchase_tax_template:
+ if doctype == "Purchase Invoice" and self.purchase_tax_template:
tax_template = self.purchase_tax_template
if tax_template:
@@ -391,11 +400,11 @@ class Subscription(Document):
# Due date
if self.days_until_due:
invoice.append(
- 'payment_schedule',
+ "payment_schedule",
{
- 'due_date': add_days(invoice.posting_date, cint(self.days_until_due)),
- 'invoice_portion': 100
- }
+ "due_date": add_days(invoice.posting_date, cint(self.days_until_due)),
+ "invoice_portion": 100,
+ },
)
# Discounts
@@ -407,7 +416,7 @@ class Subscription(Document):
if self.additional_discount_percentage or self.additional_discount_amount:
discount_on = self.apply_additional_discount
- invoice.apply_discount_on = discount_on if discount_on else 'Grand Total'
+ invoice.apply_discount_on = discount_on if discount_on else "Grand Total"
# Subscription period
invoice.from_date = self.current_invoice_start
@@ -427,44 +436,62 @@ class Subscription(Document):
Returns the `Item`s linked to `Subscription Plan`
"""
if prorate:
- prorate_factor = get_prorata_factor(self.current_invoice_end, self.current_invoice_start,
- self.generate_invoice_at_period_start)
+ prorate_factor = get_prorata_factor(
+ self.current_invoice_end, self.current_invoice_start, self.generate_invoice_at_period_start
+ )
items = []
party = self.party
for plan in plans:
- plan_doc = frappe.get_doc('Subscription Plan', plan.plan)
+ plan_doc = frappe.get_doc("Subscription Plan", plan.plan)
item_code = plan_doc.item
- if self.party == 'Customer':
- deferred_field = 'enable_deferred_revenue'
+ if self.party == "Customer":
+ deferred_field = "enable_deferred_revenue"
else:
- deferred_field = 'enable_deferred_expense'
+ deferred_field = "enable_deferred_expense"
- deferred = frappe.db.get_value('Item', item_code, deferred_field)
+ deferred = frappe.db.get_value("Item", item_code, deferred_field)
if not prorate:
- item = {'item_code': item_code, 'qty': plan.qty, 'rate': get_plan_rate(plan.plan, plan.qty, party,
- self.current_invoice_start, self.current_invoice_end), 'cost_center': plan_doc.cost_center}
+ item = {
+ "item_code": item_code,
+ "qty": plan.qty,
+ "rate": get_plan_rate(
+ plan.plan, plan.qty, party, self.current_invoice_start, self.current_invoice_end
+ ),
+ "cost_center": plan_doc.cost_center,
+ }
else:
- item = {'item_code': item_code, 'qty': plan.qty, 'rate': get_plan_rate(plan.plan, plan.qty, party,
- self.current_invoice_start, self.current_invoice_end, prorate_factor), 'cost_center': plan_doc.cost_center}
+ item = {
+ "item_code": item_code,
+ "qty": plan.qty,
+ "rate": get_plan_rate(
+ plan.plan,
+ plan.qty,
+ party,
+ self.current_invoice_start,
+ self.current_invoice_end,
+ prorate_factor,
+ ),
+ "cost_center": plan_doc.cost_center,
+ }
if deferred:
- item.update({
- deferred_field: deferred,
- 'service_start_date': self.current_invoice_start,
- 'service_end_date': self.current_invoice_end
- })
+ item.update(
+ {
+ deferred_field: deferred,
+ "service_start_date": self.current_invoice_start,
+ "service_end_date": self.current_invoice_end,
+ }
+ )
accounting_dimensions = get_accounting_dimensions()
for dimension in accounting_dimensions:
if plan_doc.get(dimension):
- item.update({
- dimension: plan_doc.get(dimension)
- })
+ item.update({dimension: plan_doc.get(dimension)})
items.append(item)
@@ -477,9 +504,9 @@ class Subscription(Document):
1. `process_for_active`
2. `process_for_past_due`
"""
- if self.status == 'Active':
+ if self.status == "Active":
self.process_for_active()
- elif self.status in ['Past Due Date', 'Unpaid']:
+ elif self.status in ["Past Due Date", "Unpaid"]:
self.process_for_past_due_date()
self.set_subscription_status()
@@ -487,8 +514,10 @@ class Subscription(Document):
self.save()
def is_postpaid_to_invoice(self):
- return getdate() > getdate(self.current_invoice_end) or \
- (getdate() >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start))
+ return getdate() > getdate(self.current_invoice_end) or (
+ getdate() >= getdate(self.current_invoice_end)
+ and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)
+ )
def is_prepaid_to_invoice(self):
if not self.generate_invoice_at_period_start:
@@ -504,9 +533,13 @@ class Subscription(Document):
invoice = self.get_current_invoice()
if not (_current_start_date and _current_end_date):
- _current_start_date, _current_end_date = self.update_subscription_period(date=add_days(self.current_invoice_end, 1), return_date=True)
+ _current_start_date, _current_end_date = self.update_subscription_period(
+ date=add_days(self.current_invoice_end, 1), return_date=True
+ )
- if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(_current_end_date):
+ if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(
+ _current_end_date
+ ):
return True
return False
@@ -521,10 +554,11 @@ class Subscription(Document):
3. Change the `Subscription` status to 'Cancelled'
"""
- if not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) \
- and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
+ if not self.is_current_invoice_generated(
+ self.current_invoice_start, self.current_invoice_end
+ ) and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
- prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
+ prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
self.generate_invoice(prorate)
if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice():
@@ -540,7 +574,7 @@ class Subscription(Document):
if self.end_date and getdate() < getdate(self.end_date):
return
- self.status = 'Cancelled'
+ self.status = "Cancelled"
if not self.cancelation_date:
self.cancelation_date = nowdate()
@@ -555,19 +589,21 @@ class Subscription(Document):
"""
current_invoice = self.get_current_invoice()
if not current_invoice:
- frappe.throw(_('Current invoice {0} is missing').format(current_invoice.invoice))
+ frappe.throw(_("Current invoice {0} is missing").format(current_invoice.invoice))
else:
if not self.has_outstanding_invoice():
- self.status = 'Active'
+ self.status = "Active"
else:
self.set_status_grace_period()
# Generate invoices periodically even if current invoice are unpaid
- if self.generate_new_invoices_past_due_date and not \
- self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) \
- and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
+ if (
+ self.generate_new_invoices_past_due_date
+ and not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end)
+ and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice())
+ ):
- prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
+ prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
self.generate_invoice(prorate)
if getdate() > getdate(self.current_invoice_end):
@@ -578,18 +614,19 @@ class Subscription(Document):
"""
Return `True` if the given invoice is paid
"""
- return invoice.status == 'Paid'
+ return invoice.status == "Paid"
def has_outstanding_invoice(self):
"""
Returns `True` if the most recent invoice for the `Subscription` is not paid
"""
- doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
+ doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
current_invoice = self.get_current_invoice()
invoice_list = [d.invoice for d in self.invoices]
- outstanding_invoices = frappe.get_all(doctype, fields=['name'],
- filters={'status': ('!=', 'Paid'), 'name': ('in', invoice_list)})
+ outstanding_invoices = frappe.get_all(
+ doctype, fields=["name"], filters={"status": ("!=", "Paid"), "name": ("in", invoice_list)}
+ )
if outstanding_invoices:
return True
@@ -601,10 +638,12 @@ class Subscription(Document):
This sets the subscription as cancelled. It will stop invoices from being generated
but it will not affect already created invoices.
"""
- if self.status != 'Cancelled':
- to_generate_invoice = True if self.status == 'Active' and not self.generate_invoice_at_period_start else False
- to_prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
- self.status = 'Cancelled'
+ if self.status != "Cancelled":
+ to_generate_invoice = (
+ True if self.status == "Active" and not self.generate_invoice_at_period_start else False
+ )
+ to_prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
+ self.status = "Cancelled"
self.cancelation_date = nowdate()
if to_generate_invoice:
self.generate_invoice(prorate=to_prorate)
@@ -616,19 +655,20 @@ class Subscription(Document):
subscription and the `Subscription` will lose all the history of generated invoices
it has.
"""
- if self.status == 'Cancelled':
- self.status = 'Active'
- self.db_set('start_date', nowdate())
+ if self.status == "Cancelled":
+ self.status = "Active"
+ self.db_set("start_date", nowdate())
self.update_subscription_period(nowdate())
self.invoices = []
self.save()
else:
- frappe.throw(_('You cannot restart a Subscription that is not cancelled.'))
+ frappe.throw(_("You cannot restart a Subscription that is not cancelled."))
def get_precision(self):
invoice = self.get_current_invoice()
if invoice:
- return invoice.precision('grand_total')
+ return invoice.precision("grand_total")
+
def get_calendar_months(billing_interval):
calendar_months = []
@@ -639,6 +679,7 @@ def get_calendar_months(billing_interval):
return calendar_months
+
def get_prorata_factor(period_end, period_start, is_prepaid):
if is_prepaid:
prorate_factor = 1
@@ -663,7 +704,7 @@ def get_all_subscriptions():
"""
Returns all `Subscription` documents
"""
- return frappe.db.get_all('Subscription', {'status': ('!=','Cancelled')})
+ return frappe.db.get_all("Subscription", {"status": ("!=", "Cancelled")})
def process(data):
@@ -672,7 +713,7 @@ def process(data):
"""
if data:
try:
- subscription = frappe.get_doc('Subscription', data['name'])
+ subscription = frappe.get_doc("Subscription", data["name"])
subscription.process()
frappe.db.commit()
except frappe.ValidationError:
@@ -688,7 +729,7 @@ def cancel_subscription(name):
Cancels a `Subscription`. This will stop the `Subscription` from further invoicing the
`Subscriber` but all already outstanding invoices will not be affected.
"""
- subscription = frappe.get_doc('Subscription', name)
+ subscription = frappe.get_doc("Subscription", name)
subscription.cancel_subscription()
@@ -698,7 +739,7 @@ def restart_subscription(name):
Restarts a cancelled `Subscription`. The `Subscription` will 'forget' the history of
all invoices it has generated
"""
- subscription = frappe.get_doc('Subscription', name)
+ subscription = frappe.get_doc("Subscription", name)
subscription.restart_subscription()
@@ -707,5 +748,5 @@ def get_subscription_updates(name):
"""
Use this to get the latest state of the given `Subscription`
"""
- subscription = frappe.get_doc('Subscription', name)
+ subscription = frappe.get_doc("Subscription", name)
subscription.process()
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index 6f67bc5128b..eb17daa282f 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -18,104 +18,111 @@ from erpnext.accounts.doctype.subscription.subscription import get_prorata_facto
test_dependencies = ("UOM", "Item Group", "Item")
+
def create_plan():
- if not frappe.db.exists('Subscription Plan', '_Test Plan Name'):
- plan = frappe.new_doc('Subscription Plan')
- plan.plan_name = '_Test Plan Name'
- plan.item = '_Test Non Stock Item'
+ if not frappe.db.exists("Subscription Plan", "_Test Plan Name"):
+ plan = frappe.new_doc("Subscription Plan")
+ plan.plan_name = "_Test Plan Name"
+ plan.item = "_Test Non Stock Item"
plan.price_determination = "Fixed Rate"
plan.cost = 900
- plan.billing_interval = 'Month'
+ plan.billing_interval = "Month"
plan.billing_interval_count = 1
plan.insert()
- if not frappe.db.exists('Subscription Plan', '_Test Plan Name 2'):
- plan = frappe.new_doc('Subscription Plan')
- plan.plan_name = '_Test Plan Name 2'
- plan.item = '_Test Non Stock Item'
+ if not frappe.db.exists("Subscription Plan", "_Test Plan Name 2"):
+ plan = frappe.new_doc("Subscription Plan")
+ plan.plan_name = "_Test Plan Name 2"
+ plan.item = "_Test Non Stock Item"
plan.price_determination = "Fixed Rate"
plan.cost = 1999
- plan.billing_interval = 'Month'
+ plan.billing_interval = "Month"
plan.billing_interval_count = 1
plan.insert()
- if not frappe.db.exists('Subscription Plan', '_Test Plan Name 3'):
- plan = frappe.new_doc('Subscription Plan')
- plan.plan_name = '_Test Plan Name 3'
- plan.item = '_Test Non Stock Item'
+ if not frappe.db.exists("Subscription Plan", "_Test Plan Name 3"):
+ plan = frappe.new_doc("Subscription Plan")
+ plan.plan_name = "_Test Plan Name 3"
+ plan.item = "_Test Non Stock Item"
plan.price_determination = "Fixed Rate"
plan.cost = 1999
- plan.billing_interval = 'Day'
+ plan.billing_interval = "Day"
plan.billing_interval_count = 14
plan.insert()
# Defined a quarterly Subscription Plan
- if not frappe.db.exists('Subscription Plan', '_Test Plan Name 4'):
- plan = frappe.new_doc('Subscription Plan')
- plan.plan_name = '_Test Plan Name 4'
- plan.item = '_Test Non Stock Item'
+ if not frappe.db.exists("Subscription Plan", "_Test Plan Name 4"):
+ plan = frappe.new_doc("Subscription Plan")
+ plan.plan_name = "_Test Plan Name 4"
+ plan.item = "_Test Non Stock Item"
plan.price_determination = "Monthly Rate"
plan.cost = 20000
- plan.billing_interval = 'Month'
+ plan.billing_interval = "Month"
plan.billing_interval_count = 3
plan.insert()
- if not frappe.db.exists('Subscription Plan', '_Test Plan Multicurrency'):
- plan = frappe.new_doc('Subscription Plan')
- plan.plan_name = '_Test Plan Multicurrency'
- plan.item = '_Test Non Stock Item'
+ if not frappe.db.exists("Subscription Plan", "_Test Plan Multicurrency"):
+ plan = frappe.new_doc("Subscription Plan")
+ plan.plan_name = "_Test Plan Multicurrency"
+ plan.item = "_Test Non Stock Item"
plan.price_determination = "Fixed Rate"
plan.cost = 50
- plan.currency = 'USD'
- plan.billing_interval = 'Month'
+ plan.currency = "USD"
+ plan.billing_interval = "Month"
plan.billing_interval_count = 1
plan.insert()
+
def create_parties():
- if not frappe.db.exists('Supplier', '_Test Supplier'):
- supplier = frappe.new_doc('Supplier')
- supplier.supplier_name = '_Test Supplier'
- supplier.supplier_group = 'All Supplier Groups'
+ if not frappe.db.exists("Supplier", "_Test Supplier"):
+ supplier = frappe.new_doc("Supplier")
+ supplier.supplier_name = "_Test Supplier"
+ supplier.supplier_group = "All Supplier Groups"
supplier.insert()
- if not frappe.db.exists('Customer', '_Test Subscription Customer'):
- customer = frappe.new_doc('Customer')
- customer.customer_name = '_Test Subscription Customer'
- customer.billing_currency = 'USD'
- customer.append('accounts', {
- 'company': '_Test Company',
- 'account': '_Test Receivable USD - _TC'
- })
+ if not frappe.db.exists("Customer", "_Test Subscription Customer"):
+ customer = frappe.new_doc("Customer")
+ customer.customer_name = "_Test Subscription Customer"
+ customer.billing_currency = "USD"
+ customer.append(
+ "accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"}
+ )
customer.insert()
+
class TestSubscription(unittest.TestCase):
def setUp(self):
create_plan()
create_parties()
def test_create_subscription_with_trial_with_correct_period(self):
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
subscription.trial_period_start = nowdate()
subscription.trial_period_end = add_months(nowdate(), 1)
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
self.assertEqual(subscription.trial_period_start, nowdate())
self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1))
- self.assertEqual(add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start))
- self.assertEqual(add_to_date(subscription.current_invoice_start, months=1, days=-1), get_date_str(subscription.current_invoice_end))
+ self.assertEqual(
+ add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start)
+ )
+ self.assertEqual(
+ add_to_date(subscription.current_invoice_start, months=1, days=-1),
+ get_date_str(subscription.current_invoice_end),
+ )
self.assertEqual(subscription.invoices, [])
- self.assertEqual(subscription.status, 'Trialling')
+ self.assertEqual(subscription.status, "Trialling")
subscription.delete()
def test_create_subscription_without_trial_with_correct_period(self):
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
self.assertEqual(subscription.trial_period_start, None)
@@ -124,190 +131,190 @@ class TestSubscription(unittest.TestCase):
self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
# No invoice is created
self.assertEqual(len(subscription.invoices), 0)
- self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(subscription.status, "Active")
subscription.delete()
def test_create_subscription_trial_with_wrong_dates(self):
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
subscription.trial_period_end = nowdate()
subscription.trial_period_start = add_days(nowdate(), 30)
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
self.assertRaises(frappe.ValidationError, subscription.save)
subscription.delete()
def test_create_subscription_multi_with_different_billing_fails(self):
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
subscription.trial_period_end = nowdate()
subscription.trial_period_start = add_days(nowdate(), 30)
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
- subscription.append('plans', {'plan': '_Test Plan Name 3', 'qty': 1})
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
+ subscription.append("plans", {"plan": "_Test Plan Name 3", "qty": 1})
self.assertRaises(frappe.ValidationError, subscription.save)
subscription.delete()
def test_invoice_is_generated_at_end_of_billing_period(self):
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.start_date = '2018-01-01'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.start_date = "2018-01-01"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.insert()
- self.assertEqual(subscription.status, 'Active')
- self.assertEqual(subscription.current_invoice_start, '2018-01-01')
- self.assertEqual(subscription.current_invoice_end, '2018-01-31')
+ self.assertEqual(subscription.status, "Active")
+ self.assertEqual(subscription.current_invoice_start, "2018-01-01")
+ self.assertEqual(subscription.current_invoice_end, "2018-01-31")
subscription.process()
self.assertEqual(len(subscription.invoices), 1)
- self.assertEqual(subscription.current_invoice_start, '2018-01-01')
+ self.assertEqual(subscription.current_invoice_start, "2018-01-01")
subscription.process()
- self.assertEqual(subscription.status, 'Unpaid')
+ self.assertEqual(subscription.status, "Unpaid")
subscription.delete()
def test_status_goes_back_to_active_after_invoice_is_paid(self):
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
- subscription.start_date = '2018-01-01'
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
+ subscription.start_date = "2018-01-01"
subscription.insert()
- subscription.process() # generate first invoice
+ subscription.process() # generate first invoice
self.assertEqual(len(subscription.invoices), 1)
# Status is unpaid as Days until Due is zero and grace period is Zero
- self.assertEqual(subscription.status, 'Unpaid')
+ self.assertEqual(subscription.status, "Unpaid")
subscription.get_current_invoice()
current_invoice = subscription.get_current_invoice()
self.assertIsNotNone(current_invoice)
- current_invoice.db_set('outstanding_amount', 0)
- current_invoice.db_set('status', 'Paid')
+ current_invoice.db_set("outstanding_amount", 0)
+ current_invoice.db_set("status", "Paid")
subscription.process()
- self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(subscription.status, "Active")
self.assertEqual(subscription.current_invoice_start, add_months(subscription.start_date, 1))
self.assertEqual(len(subscription.invoices), 1)
subscription.delete()
def test_subscription_cancel_after_grace_period(self):
- settings = frappe.get_single('Subscription Settings')
+ settings = frappe.get_single("Subscription Settings")
default_grace_period_action = settings.cancel_after_grace
settings.cancel_after_grace = 1
settings.save()
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
- subscription.start_date = '2018-01-01'
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
+ subscription.start_date = "2018-01-01"
subscription.insert()
- self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(subscription.status, "Active")
- subscription.process() # generate first invoice
+ subscription.process() # generate first invoice
# This should change status to Cancelled since grace period is 0
# And is backdated subscription so subscription will be cancelled after processing
- self.assertEqual(subscription.status, 'Cancelled')
+ self.assertEqual(subscription.status, "Cancelled")
settings.cancel_after_grace = default_grace_period_action
settings.save()
subscription.delete()
def test_subscription_unpaid_after_grace_period(self):
- settings = frappe.get_single('Subscription Settings')
+ settings = frappe.get_single("Subscription Settings")
default_grace_period_action = settings.cancel_after_grace
settings.cancel_after_grace = 0
settings.save()
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
- subscription.start_date = '2018-01-01'
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
+ subscription.start_date = "2018-01-01"
subscription.insert()
- subscription.process() # generate first invoice
+ subscription.process() # generate first invoice
# Status is unpaid as Days until Due is zero and grace period is Zero
- self.assertEqual(subscription.status, 'Unpaid')
+ self.assertEqual(subscription.status, "Unpaid")
settings.cancel_after_grace = default_grace_period_action
settings.save()
subscription.delete()
def test_subscription_invoice_days_until_due(self):
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.days_until_due = 10
subscription.start_date = add_months(nowdate(), -1)
subscription.insert()
- subscription.process() # generate first invoice
+ subscription.process() # generate first invoice
self.assertEqual(len(subscription.invoices), 1)
- self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(subscription.status, "Active")
subscription.delete()
def test_subscription_is_past_due_doesnt_change_within_grace_period(self):
- settings = frappe.get_single('Subscription Settings')
+ settings = frappe.get_single("Subscription Settings")
grace_period = settings.grace_period
settings.grace_period = 1000
settings.save()
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.start_date = add_days(nowdate(), -1000)
subscription.insert()
- subscription.process() # generate first invoice
+ subscription.process() # generate first invoice
- self.assertEqual(subscription.status, 'Past Due Date')
+ self.assertEqual(subscription.status, "Past Due Date")
subscription.process()
# Grace period is 1000 days so status should remain as Past Due Date
- self.assertEqual(subscription.status, 'Past Due Date')
+ self.assertEqual(subscription.status, "Past Due Date")
subscription.process()
- self.assertEqual(subscription.status, 'Past Due Date')
+ self.assertEqual(subscription.status, "Past Due Date")
subscription.process()
- self.assertEqual(subscription.status, 'Past Due Date')
+ self.assertEqual(subscription.status, "Past Due Date")
settings.grace_period = grace_period
settings.save()
subscription.delete()
def test_subscription_remains_active_during_invoice_period(self):
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
- subscription.process() # no changes expected
+ subscription.process() # no changes expected
- self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(subscription.status, "Active")
self.assertEqual(subscription.current_invoice_start, nowdate())
self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
self.assertEqual(len(subscription.invoices), 0)
- subscription.process() # no changes expected still
- self.assertEqual(subscription.status, 'Active')
+ subscription.process() # no changes expected still
+ self.assertEqual(subscription.status, "Active")
self.assertEqual(subscription.current_invoice_start, nowdate())
self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
self.assertEqual(len(subscription.invoices), 0)
- subscription.process() # no changes expected yet still
- self.assertEqual(subscription.status, 'Active')
+ subscription.process() # no changes expected yet still
+ self.assertEqual(subscription.status, "Active")
self.assertEqual(subscription.current_invoice_start, nowdate())
self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
self.assertEqual(len(subscription.invoices), 0)
@@ -315,30 +322,30 @@ class TestSubscription(unittest.TestCase):
subscription.delete()
def test_subscription_cancelation(self):
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
subscription.cancel_subscription()
- self.assertEqual(subscription.status, 'Cancelled')
+ self.assertEqual(subscription.status, "Cancelled")
subscription.delete()
def test_subscription_cancellation_invoices(self):
- settings = frappe.get_single('Subscription Settings')
+ settings = frappe.get_single("Subscription Settings")
to_prorate = settings.prorate
settings.prorate = 1
settings.save()
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
- self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(subscription.status, "Active")
subscription.cancel_subscription()
# Invoice must have been generated
@@ -346,33 +353,39 @@ class TestSubscription(unittest.TestCase):
invoice = subscription.get_current_invoice()
diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1)
- plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1)
- prorate_factor = flt(diff/plan_days)
+ plan_days = flt(
+ date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1
+ )
+ prorate_factor = flt(diff / plan_days)
self.assertEqual(
flt(
- get_prorata_factor(subscription.current_invoice_end, subscription.current_invoice_start,
- subscription.generate_invoice_at_period_start),
- 2),
- flt(prorate_factor, 2)
+ get_prorata_factor(
+ subscription.current_invoice_end,
+ subscription.current_invoice_start,
+ subscription.generate_invoice_at_period_start,
+ ),
+ 2,
+ ),
+ flt(prorate_factor, 2),
)
self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2))
- self.assertEqual(subscription.status, 'Cancelled')
+ self.assertEqual(subscription.status, "Cancelled")
subscription.delete()
settings.prorate = to_prorate
settings.save()
def test_subscription_cancellation_invoices_with_prorata_false(self):
- settings = frappe.get_single('Subscription Settings')
+ settings = frappe.get_single("Subscription Settings")
to_prorate = settings.prorate
settings.prorate = 0
settings.save()
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
subscription.cancel_subscription()
invoice = subscription.get_current_invoice()
@@ -385,21 +398,23 @@ class TestSubscription(unittest.TestCase):
subscription.delete()
def test_subscription_cancellation_invoices_with_prorata_true(self):
- settings = frappe.get_single('Subscription Settings')
+ settings = frappe.get_single("Subscription Settings")
to_prorate = settings.prorate
settings.prorate = 1
settings.save()
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
subscription.cancel_subscription()
invoice = subscription.get_current_invoice()
diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1)
- plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1)
+ plan_days = flt(
+ date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1
+ )
prorate_factor = flt(diff / plan_days)
self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2))
@@ -410,30 +425,30 @@ class TestSubscription(unittest.TestCase):
subscription.delete()
def test_subcription_cancellation_and_process(self):
- settings = frappe.get_single('Subscription Settings')
+ settings = frappe.get_single("Subscription Settings")
default_grace_period_action = settings.cancel_after_grace
settings.cancel_after_grace = 1
settings.save()
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
- subscription.start_date = '2018-01-01'
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
+ subscription.start_date = "2018-01-01"
subscription.insert()
- subscription.process() # generate first invoice
+ subscription.process() # generate first invoice
invoices = len(subscription.invoices)
subscription.cancel_subscription()
- self.assertEqual(subscription.status, 'Cancelled')
+ self.assertEqual(subscription.status, "Cancelled")
self.assertEqual(len(subscription.invoices), invoices)
subscription.process()
- self.assertEqual(subscription.status, 'Cancelled')
+ self.assertEqual(subscription.status, "Cancelled")
self.assertEqual(len(subscription.invoices), invoices)
subscription.process()
- self.assertEqual(subscription.status, 'Cancelled')
+ self.assertEqual(subscription.status, "Cancelled")
self.assertEqual(len(subscription.invoices), invoices)
settings.cancel_after_grace = default_grace_period_action
@@ -441,36 +456,36 @@ class TestSubscription(unittest.TestCase):
subscription.delete()
def test_subscription_restart_and_process(self):
- settings = frappe.get_single('Subscription Settings')
+ settings = frappe.get_single("Subscription Settings")
default_grace_period_action = settings.cancel_after_grace
settings.grace_period = 0
settings.cancel_after_grace = 0
settings.save()
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
- subscription.start_date = '2018-01-01'
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
+ subscription.start_date = "2018-01-01"
subscription.insert()
- subscription.process() # generate first invoice
+ subscription.process() # generate first invoice
# Status is unpaid as Days until Due is zero and grace period is Zero
- self.assertEqual(subscription.status, 'Unpaid')
+ self.assertEqual(subscription.status, "Unpaid")
subscription.cancel_subscription()
- self.assertEqual(subscription.status, 'Cancelled')
+ self.assertEqual(subscription.status, "Cancelled")
subscription.restart_subscription()
- self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(subscription.status, "Active")
self.assertEqual(len(subscription.invoices), 0)
subscription.process()
- self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(subscription.status, "Active")
self.assertEqual(len(subscription.invoices), 0)
subscription.process()
- self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(subscription.status, "Active")
self.assertEqual(len(subscription.invoices), 0)
settings.cancel_after_grace = default_grace_period_action
@@ -478,42 +493,42 @@ class TestSubscription(unittest.TestCase):
subscription.delete()
def test_subscription_unpaid_back_to_active(self):
- settings = frappe.get_single('Subscription Settings')
+ settings = frappe.get_single("Subscription Settings")
default_grace_period_action = settings.cancel_after_grace
settings.cancel_after_grace = 0
settings.save()
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
- subscription.start_date = '2018-01-01'
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
+ subscription.start_date = "2018-01-01"
subscription.insert()
- subscription.process() # generate first invoice
+ subscription.process() # generate first invoice
# This should change status to Unpaid since grace period is 0
- self.assertEqual(subscription.status, 'Unpaid')
+ self.assertEqual(subscription.status, "Unpaid")
invoice = subscription.get_current_invoice()
- invoice.db_set('outstanding_amount', 0)
- invoice.db_set('status', 'Paid')
+ invoice.db_set("outstanding_amount", 0)
+ invoice.db_set("status", "Paid")
subscription.process()
- self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(subscription.status, "Active")
# A new invoice is generated
subscription.process()
- self.assertEqual(subscription.status, 'Unpaid')
+ self.assertEqual(subscription.status, "Unpaid")
settings.cancel_after_grace = default_grace_period_action
settings.save()
subscription.delete()
def test_restart_active_subscription(self):
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
self.assertRaises(frappe.ValidationError, subscription.restart_subscription)
@@ -521,44 +536,44 @@ class TestSubscription(unittest.TestCase):
subscription.delete()
def test_subscription_invoice_discount_percentage(self):
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
subscription.additional_discount_percentage = 10
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
subscription.cancel_subscription()
invoice = subscription.get_current_invoice()
self.assertEqual(invoice.additional_discount_percentage, 10)
- self.assertEqual(invoice.apply_discount_on, 'Grand Total')
+ self.assertEqual(invoice.apply_discount_on, "Grand Total")
subscription.delete()
def test_subscription_invoice_discount_amount(self):
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
subscription.additional_discount_amount = 11
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
subscription.cancel_subscription()
invoice = subscription.get_current_invoice()
self.assertEqual(invoice.discount_amount, 11)
- self.assertEqual(invoice.apply_discount_on, 'Grand Total')
+ self.assertEqual(invoice.apply_discount_on, "Grand Total")
subscription.delete()
def test_prepaid_subscriptions(self):
# Create a non pre-billed subscription, processing should not create
# invoices.
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
subscription.process()
@@ -573,16 +588,16 @@ class TestSubscription(unittest.TestCase):
self.assertEqual(len(subscription.invoices), 1)
def test_prepaid_subscriptions_with_prorate_true(self):
- settings = frappe.get_single('Subscription Settings')
+ settings = frappe.get_single("Subscription Settings")
to_prorate = settings.prorate
settings.prorate = 1
settings.save()
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Customer'
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Customer"
subscription.generate_invoice_at_period_start = True
- subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
subscription.process()
subscription.cancel_subscription()
@@ -602,38 +617,38 @@ class TestSubscription(unittest.TestCase):
subscription.delete()
def test_subscription_with_follow_calendar_months(self):
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Supplier'
- subscription.party = '_Test Supplier'
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Supplier"
+ subscription.party = "_Test Supplier"
subscription.generate_invoice_at_period_start = 1
subscription.follow_calendar_months = 1
# select subscription start date as '2018-01-15'
- subscription.start_date = '2018-01-15'
- subscription.end_date = '2018-07-15'
- subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1})
+ subscription.start_date = "2018-01-15"
+ subscription.end_date = "2018-07-15"
+ subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
subscription.save()
# even though subscription starts at '2018-01-15' and Billing interval is Month and count 3
# First invoice will end at '2018-03-31' instead of '2018-04-14'
- self.assertEqual(get_date_str(subscription.current_invoice_end), '2018-03-31')
+ self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31")
def test_subscription_generate_invoice_past_due(self):
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Supplier'
- subscription.party = '_Test Supplier'
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Supplier"
+ subscription.party = "_Test Supplier"
subscription.generate_invoice_at_period_start = 1
subscription.generate_new_invoices_past_due_date = 1
# select subscription start date as '2018-01-15'
- subscription.start_date = '2018-01-01'
- subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1})
+ subscription.start_date = "2018-01-01"
+ subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
subscription.save()
# Process subscription and create first invoice
# Subscription status will be unpaid since due date has already passed
subscription.process()
self.assertEqual(len(subscription.invoices), 1)
- self.assertEqual(subscription.status, 'Unpaid')
+ self.assertEqual(subscription.status, "Unpaid")
# Now the Subscription is unpaid
# Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in
@@ -643,39 +658,39 @@ class TestSubscription(unittest.TestCase):
self.assertEqual(len(subscription.invoices), 2)
def test_subscription_without_generate_invoice_past_due(self):
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Supplier'
- subscription.party = '_Test Supplier'
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Supplier"
+ subscription.party = "_Test Supplier"
subscription.generate_invoice_at_period_start = 1
# select subscription start date as '2018-01-15'
- subscription.start_date = '2018-01-01'
- subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1})
+ subscription.start_date = "2018-01-01"
+ subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
subscription.save()
# Process subscription and create first invoice
# Subscription status will be unpaid since due date has already passed
subscription.process()
self.assertEqual(len(subscription.invoices), 1)
- self.assertEqual(subscription.status, 'Unpaid')
+ self.assertEqual(subscription.status, "Unpaid")
subscription.process()
self.assertEqual(len(subscription.invoices), 1)
def test_multicurrency_subscription(self):
- subscription = frappe.new_doc('Subscription')
- subscription.party_type = 'Customer'
- subscription.party = '_Test Subscription Customer'
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = "Customer"
+ subscription.party = "_Test Subscription Customer"
subscription.generate_invoice_at_period_start = 1
- subscription.company = '_Test Company'
+ subscription.company = "_Test Company"
# select subscription start date as '2018-01-15'
- subscription.start_date = '2018-01-01'
- subscription.append('plans', {'plan': '_Test Plan Multicurrency', 'qty': 1})
+ subscription.start_date = "2018-01-01"
+ subscription.append("plans", {"plan": "_Test Plan Multicurrency", "qty": 1})
subscription.save()
subscription.process()
self.assertEqual(len(subscription.invoices), 1)
- self.assertEqual(subscription.status, 'Unpaid')
+ self.assertEqual(subscription.status, "Unpaid")
# Check the currency of the created invoice
- currency = frappe.db.get_value('Sales Invoice', subscription.invoices[0].invoice, 'currency')
- self.assertEqual(currency, 'USD')
\ No newline at end of file
+ currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].invoice, "currency")
+ self.assertEqual(currency, "USD")
diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py
index 1285343d196..a95e0a9c2da 100644
--- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py
+++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py
@@ -16,10 +16,13 @@ class SubscriptionPlan(Document):
def validate_interval_count(self):
if self.billing_interval_count < 1:
- frappe.throw(_('Billing Interval Count cannot be less than 1'))
+ frappe.throw(_("Billing Interval Count cannot be less than 1"))
+
@frappe.whitelist()
-def get_plan_rate(plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1):
+def get_plan_rate(
+ plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1
+):
plan = frappe.get_doc("Subscription Plan", plan)
if plan.price_determination == "Fixed Rate":
return plan.cost * prorate_factor
@@ -30,13 +33,19 @@ def get_plan_rate(plan, quantity=1, customer=None, start_date=None, end_date=Non
else:
customer_group = None
- price = get_price(item_code=plan.item, price_list=plan.price_list, customer_group=customer_group, company=None, qty=quantity)
+ price = get_price(
+ item_code=plan.item,
+ price_list=plan.price_list,
+ customer_group=customer_group,
+ company=None,
+ qty=quantity,
+ )
if not price:
return 0
else:
return price.price_list_rate * prorate_factor
- elif plan.price_determination == 'Monthly Rate':
+ elif plan.price_determination == "Monthly Rate":
start_date = getdate(start_date)
end_date = getdate(end_date)
@@ -44,15 +53,21 @@ def get_plan_rate(plan, quantity=1, customer=None, start_date=None, end_date=Non
cost = plan.cost * no_of_months
# Adjust cost if start or end date is not month start or end
- prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
+ prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
if prorate:
- prorate_factor = flt(date_diff(start_date, get_first_day(start_date)) / date_diff(
- get_last_day(start_date), get_first_day(start_date)), 1)
+ prorate_factor = flt(
+ date_diff(start_date, get_first_day(start_date))
+ / date_diff(get_last_day(start_date), get_first_day(start_date)),
+ 1,
+ )
- prorate_factor += flt(date_diff(get_last_day(end_date), end_date) / date_diff(
- get_last_day(end_date), get_first_day(end_date)), 1)
+ prorate_factor += flt(
+ date_diff(get_last_day(end_date), end_date)
+ / date_diff(get_last_day(end_date), get_first_day(end_date)),
+ 1,
+ )
- cost -= (plan.cost * prorate_factor)
+ cost -= plan.cost * prorate_factor
return cost
diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan_dashboard.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan_dashboard.py
index 15df62daa23..7df76cde80c 100644
--- a/erpnext/accounts/doctype/subscription_plan/subscription_plan_dashboard.py
+++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan_dashboard.py
@@ -1,18 +1,9 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'subscription_plan',
- 'non_standard_fieldnames': {
- 'Payment Request': 'plan',
- 'Subscription': 'plan'
- },
- 'transactions': [
- {
- 'label': _('References'),
- 'items': ['Payment Request', 'Subscription']
- }
- ]
+ "fieldname": "subscription_plan",
+ "non_standard_fieldnames": {"Payment Request": "plan", "Subscription": "plan"},
+ "transactions": [{"label": _("References"), "items": ["Payment Request", "Subscription"]}],
}
diff --git a/erpnext/accounts/doctype/tax_category/tax_category_dashboard.py b/erpnext/accounts/doctype/tax_category/tax_category_dashboard.py
index c9d52da78ad..17a275ebc34 100644
--- a/erpnext/accounts/doctype/tax_category/tax_category_dashboard.py
+++ b/erpnext/accounts/doctype/tax_category/tax_category_dashboard.py
@@ -1,30 +1,14 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'tax_category',
- 'transactions': [
- {
- 'label': _('Pre Sales'),
- 'items': ['Quotation', 'Supplier Quotation']
- },
- {
- 'label': _('Sales'),
- 'items': ['Sales Invoice', 'Delivery Note', 'Sales Order']
- },
- {
- 'label': _('Purchase'),
- 'items': ['Purchase Invoice', 'Purchase Receipt']
- },
- {
- 'label': _('Party'),
- 'items': ['Customer', 'Supplier']
- },
- {
- 'label': _('Taxes'),
- 'items': ['Item', 'Tax Rule']
- }
- ]
+ "fieldname": "tax_category",
+ "transactions": [
+ {"label": _("Pre Sales"), "items": ["Quotation", "Supplier Quotation"]},
+ {"label": _("Sales"), "items": ["Sales Invoice", "Delivery Note", "Sales Order"]},
+ {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Receipt"]},
+ {"label": _("Party"), "items": ["Customer", "Supplier"]},
+ {"label": _("Taxes"), "items": ["Item", "Tax Rule"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py
index ce64d222856..a21d48415fd 100644
--- a/erpnext/accounts/doctype/tax_rule/tax_rule.py
+++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py
@@ -16,9 +16,17 @@ from six import iteritems
from erpnext.setup.doctype.customer_group.customer_group import get_parent_customer_groups
-class IncorrectCustomerGroup(frappe.ValidationError): pass
-class IncorrectSupplierType(frappe.ValidationError): pass
-class ConflictingTaxRule(frappe.ValidationError): pass
+class IncorrectCustomerGroup(frappe.ValidationError):
+ pass
+
+
+class IncorrectSupplierType(frappe.ValidationError):
+ pass
+
+
+class ConflictingTaxRule(frappe.ValidationError):
+ pass
+
class TaxRule(Document):
def __setup__(self):
@@ -31,7 +39,7 @@ class TaxRule(Document):
self.validate_use_for_shopping_cart()
def validate_tax_template(self):
- if self.tax_type== "Sales":
+ if self.tax_type == "Sales":
self.purchase_tax_template = self.supplier = self.supplier_group = None
if self.customer:
self.customer_group = None
@@ -51,28 +59,28 @@ class TaxRule(Document):
def validate_filters(self):
filters = {
- "tax_type": self.tax_type,
- "customer": self.customer,
- "customer_group": self.customer_group,
- "supplier": self.supplier,
- "supplier_group": self.supplier_group,
- "item": self.item,
- "item_group": self.item_group,
- "billing_city": self.billing_city,
- "billing_county": self.billing_county,
- "billing_state": self.billing_state,
- "billing_zipcode": self.billing_zipcode,
- "billing_country": self.billing_country,
- "shipping_city": self.shipping_city,
- "shipping_county": self.shipping_county,
- "shipping_state": self.shipping_state,
- "shipping_zipcode": self.shipping_zipcode,
- "shipping_country": self.shipping_country,
- "tax_category": self.tax_category,
- "company": self.company
+ "tax_type": self.tax_type,
+ "customer": self.customer,
+ "customer_group": self.customer_group,
+ "supplier": self.supplier,
+ "supplier_group": self.supplier_group,
+ "item": self.item,
+ "item_group": self.item_group,
+ "billing_city": self.billing_city,
+ "billing_county": self.billing_county,
+ "billing_state": self.billing_state,
+ "billing_zipcode": self.billing_zipcode,
+ "billing_country": self.billing_country,
+ "shipping_city": self.shipping_city,
+ "shipping_county": self.shipping_county,
+ "shipping_state": self.shipping_state,
+ "shipping_zipcode": self.shipping_zipcode,
+ "shipping_country": self.shipping_country,
+ "tax_category": self.tax_category,
+ "company": self.company,
}
- conds=""
+ conds = ""
for d in filters:
if conds:
conds += " and "
@@ -82,85 +90,112 @@ class TaxRule(Document):
conds += """ and ((from_date > '{from_date}' and from_date < '{to_date}') or
(to_date > '{from_date}' and to_date < '{to_date}') or
('{from_date}' > from_date and '{from_date}' < to_date) or
- ('{from_date}' = from_date and '{to_date}' = to_date))""".format(from_date=self.from_date, to_date=self.to_date)
+ ('{from_date}' = from_date and '{to_date}' = to_date))""".format(
+ from_date=self.from_date, to_date=self.to_date
+ )
elif self.from_date and not self.to_date:
- conds += """ and to_date > '{from_date}'""".format(from_date = self.from_date)
+ conds += """ and to_date > '{from_date}'""".format(from_date=self.from_date)
elif self.to_date and not self.from_date:
- conds += """ and from_date < '{to_date}'""".format(to_date = self.to_date)
+ conds += """ and from_date < '{to_date}'""".format(to_date=self.to_date)
- tax_rule = frappe.db.sql("select name, priority \
- from `tabTax Rule` where {0} and name != '{1}'".format(conds, self.name), as_dict=1)
+ tax_rule = frappe.db.sql(
+ "select name, priority \
+ from `tabTax Rule` where {0} and name != '{1}'".format(
+ conds, self.name
+ ),
+ as_dict=1,
+ )
if tax_rule:
if tax_rule[0].priority == self.priority:
frappe.throw(_("Tax Rule Conflicts with {0}").format(tax_rule[0].name), ConflictingTaxRule)
def validate_use_for_shopping_cart(self):
- '''If shopping cart is enabled and no tax rule exists for shopping cart, enable this one'''
- if (not self.use_for_shopping_cart
- and cint(frappe.db.get_single_value('E Commerce Settings', 'enabled'))
- and not frappe.db.get_value('Tax Rule', {'use_for_shopping_cart': 1, 'name': ['!=', self.name]})):
+ """If shopping cart is enabled and no tax rule exists for shopping cart, enable this one"""
+ if (
+ not self.use_for_shopping_cart
+ and cint(frappe.db.get_single_value("E Commerce Settings", "enabled"))
+ and not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1, "name": ["!=", self.name]})
+ ):
self.use_for_shopping_cart = 1
- frappe.msgprint(_("Enabling 'Use for Shopping Cart', as Shopping Cart is enabled and there should be at least one Tax Rule for Shopping Cart"))
+ frappe.msgprint(
+ _(
+ "Enabling 'Use for Shopping Cart', as Shopping Cart is enabled and there should be at least one Tax Rule for Shopping Cart"
+ )
+ )
+
@frappe.whitelist()
def get_party_details(party, party_type, args=None):
out = {}
billing_address, shipping_address = None, None
if args:
- if args.get('billing_address'):
- billing_address = frappe.get_doc('Address', args.get('billing_address'))
- if args.get('shipping_address'):
- shipping_address = frappe.get_doc('Address', args.get('shipping_address'))
+ if args.get("billing_address"):
+ billing_address = frappe.get_doc("Address", args.get("billing_address"))
+ if args.get("shipping_address"):
+ shipping_address = frappe.get_doc("Address", args.get("shipping_address"))
else:
billing_address_name = get_default_address(party_type, party)
- shipping_address_name = get_default_address(party_type, party, 'is_shipping_address')
+ shipping_address_name = get_default_address(party_type, party, "is_shipping_address")
if billing_address_name:
- billing_address = frappe.get_doc('Address', billing_address_name)
+ billing_address = frappe.get_doc("Address", billing_address_name)
if shipping_address_name:
- shipping_address = frappe.get_doc('Address', shipping_address_name)
+ shipping_address = frappe.get_doc("Address", shipping_address_name)
if billing_address:
- out["billing_city"]= billing_address.city
- out["billing_county"]= billing_address.county
- out["billing_state"]= billing_address.state
- out["billing_zipcode"]= billing_address.pincode
- out["billing_country"]= billing_address.country
+ out["billing_city"] = billing_address.city
+ out["billing_county"] = billing_address.county
+ out["billing_state"] = billing_address.state
+ out["billing_zipcode"] = billing_address.pincode
+ out["billing_country"] = billing_address.country
if shipping_address:
- out["shipping_city"]= shipping_address.city
- out["shipping_county"]= shipping_address.county
- out["shipping_state"]= shipping_address.state
- out["shipping_zipcode"]= shipping_address.pincode
- out["shipping_country"]= shipping_address.country
+ out["shipping_city"] = shipping_address.city
+ out["shipping_county"] = shipping_address.county
+ out["shipping_state"] = shipping_address.state
+ out["shipping_zipcode"] = shipping_address.pincode
+ out["shipping_country"] = shipping_address.country
return out
+
def get_tax_template(posting_date, args):
"""Get matching tax rule"""
args = frappe._dict(args)
- conditions = ["""(from_date is null or from_date <= '{0}')
- and (to_date is null or to_date >= '{0}')""".format(posting_date)]
+ conditions = [
+ """(from_date is null or from_date <= '{0}')
+ and (to_date is null or to_date >= '{0}')""".format(
+ posting_date
+ )
+ ]
- conditions.append("ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category")))))
- if 'tax_category' in args.keys():
- del args['tax_category']
+ conditions.append(
+ "ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category"))))
+ )
+ if "tax_category" in args.keys():
+ del args["tax_category"]
for key, value in iteritems(args):
- if key=="use_for_shopping_cart":
+ if key == "use_for_shopping_cart":
conditions.append("use_for_shopping_cart = {0}".format(1 if value else 0))
- elif key == 'customer_group':
- if not value: value = get_root_of("Customer Group")
+ elif key == "customer_group":
+ if not value:
+ value = get_root_of("Customer Group")
customer_group_condition = get_customer_group_condition(value)
conditions.append("ifnull({0}, '') in ('', {1})".format(key, customer_group_condition))
else:
conditions.append("ifnull({0}, '') in ('', {1})".format(key, frappe.db.escape(cstr(value))))
- tax_rule = frappe.db.sql("""select * from `tabTax Rule`
- where {0}""".format(" and ".join(conditions)), as_dict = True)
+ tax_rule = frappe.db.sql(
+ """select * from `tabTax Rule`
+ where {0}""".format(
+ " and ".join(conditions)
+ ),
+ as_dict=True,
+ )
if not tax_rule:
return None
@@ -168,24 +203,30 @@ def get_tax_template(posting_date, args):
for rule in tax_rule:
rule.no_of_keys_matched = 0
for key in args:
- if rule.get(key): rule.no_of_keys_matched += 1
+ if rule.get(key):
+ rule.no_of_keys_matched += 1
- rule = sorted(tax_rule,
- key = functools.cmp_to_key(lambda b, a:
- cmp(a.no_of_keys_matched, b.no_of_keys_matched) or
- cmp(a.priority, b.priority)))[0]
+ rule = sorted(
+ tax_rule,
+ key=functools.cmp_to_key(
+ lambda b, a: cmp(a.no_of_keys_matched, b.no_of_keys_matched) or cmp(a.priority, b.priority)
+ ),
+ )[0]
tax_template = rule.sales_tax_template or rule.purchase_tax_template
doctype = "{0} Taxes and Charges Template".format(rule.tax_type)
- if frappe.db.get_value(doctype, tax_template, 'disabled')==1:
+ if frappe.db.get_value(doctype, tax_template, "disabled") == 1:
return None
return tax_template
+
def get_customer_group_condition(customer_group):
condition = ""
- customer_groups = ["%s"%(frappe.db.escape(d.name)) for d in get_parent_customer_groups(customer_group)]
+ customer_groups = [
+ "%s" % (frappe.db.escape(d.name)) for d in get_parent_customer_groups(customer_group)
+ ]
if customer_groups:
- condition = ",".join(['%s'] * len(customer_groups))%(tuple(customer_groups))
+ condition = ",".join(["%s"] * len(customer_groups)) % (tuple(customer_groups))
return condition
diff --git a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py
index 44344bb7635..2cb9564875f 100644
--- a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py
+++ b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py
@@ -9,7 +9,7 @@ from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule, get_t
from erpnext.crm.doctype.opportunity.opportunity import make_quotation
from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity
-test_records = frappe.get_test_records('Tax Rule')
+test_records = frappe.get_test_records("Tax Rule")
from six import iteritems
@@ -27,40 +27,70 @@ class TestTaxRule(unittest.TestCase):
frappe.db.sql("delete from `tabTax Rule`")
def test_conflict(self):
- tax_rule1 = make_tax_rule(customer= "_Test Customer",
- sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1)
+ tax_rule1 = make_tax_rule(
+ customer="_Test Customer",
+ sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
+ priority=1,
+ )
tax_rule1.save()
- tax_rule2 = make_tax_rule(customer= "_Test Customer",
- sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1)
+ tax_rule2 = make_tax_rule(
+ customer="_Test Customer",
+ sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
+ priority=1,
+ )
self.assertRaises(ConflictingTaxRule, tax_rule2.save)
def test_conflict_with_non_overlapping_dates(self):
- tax_rule1 = make_tax_rule(customer= "_Test Customer",
- sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01")
+ tax_rule1 = make_tax_rule(
+ customer="_Test Customer",
+ sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
+ priority=1,
+ from_date="2015-01-01",
+ )
tax_rule1.save()
- tax_rule2 = make_tax_rule(customer= "_Test Customer",
- sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, to_date = "2013-01-01")
+ tax_rule2 = make_tax_rule(
+ customer="_Test Customer",
+ sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
+ priority=1,
+ to_date="2013-01-01",
+ )
tax_rule2.save()
self.assertTrue(tax_rule2.name)
def test_for_parent_customer_group(self):
- tax_rule1 = make_tax_rule(customer_group= "All Customer Groups",
- sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01")
+ tax_rule1 = make_tax_rule(
+ customer_group="All Customer Groups",
+ sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
+ priority=1,
+ from_date="2015-01-01",
+ )
tax_rule1.save()
- self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":1}),
- "_Test Sales Taxes and Charges Template - _TC")
+ self.assertEqual(
+ get_tax_template("2015-01-01", {"customer_group": "Commercial", "use_for_shopping_cart": 1}),
+ "_Test Sales Taxes and Charges Template - _TC",
+ )
def test_conflict_with_overlapping_dates(self):
- tax_rule1 = make_tax_rule(customer= "_Test Customer",
- sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01", to_date = "2015-01-05")
+ tax_rule1 = make_tax_rule(
+ customer="_Test Customer",
+ sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
+ priority=1,
+ from_date="2015-01-01",
+ to_date="2015-01-05",
+ )
tax_rule1.save()
- tax_rule2 = make_tax_rule(customer= "_Test Customer",
- sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-03", to_date = "2015-01-09")
+ tax_rule2 = make_tax_rule(
+ customer="_Test Customer",
+ sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
+ priority=1,
+ from_date="2015-01-03",
+ to_date="2015-01-09",
+ )
self.assertRaises(ConflictingTaxRule, tax_rule2.save)
@@ -68,93 +98,186 @@ class TestTaxRule(unittest.TestCase):
tax_rule = make_tax_rule()
self.assertEqual(tax_rule.purchase_tax_template, None)
-
def test_select_tax_rule_based_on_customer(self):
- make_tax_rule(customer= "_Test Customer",
- sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1)
+ make_tax_rule(
+ customer="_Test Customer",
+ sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
+ save=1,
+ )
- make_tax_rule(customer= "_Test Customer 1",
- sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", save=1)
+ make_tax_rule(
+ customer="_Test Customer 1",
+ sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC",
+ save=1,
+ )
- make_tax_rule(customer= "_Test Customer 2",
- sales_tax_template = "_Test Sales Taxes and Charges Template 2 - _TC", save=1)
+ make_tax_rule(
+ customer="_Test Customer 2",
+ sales_tax_template="_Test Sales Taxes and Charges Template 2 - _TC",
+ save=1,
+ )
- self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer 2"}),
- "_Test Sales Taxes and Charges Template 2 - _TC")
+ self.assertEqual(
+ get_tax_template("2015-01-01", {"customer": "_Test Customer 2"}),
+ "_Test Sales Taxes and Charges Template 2 - _TC",
+ )
def test_select_tax_rule_based_on_tax_category(self):
- make_tax_rule(customer="_Test Customer", tax_category="_Test Tax Category 1",
- sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", save=1)
+ make_tax_rule(
+ customer="_Test Customer",
+ tax_category="_Test Tax Category 1",
+ sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC",
+ save=1,
+ )
- make_tax_rule(customer="_Test Customer", tax_category="_Test Tax Category 2",
- sales_tax_template="_Test Sales Taxes and Charges Template 2 - _TC", save=1)
+ make_tax_rule(
+ customer="_Test Customer",
+ tax_category="_Test Tax Category 2",
+ sales_tax_template="_Test Sales Taxes and Charges Template 2 - _TC",
+ save=1,
+ )
self.assertFalse(get_tax_template("2015-01-01", {"customer": "_Test Customer"}))
- self.assertEqual(get_tax_template("2015-01-01", {"customer": "_Test Customer", "tax_category": "_Test Tax Category 1"}),
- "_Test Sales Taxes and Charges Template 1 - _TC")
- self.assertEqual(get_tax_template("2015-01-01", {"customer": "_Test Customer", "tax_category": "_Test Tax Category 2"}),
- "_Test Sales Taxes and Charges Template 2 - _TC")
+ self.assertEqual(
+ get_tax_template(
+ "2015-01-01", {"customer": "_Test Customer", "tax_category": "_Test Tax Category 1"}
+ ),
+ "_Test Sales Taxes and Charges Template 1 - _TC",
+ )
+ self.assertEqual(
+ get_tax_template(
+ "2015-01-01", {"customer": "_Test Customer", "tax_category": "_Test Tax Category 2"}
+ ),
+ "_Test Sales Taxes and Charges Template 2 - _TC",
+ )
- make_tax_rule(customer="_Test Customer", tax_category="",
- sales_tax_template="_Test Sales Taxes and Charges Template - _TC", save=1)
+ make_tax_rule(
+ customer="_Test Customer",
+ tax_category="",
+ sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
+ save=1,
+ )
- self.assertEqual(get_tax_template("2015-01-01", {"customer": "_Test Customer"}),
- "_Test Sales Taxes and Charges Template - _TC")
+ self.assertEqual(
+ get_tax_template("2015-01-01", {"customer": "_Test Customer"}),
+ "_Test Sales Taxes and Charges Template - _TC",
+ )
def test_select_tax_rule_based_on_better_match(self):
- make_tax_rule(customer= "_Test Customer", billing_city = "Test City", billing_state = "Test State",
- sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1)
+ make_tax_rule(
+ customer="_Test Customer",
+ billing_city="Test City",
+ billing_state="Test State",
+ sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
+ save=1,
+ )
- make_tax_rule(customer= "_Test Customer", billing_city = "Test City1", billing_state = "Test State",
- sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", save=1)
+ make_tax_rule(
+ customer="_Test Customer",
+ billing_city="Test City1",
+ billing_state="Test State",
+ sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC",
+ save=1,
+ )
- self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City", "billing_state": "Test State"}),
- "_Test Sales Taxes and Charges Template - _TC")
+ self.assertEqual(
+ get_tax_template(
+ "2015-01-01",
+ {"customer": "_Test Customer", "billing_city": "Test City", "billing_state": "Test State"},
+ ),
+ "_Test Sales Taxes and Charges Template - _TC",
+ )
def test_select_tax_rule_based_on_state_match(self):
- make_tax_rule(customer= "_Test Customer", shipping_state = "Test State",
- sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1)
+ make_tax_rule(
+ customer="_Test Customer",
+ shipping_state="Test State",
+ sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
+ save=1,
+ )
- make_tax_rule(customer= "_Test Customer", shipping_state = "Test State12",
- sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", priority=2, save=1)
+ make_tax_rule(
+ customer="_Test Customer",
+ shipping_state="Test State12",
+ sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC",
+ priority=2,
+ save=1,
+ )
- self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "shipping_state": "Test State"}),
- "_Test Sales Taxes and Charges Template - _TC")
+ self.assertEqual(
+ get_tax_template("2015-01-01", {"customer": "_Test Customer", "shipping_state": "Test State"}),
+ "_Test Sales Taxes and Charges Template - _TC",
+ )
def test_select_tax_rule_based_on_better_priority(self):
- make_tax_rule(customer= "_Test Customer", billing_city = "Test City",
- sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority=1, save=1)
+ make_tax_rule(
+ customer="_Test Customer",
+ billing_city="Test City",
+ sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
+ priority=1,
+ save=1,
+ )
- make_tax_rule(customer= "_Test Customer", billing_city = "Test City",
- sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", priority=2, save=1)
+ make_tax_rule(
+ customer="_Test Customer",
+ billing_city="Test City",
+ sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC",
+ priority=2,
+ save=1,
+ )
- self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City"}),
- "_Test Sales Taxes and Charges Template 1 - _TC")
+ self.assertEqual(
+ get_tax_template("2015-01-01", {"customer": "_Test Customer", "billing_city": "Test City"}),
+ "_Test Sales Taxes and Charges Template 1 - _TC",
+ )
def test_select_tax_rule_based_cross_matching_keys(self):
- make_tax_rule(customer= "_Test Customer", billing_city = "Test City",
- sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1)
+ make_tax_rule(
+ customer="_Test Customer",
+ billing_city="Test City",
+ sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
+ save=1,
+ )
- make_tax_rule(customer= "_Test Customer 1", billing_city = "Test City 1",
- sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", save=1)
+ make_tax_rule(
+ customer="_Test Customer 1",
+ billing_city="Test City 1",
+ sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC",
+ save=1,
+ )
- self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City 1"}),
- None)
+ self.assertEqual(
+ get_tax_template("2015-01-01", {"customer": "_Test Customer", "billing_city": "Test City 1"}),
+ None,
+ )
def test_select_tax_rule_based_cross_partially_keys(self):
- make_tax_rule(customer= "_Test Customer", billing_city = "Test City",
- sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1)
+ make_tax_rule(
+ customer="_Test Customer",
+ billing_city="Test City",
+ sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
+ save=1,
+ )
- make_tax_rule(billing_city = "Test City 1",
- sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", save=1)
+ make_tax_rule(
+ billing_city="Test City 1",
+ sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC",
+ save=1,
+ )
- self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City 1"}),
- "_Test Sales Taxes and Charges Template 1 - _TC")
+ self.assertEqual(
+ get_tax_template("2015-01-01", {"customer": "_Test Customer", "billing_city": "Test City 1"}),
+ "_Test Sales Taxes and Charges Template 1 - _TC",
+ )
def test_taxes_fetch_via_tax_rule(self):
- make_tax_rule(customer= "_Test Customer", billing_city = "_Test City",
- sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1)
+ make_tax_rule(
+ customer="_Test Customer",
+ billing_city="_Test City",
+ sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
+ save=1,
+ )
# create opportunity for customer
opportunity = make_opportunity(with_items=1)
@@ -169,7 +292,6 @@ class TestTaxRule(unittest.TestCase):
self.assertTrue(len(quotation.taxes) > 0)
-
def make_tax_rule(**args):
args = frappe._dict(args)
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index f33aa31e161..63698439be1 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -15,7 +15,7 @@ class TaxWithholdingCategory(Document):
def validate_dates(self):
last_date = None
- for d in self.get('rates'):
+ for d in self.get("rates"):
if getdate(d.from_date) >= getdate(d.to_date):
frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx))
@@ -24,18 +24,25 @@ class TaxWithholdingCategory(Document):
frappe.throw(_("Row #{0}: Dates overlapping with other row").format(d.idx))
def validate_thresholds(self):
- for d in self.get('rates'):
- if d.cumulative_threshold and d.cumulative_threshold < d.single_threshold:
- frappe.throw(_("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format(d.idx))
+ for d in self.get("rates"):
+ if (
+ d.cumulative_threshold and d.single_threshold and d.cumulative_threshold < d.single_threshold
+ ):
+ frappe.throw(
+ _("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format(
+ d.idx
+ )
+ )
+
def get_party_details(inv):
- party_type, party = '', ''
+ party_type, party = "", ""
- if inv.doctype == 'Sales Invoice':
- party_type = 'Customer'
+ if inv.doctype == "Sales Invoice":
+ party_type = "Customer"
party = inv.customer
else:
- party_type = 'Supplier'
+ party_type = "Supplier"
party = inv.supplier
if not party:
@@ -43,65 +50,71 @@ def get_party_details(inv):
return party_type, party
+
def get_party_tax_withholding_details(inv, tax_withholding_category=None):
- pan_no = ''
+ pan_no = ""
parties = []
party_type, party = get_party_details(inv)
has_pan_field = frappe.get_meta(party_type).has_field("pan")
if not tax_withholding_category:
if has_pan_field:
- fields = ['tax_withholding_category', 'pan']
+ fields = ["tax_withholding_category", "pan"]
else:
- fields = ['tax_withholding_category']
+ fields = ["tax_withholding_category"]
tax_withholding_details = frappe.db.get_value(party_type, party, fields, as_dict=1)
- tax_withholding_category = tax_withholding_details.get('tax_withholding_category')
- pan_no = tax_withholding_details.get('pan')
+ tax_withholding_category = tax_withholding_details.get("tax_withholding_category")
+ pan_no = tax_withholding_details.get("pan")
if not tax_withholding_category:
return
# if tax_withholding_category passed as an argument but not pan_no
if not pan_no and has_pan_field:
- pan_no = frappe.db.get_value(party_type, party, 'pan')
+ pan_no = frappe.db.get_value(party_type, party, "pan")
# Get others suppliers with the same PAN No
if pan_no:
- parties = frappe.get_all(party_type, filters={ 'pan': pan_no }, pluck='name')
+ parties = frappe.get_all(party_type, filters={"pan": pan_no}, pluck="name")
if not parties:
parties.append(party)
- posting_date = inv.get('posting_date') or inv.get('transaction_date')
+ posting_date = inv.get("posting_date") or inv.get("transaction_date")
tax_details = get_tax_withholding_details(tax_withholding_category, posting_date, inv.company)
if not tax_details:
- frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}')
- .format(tax_withholding_category, inv.company))
+ frappe.throw(
+ _("Please set associated account in Tax Withholding Category {0} against Company {1}").format(
+ tax_withholding_category, inv.company
+ )
+ )
- if party_type == 'Customer' and not tax_details.cumulative_threshold:
+ if party_type == "Customer" and not tax_details.cumulative_threshold:
# TCS is only chargeable on sum of invoiced value
- frappe.throw(_('Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.')
- .format(tax_withholding_category, inv.company, party))
+ frappe.throw(
+ _(
+ "Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value."
+ ).format(tax_withholding_category, inv.company, party)
+ )
tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount(
- party_type, parties,
- inv, tax_details,
- posting_date, pan_no
+ party_type, parties, inv, tax_details, posting_date, pan_no
)
- if party_type == 'Supplier':
+ if party_type == "Supplier":
tax_row = get_tax_row_for_tds(tax_details, tax_amount)
else:
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
- if inv.doctype == 'Purchase Invoice':
+ if inv.doctype == "Purchase Invoice":
return tax_row, tax_deducted_on_advances
else:
return tax_row
+
def get_tax_withholding_details(tax_withholding_category, posting_date, company):
tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category)
@@ -109,19 +122,24 @@ def get_tax_withholding_details(tax_withholding_category, posting_date, company)
for account_detail in tax_withholding.accounts:
if company == account_detail.company:
- return frappe._dict({
- "tax_withholding_category": tax_withholding_category,
- "account_head": account_detail.account,
- "rate": tax_rate_detail.tax_withholding_rate,
- "from_date": tax_rate_detail.from_date,
- "to_date": tax_rate_detail.to_date,
- "threshold": tax_rate_detail.single_threshold,
- "cumulative_threshold": tax_rate_detail.cumulative_threshold,
- "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
- })
+ return frappe._dict(
+ {
+ "tax_withholding_category": tax_withholding_category,
+ "account_head": account_detail.account,
+ "rate": tax_rate_detail.tax_withholding_rate,
+ "from_date": tax_rate_detail.from_date,
+ "to_date": tax_rate_detail.to_date,
+ "threshold": tax_rate_detail.single_threshold,
+ "cumulative_threshold": tax_rate_detail.cumulative_threshold,
+ "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, posting_date):
# returns the row that matches with the fiscal year from posting date
@@ -131,13 +149,14 @@ def get_tax_withholding_rates(tax_withholding, posting_date):
frappe.throw(_("No Tax Withholding data found for the current posting date."))
+
def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted):
row = {
"category": "Total",
"charge_type": "Actual",
"tax_amount": tax_amount,
"description": tax_details.description,
- "account_head": tax_details.account_head
+ "account_head": tax_details.account_head,
}
if tax_deducted:
@@ -147,20 +166,20 @@ def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted):
taxes_excluding_tcs = [d for d in inv.taxes if d.account_head != tax_details.account_head]
if taxes_excluding_tcs:
# chargeable amount is the total amount after other charges are applied
- row.update({
- "charge_type": "On Previous Row Total",
- "row_id": len(taxes_excluding_tcs),
- "rate": tax_details.rate
- })
+ row.update(
+ {
+ "charge_type": "On Previous Row Total",
+ "row_id": len(taxes_excluding_tcs),
+ "rate": tax_details.rate,
+ }
+ )
else:
# if only TCS is to be charged, then net total is chargeable amount
- row.update({
- "charge_type": "On Net Total",
- "rate": tax_details.rate
- })
+ row.update({"charge_type": "On Net Total", "rate": tax_details.rate})
return row
+
def get_tax_row_for_tds(tax_details, tax_amount):
return {
"category": "Total",
@@ -168,29 +187,39 @@ def get_tax_row_for_tds(tax_details, tax_amount):
"tax_amount": tax_amount,
"add_deduct_tax": "Deduct",
"description": tax_details.description,
- "account_head": tax_details.account_head
+ "account_head": tax_details.account_head,
}
+
def get_lower_deduction_certificate(tax_details, pan_no):
- ldc_name = frappe.db.get_value('Lower Deduction Certificate',
+ ldc_name = frappe.db.get_value(
+ "Lower Deduction Certificate",
{
- 'pan_no': pan_no,
- 'tax_withholding_category': tax_details.tax_withholding_category,
- 'valid_from': ('>=', tax_details.from_date),
- 'valid_upto': ('<=', tax_details.to_date)
- }, 'name')
+ "pan_no": pan_no,
+ "tax_withholding_category": tax_details.tax_withholding_category,
+ "valid_from": (">=", tax_details.from_date),
+ "valid_upto": ("<=", tax_details.to_date),
+ },
+ "name",
+ )
if ldc_name:
- return frappe.get_doc('Lower Deduction Certificate', ldc_name)
+ return frappe.get_doc("Lower Deduction Certificate", ldc_name)
+
def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None):
vouchers = get_invoice_vouchers(parties, tax_details, inv.company, party_type=party_type)
- advance_vouchers = get_advance_vouchers(parties, company=inv.company, from_date=tax_details.from_date,
- to_date=tax_details.to_date, party_type=party_type)
+ advance_vouchers = get_advance_vouchers(
+ parties,
+ company=inv.company,
+ from_date=tax_details.from_date,
+ to_date=tax_details.to_date,
+ party_type=party_type,
+ )
taxable_vouchers = vouchers + advance_vouchers
tax_deducted_on_advances = 0
- if inv.doctype == 'Purchase Invoice':
+ if inv.doctype == "Purchase Invoice":
tax_deducted_on_advances = get_taxes_deducted_on_advances_allocated(inv, tax_details)
tax_deducted = 0
@@ -198,18 +227,20 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
tax_amount = 0
- if party_type == 'Supplier':
+ if party_type == "Supplier":
ldc = get_lower_deduction_certificate(tax_details, pan_no)
if tax_deducted:
net_total = inv.net_total
if ldc:
- tax_amount = get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total)
+ tax_amount = get_tds_amount_from_ldc(
+ ldc, parties, pan_no, tax_details, posting_date, net_total
+ )
else:
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
else:
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers)
- elif party_type == 'Customer':
+ elif party_type == "Customer":
if tax_deducted:
# if already TCS is charged, then amount will be calculated based on 'Previous Row Total'
tax_amount = 0
@@ -223,27 +254,28 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
return tax_amount, tax_deducted, tax_deducted_on_advances
-def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'):
- dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit'
- doctype = 'Purchase Invoice' if party_type == 'Supplier' else 'Sales Invoice'
+
+def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
+ dr_or_cr = "credit" if party_type == "Supplier" else "debit"
+ doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
filters = {
- 'company': company,
- frappe.scrub(party_type): ['in', parties],
- 'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
- 'is_opening': 'No',
- 'docstatus': 1
+ "company": company,
+ frappe.scrub(party_type): ["in", parties],
+ "posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
+ "is_opening": "No",
+ "docstatus": 1,
}
- if not tax_details.get('consider_party_ledger_amount') and doctype != "Sales Invoice":
- filters.update({
- 'apply_tds': 1,
- 'tax_withholding_category': tax_details.get('tax_withholding_category')
- })
+ if not tax_details.get("consider_party_ledger_amount") and doctype != "Sales Invoice":
+ filters.update(
+ {"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
+ )
invoices = frappe.get_all(doctype, filters=filters, pluck="name") or [""]
- journal_entries = frappe.db.sql("""
+ journal_entries = frappe.db.sql(
+ """
SELECT j.name
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
WHERE
@@ -252,52 +284,60 @@ def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'):
AND j.posting_date between %s and %s
AND ja.{dr_or_cr} > 0
AND ja.party in %s
- """.format(dr_or_cr=dr_or_cr), (tax_details.from_date, tax_details.to_date, tuple(parties)), as_list=1)
+ """.format(
+ dr_or_cr=dr_or_cr
+ ),
+ (tax_details.from_date, tax_details.to_date, tuple(parties)),
+ as_list=1,
+ )
if journal_entries:
journal_entries = journal_entries[0]
return invoices + journal_entries
-def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type='Supplier'):
+
+def get_advance_vouchers(
+ parties, company=None, from_date=None, to_date=None, party_type="Supplier"
+):
# for advance vouchers, debit and credit is reversed
- dr_or_cr = 'debit' if party_type == 'Supplier' else 'credit'
+ dr_or_cr = "debit" if party_type == "Supplier" else "credit"
filters = {
- dr_or_cr: ['>', 0],
- 'is_opening': 'No',
- 'is_cancelled': 0,
- 'party_type': party_type,
- 'party': ['in', parties],
- 'against_voucher': ['is', 'not set']
+ dr_or_cr: [">", 0],
+ "is_opening": "No",
+ "is_cancelled": 0,
+ "party_type": party_type,
+ "party": ["in", parties],
+ "against_voucher": ["is", "not set"],
}
if company:
- filters['company'] = company
+ filters["company"] = company
if from_date and to_date:
- filters['posting_date'] = ['between', (from_date, to_date)]
+ filters["posting_date"] = ["between", (from_date, to_date)]
+
+ return frappe.get_all("GL Entry", filters=filters, distinct=1, pluck="voucher_no") or [""]
- return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""]
def get_taxes_deducted_on_advances_allocated(inv, tax_details):
- advances = [d.reference_name for d in inv.get('advances')]
+ advances = [d.reference_name for d in inv.get("advances")]
tax_info = []
if advances:
pe = frappe.qb.DocType("Payment Entry").as_("pe")
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
- tax_info = frappe.qb.from_(at).inner_join(pe).on(
- pe.name == at.parent
- ).select(
- at.parent, at.name, at.tax_amount, at.allocated_amount
- ).where(
- pe.tax_withholding_category == tax_details.get('tax_withholding_category')
- ).where(
- at.parent.isin(advances)
- ).where(
- at.account_head == tax_details.account_head
- ).run(as_dict=True)
+ tax_info = (
+ frappe.qb.from_(at)
+ .inner_join(pe)
+ .on(pe.name == at.parent)
+ .select(at.parent, at.name, at.tax_amount, at.allocated_amount)
+ .where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
+ .where(at.parent.isin(advances))
+ .where(at.account_head == tax_details.account_head)
+ .run(as_dict=True)
+ )
return tax_info
@@ -305,59 +345,74 @@ def get_taxes_deducted_on_advances_allocated(inv, tax_details):
def get_deducted_tax(taxable_vouchers, tax_details):
# check if TDS / TCS account is already charged on taxable vouchers
filters = {
- 'is_cancelled': 0,
- 'credit': ['>', 0],
- 'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
- 'account': tax_details.account_head,
- 'voucher_no': ['in', taxable_vouchers],
+ "is_cancelled": 0,
+ "credit": [">", 0],
+ "posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
+ "account": tax_details.account_head,
+ "voucher_no": ["in", taxable_vouchers],
}
field = "credit"
- entries = frappe.db.get_all('GL Entry', filters, pluck=field)
+ entries = frappe.db.get_all("GL Entry", filters, pluck=field)
return sum(entries)
+
def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
tds_amount = 0
- invoice_filters = {
- 'name': ('in', vouchers),
- 'docstatus': 1,
- 'apply_tds': 1
- }
+ invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
- field = 'sum(net_total)'
+ field = "sum(net_total)"
if cint(tax_details.consider_party_ledger_amount):
- invoice_filters.pop('apply_tds', None)
- field = 'sum(grand_total)'
+ invoice_filters.pop("apply_tds", None)
+ field = "sum(grand_total)"
- supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0
+ 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', {
- 'parent': ('in', vouchers), 'docstatus': 1,
- 'party': ('in', parties), 'reference_type': ('!=', 'Purchase Invoice')
- }, 'sum(credit_in_account_currency)') or 0.0
+ supp_jv_credit_amt = (
+ frappe.db.get_value(
+ "Journal Entry Account",
+ {
+ "parent": ("in", vouchers),
+ "docstatus": 1,
+ "party": ("in", parties),
+ "reference_type": ("!=", "Purchase Invoice"),
+ },
+ "sum(credit_in_account_currency)",
+ )
+ or 0.0
+ )
supp_credit_amt += supp_jv_credit_amt
supp_credit_amt += inv.net_total
- debit_note_amount = get_debit_note_amount(parties, tax_details.from_date, tax_details.to_date, inv.company)
+ debit_note_amount = get_debit_note_amount(
+ parties, tax_details.from_date, tax_details.to_date, inv.company
+ )
supp_credit_amt -= debit_note_amount
- threshold = tax_details.get('threshold', 0)
- cumulative_threshold = tax_details.get('cumulative_threshold', 0)
+ threshold = tax_details.get("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 (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(tax_details.tax_on_excess_amount):
+ 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 = 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(
- ldc.valid_from, ldc.valid_upto,
- inv.get('posting_date') or inv.get('transaction_date'), tax_deducted,
- inv.net_total, ldc.certificate_limit
+ ldc.valid_from,
+ ldc.valid_upto,
+ inv.get("posting_date") or inv.get("transaction_date"),
+ tax_deducted,
+ inv.net_total,
+ ldc.certificate_limit,
):
tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
else:
@@ -365,98 +420,127 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
return tds_amount
+
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
tcs_amount = 0
# sum of debit entries made from sales invoices
- invoiced_amt = frappe.db.get_value('GL Entry', {
- 'is_cancelled': 0,
- 'party': ['in', parties],
- 'company': inv.company,
- 'voucher_no': ['in', vouchers],
- }, 'sum(debit)') or 0.0
+ invoiced_amt = (
+ frappe.db.get_value(
+ "GL Entry",
+ {
+ "is_cancelled": 0,
+ "party": ["in", parties],
+ "company": inv.company,
+ "voucher_no": ["in", vouchers],
+ },
+ "sum(debit)",
+ )
+ or 0.0
+ )
# sum of credit entries made from PE / JV with unset 'against voucher'
- advance_amt = frappe.db.get_value('GL Entry', {
- 'is_cancelled': 0,
- 'party': ['in', parties],
- 'company': inv.company,
- 'voucher_no': ['in', adv_vouchers],
- }, 'sum(credit)') or 0.0
+ advance_amt = (
+ frappe.db.get_value(
+ "GL Entry",
+ {
+ "is_cancelled": 0,
+ "party": ["in", parties],
+ "company": inv.company,
+ "voucher_no": ["in", adv_vouchers],
+ },
+ "sum(credit)",
+ )
+ or 0.0
+ )
# sum of credit entries made from sales invoice
- credit_note_amt = sum(frappe.db.get_all('GL Entry', {
- 'is_cancelled': 0,
- 'credit': ['>', 0],
- 'party': ['in', parties],
- 'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
- 'company': inv.company,
- 'voucher_type': 'Sales Invoice',
- }, pluck='credit'))
+ credit_note_amt = sum(
+ frappe.db.get_all(
+ "GL Entry",
+ {
+ "is_cancelled": 0,
+ "credit": [">", 0],
+ "party": ["in", parties],
+ "posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
+ "company": inv.company,
+ "voucher_type": "Sales Invoice",
+ },
+ pluck="credit",
+ )
+ )
- cumulative_threshold = tax_details.get('cumulative_threshold', 0)
+ cumulative_threshold = tax_details.get("cumulative_threshold", 0)
current_invoice_total = get_invoice_total_without_tcs(inv, tax_details)
total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt
- if ((cumulative_threshold and total_invoiced_amt >= cumulative_threshold)):
+ if cumulative_threshold and total_invoiced_amt >= cumulative_threshold:
chargeable_amt = total_invoiced_amt - cumulative_threshold
tcs_amount = chargeable_amt * tax_details.rate / 100 if chargeable_amt > 0 else 0
return tcs_amount
+
def get_invoice_total_without_tcs(inv, tax_details):
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
return inv.grand_total - tcs_tax_row_amount
+
def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total):
tds_amount = 0
- limit_consumed = frappe.db.get_value('Purchase Invoice', {
- 'supplier': ('in', parties),
- 'apply_tds': 1,
- 'docstatus': 1
- }, 'sum(net_total)')
+ limit_consumed = frappe.db.get_value(
+ "Purchase Invoice",
+ {"supplier": ("in", parties), "apply_tds": 1, "docstatus": 1},
+ "sum(net_total)",
+ )
if is_valid_certificate(
- ldc.valid_from, ldc.valid_upto,
- posting_date, limit_consumed,
- net_total, ldc.certificate_limit
+ ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total, ldc.certificate_limit
):
- tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details)
+ tds_amount = get_ltds_amount(
+ net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details
+ )
return tds_amount
+
def get_debit_note_amount(suppliers, from_date, to_date, company=None):
filters = {
- 'supplier': ['in', suppliers],
- 'is_return': 1,
- 'docstatus': 1,
- 'posting_date': ['between', (from_date, to_date)]
+ "supplier": ["in", suppliers],
+ "is_return": 1,
+ "docstatus": 1,
+ "posting_date": ["between", (from_date, to_date)],
}
- fields = ['abs(sum(net_total)) as net_total']
+ fields = ["abs(sum(net_total)) as net_total"]
if company:
- filters['company'] = company
+ filters["company"] = company
+
+ return frappe.get_all("Purchase Invoice", filters, fields)[0].get("net_total") or 0.0
- return frappe.get_all('Purchase Invoice', filters, fields)[0].get('net_total') or 0.0
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
if current_amount < (certificate_limit - deducted_amount):
- return current_amount * rate/100
+ return current_amount * rate / 100
else:
- ltds_amount = (certificate_limit - deducted_amount)
+ ltds_amount = certificate_limit - deducted_amount
tds_amount = current_amount - ltds_amount
- return ltds_amount * rate/100 + tds_amount * tax_details.rate/100
+ return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100
-def is_valid_certificate(valid_from, valid_upto, posting_date, deducted_amount, current_amount, certificate_limit):
+
+def is_valid_certificate(
+ valid_from, valid_upto, posting_date, deducted_amount, current_amount, certificate_limit
+):
valid = False
- if ((getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and
- certificate_limit > deducted_amount):
+ if (
+ getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)
+ ) and certificate_limit > deducted_amount:
valid = True
return valid
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category_dashboard.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category_dashboard.py
index 46d0c2e487e..8a510ea023f 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category_dashboard.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category_dashboard.py
@@ -1,11 +1,2 @@
-
-
def get_data():
- return {
- 'fieldname': 'tax_withholding_category',
- 'transactions': [
- {
- 'items': ['Supplier']
- }
- ]
- }
+ return {"fieldname": "tax_withholding_category", "transactions": [{"items": ["Supplier"]}]}
diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
index a3fcf7da7a5..3059f8d64b8 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
@@ -10,6 +10,7 @@ from erpnext.accounts.utils import get_fiscal_year
test_dependencies = ["Supplier Group", "Customer Group"]
+
class TestTaxWithholdingCategory(unittest.TestCase):
@classmethod
def setUpClass(self):
@@ -21,18 +22,20 @@ class TestTaxWithholdingCategory(unittest.TestCase):
cancel_invoices()
def test_cumulative_threshold_tds(self):
- frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS")
+ frappe.db.set_value(
+ "Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS"
+ )
invoices = []
# create invoices for lower than single threshold tax rate
for _ in range(2):
- pi = create_purchase_invoice(supplier = "Test TDS Supplier")
+ pi = create_purchase_invoice(supplier="Test TDS Supplier")
pi.submit()
invoices.append(pi)
# create another invoice whose total when added to previously created invoice,
# surpasses cumulative threshhold
- pi = create_purchase_invoice(supplier = "Test TDS Supplier")
+ pi = create_purchase_invoice(supplier="Test TDS Supplier")
pi.submit()
# assert equal tax deduction on total invoice amount uptil now
@@ -41,21 +44,23 @@ class TestTaxWithholdingCategory(unittest.TestCase):
invoices.append(pi)
# TDS is already deducted, so from onward system will deduct the TDS on every invoice
- pi = create_purchase_invoice(supplier = "Test TDS Supplier", rate=5000)
+ pi = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000)
pi.submit()
# assert equal tax deduction on total invoice amount uptil now
self.assertEqual(pi.taxes_and_charges_deducted, 500)
invoices.append(pi)
- #delete invoices to avoid clashing
+ # delete invoices to avoid clashing
for d in invoices:
d.cancel()
def test_single_threshold_tds(self):
invoices = []
- frappe.db.set_value("Supplier", "Test TDS Supplier1", "tax_withholding_category", "Single Threshold TDS")
- pi = create_purchase_invoice(supplier = "Test TDS Supplier1", rate = 20000)
+ frappe.db.set_value(
+ "Supplier", "Test TDS Supplier1", "tax_withholding_category", "Single Threshold TDS"
+ )
+ pi = create_purchase_invoice(supplier="Test TDS Supplier1", rate=20000)
pi.submit()
invoices.append(pi)
@@ -63,7 +68,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
self.assertEqual(pi.grand_total, 18000)
# check gl entry for the purchase invoice
- gl_entries = frappe.db.get_all('GL Entry', filters={'voucher_no': pi.name}, fields=["*"])
+ gl_entries = frappe.db.get_all("GL Entry", filters={"voucher_no": pi.name}, fields=["*"])
self.assertEqual(len(gl_entries), 3)
for d in gl_entries:
if d.account == pi.credit_to:
@@ -75,7 +80,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
else:
raise ValueError("Account head does not match.")
- pi = create_purchase_invoice(supplier = "Test TDS Supplier1")
+ pi = create_purchase_invoice(supplier="Test TDS Supplier1")
pi.submit()
invoices.append(pi)
@@ -88,17 +93,19 @@ class TestTaxWithholdingCategory(unittest.TestCase):
def test_tax_withholding_category_checks(self):
invoices = []
- frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category")
+ 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 = 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 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=20000)
pi1.submit()
invoices.append(pi1)
@@ -110,82 +117,89 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices:
d.cancel()
-
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 = []
# create invoices for lower than single threshold tax rate
for _ in range(2):
- si = create_sales_invoice(customer = "Test TCS Customer")
+ si = create_sales_invoice(customer="Test TCS Customer")
si.submit()
invoices.append(si)
# create another invoice whose total when added to previously created invoice,
# surpasses cumulative threshhold
- si = create_sales_invoice(customer = "Test TCS Customer", rate=12000)
+ si = create_sales_invoice(customer="Test TCS Customer", rate=12000)
si.submit()
# assert tax collection on total invoice amount created until now
- tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC'])
+ tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == "TCS - _TC"])
self.assertEqual(tcs_charged, 200)
self.assertEqual(si.grand_total, 12200)
invoices.append(si)
# TCS is already collected once, so going forward system will collect TCS on every invoice
- si = create_sales_invoice(customer = "Test TCS Customer", rate=5000)
+ si = create_sales_invoice(customer="Test TCS Customer", rate=5000)
si.submit()
- tcs_charged = sum(d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC')
+ tcs_charged = sum(d.base_tax_amount for d in si.taxes if d.account_head == "TCS - _TC")
self.assertEqual(tcs_charged, 500)
invoices.append(si)
- #delete invoices to avoid clashing
+ # delete invoices to avoid clashing
for d in invoices:
d.cancel()
def test_tds_calculation_on_net_total(self):
- frappe.db.set_value("Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS")
+ frappe.db.set_value(
+ "Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS"
+ )
invoices = []
- pi = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000, do_not_save=True)
- pi.append('taxes', {
- "category": "Total",
- "charge_type": "Actual",
- "account_head": '_Test Account VAT - _TC',
- "cost_center": 'Main - _TC',
- "tax_amount": 1000,
- "description": "Test",
- "add_deduct_tax": "Add"
-
- })
+ pi = create_purchase_invoice(supplier="Test TDS Supplier4", rate=20000, do_not_save=True)
+ pi.append(
+ "taxes",
+ {
+ "category": "Total",
+ "charge_type": "Actual",
+ "account_head": "_Test Account VAT - _TC",
+ "cost_center": "Main - _TC",
+ "tax_amount": 1000,
+ "description": "Test",
+ "add_deduct_tax": "Add",
+ },
+ )
pi.save()
pi.submit()
invoices.append(pi)
# Second Invoice will apply TDS checked
- pi1 = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000)
+ pi1 = create_purchase_invoice(supplier="Test TDS Supplier4", rate=20000)
pi1.submit()
invoices.append(pi1)
self.assertEqual(pi1.taxes[0].tax_amount, 4000)
- #delete invoices to avoid clashing
+ # delete invoices to avoid clashing
for d in invoices:
d.cancel()
def test_multi_category_single_supplier(self):
- frappe.db.set_value("Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category")
+ frappe.db.set_value(
+ "Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category"
+ )
invoices = []
- pi = create_purchase_invoice(supplier = "Test TDS Supplier5", rate = 500, do_not_save=True)
+ pi = create_purchase_invoice(supplier="Test TDS Supplier5", rate=500, do_not_save=True)
pi.tax_withholding_category = "Test Service Category"
pi.save()
pi.submit()
invoices.append(pi)
# Second Invoice will apply TDS checked
- pi1 = create_purchase_invoice(supplier = "Test TDS Supplier5", rate = 2500, do_not_save=True)
+ pi1 = create_purchase_invoice(supplier="Test TDS Supplier5", rate=2500, do_not_save=True)
pi1.tax_withholding_category = "Test Goods Category"
pi1.save()
pi1.submit()
@@ -193,258 +207,294 @@ class TestTaxWithholdingCategory(unittest.TestCase):
self.assertEqual(pi1.taxes[0].tax_amount, 250)
- #delete invoices to avoid clashing
+ # delete invoices to avoid clashing
for d in invoices:
d.cancel()
-def cancel_invoices():
- purchase_invoices = frappe.get_all("Purchase Invoice", {
- 'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']],
- 'docstatus': 1
- }, pluck="name")
- sales_invoices = frappe.get_all("Sales Invoice", {
- 'customer': 'Test TCS Customer',
- 'docstatus': 1
- }, pluck="name")
+def cancel_invoices():
+ purchase_invoices = frappe.get_all(
+ "Purchase Invoice",
+ {
+ "supplier": ["in", ["Test TDS Supplier", "Test TDS Supplier1", "Test TDS Supplier2"]],
+ "docstatus": 1,
+ },
+ pluck="name",
+ )
+
+ sales_invoices = frappe.get_all(
+ "Sales Invoice", {"customer": "Test TCS Customer", "docstatus": 1}, pluck="name"
+ )
for d in purchase_invoices:
- frappe.get_doc('Purchase Invoice', d).cancel()
+ frappe.get_doc("Purchase Invoice", d).cancel()
for d in sales_invoices:
- frappe.get_doc('Sales Invoice', d).cancel()
+ frappe.get_doc("Sales Invoice", d).cancel()
+
def create_purchase_invoice(**args):
# return sales invoice doc object
- item = frappe.db.get_value('Item', {'item_name': 'TDS Item'}, "name")
+ item = frappe.db.get_value("Item", {"item_name": "TDS Item"}, "name")
args = frappe._dict(args)
- pi = frappe.get_doc({
- "doctype": "Purchase Invoice",
- "posting_date": today(),
- "apply_tds": 0 if args.do_not_apply_tds else 1,
- "supplier": args.supplier,
- "company": '_Test Company',
- "taxes_and_charges": "",
- "currency": "INR",
- "credit_to": "Creditors - _TC",
- "taxes": [],
- "items": [{
- 'doctype': 'Purchase Invoice Item',
- 'item_code': item,
- 'qty': args.qty or 1,
- 'rate': args.rate or 10000,
- 'cost_center': 'Main - _TC',
- 'expense_account': 'Stock Received But Not Billed - _TC'
- }]
- })
+ pi = frappe.get_doc(
+ {
+ "doctype": "Purchase Invoice",
+ "posting_date": today(),
+ "apply_tds": 0 if args.do_not_apply_tds else 1,
+ "supplier": args.supplier,
+ "company": "_Test Company",
+ "taxes_and_charges": "",
+ "currency": "INR",
+ "credit_to": "Creditors - _TC",
+ "taxes": [],
+ "items": [
+ {
+ "doctype": "Purchase Invoice Item",
+ "item_code": item,
+ "qty": args.qty or 1,
+ "rate": args.rate or 10000,
+ "cost_center": "Main - _TC",
+ "expense_account": "Stock Received But Not Billed - _TC",
+ }
+ ],
+ }
+ )
pi.save()
return pi
+
def create_sales_invoice(**args):
# return sales invoice doc object
- item = frappe.db.get_value('Item', {'item_name': 'TCS Item'}, "name")
+ item = frappe.db.get_value("Item", {"item_name": "TCS Item"}, "name")
args = frappe._dict(args)
- si = frappe.get_doc({
- "doctype": "Sales Invoice",
- "posting_date": today(),
- "customer": args.customer,
- "company": '_Test Company',
- "taxes_and_charges": "",
- "currency": "INR",
- "debit_to": "Debtors - _TC",
- "taxes": [],
- "items": [{
- 'doctype': 'Sales Invoice Item',
- 'item_code': item,
- 'qty': args.qty or 1,
- 'rate': args.rate or 10000,
- 'cost_center': 'Main - _TC',
- 'expense_account': 'Cost of Goods Sold - _TC',
- 'warehouse': args.warehouse or '_Test Warehouse - _TC'
- }]
- })
+ si = frappe.get_doc(
+ {
+ "doctype": "Sales Invoice",
+ "posting_date": today(),
+ "customer": args.customer,
+ "company": "_Test Company",
+ "taxes_and_charges": "",
+ "currency": "INR",
+ "debit_to": "Debtors - _TC",
+ "taxes": [],
+ "items": [
+ {
+ "doctype": "Sales Invoice Item",
+ "item_code": item,
+ "qty": args.qty or 1,
+ "rate": args.rate or 10000,
+ "cost_center": "Main - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ }
+ ],
+ }
+ )
si.save()
return si
+
def create_records():
# create a new suppliers
- for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3',
- 'Test TDS Supplier4', 'Test TDS Supplier5']:
- if frappe.db.exists('Supplier', name):
+ for name in [
+ "Test TDS Supplier",
+ "Test TDS Supplier1",
+ "Test TDS Supplier2",
+ "Test TDS Supplier3",
+ "Test TDS Supplier4",
+ "Test TDS Supplier5",
+ ]:
+ if frappe.db.exists("Supplier", name):
continue
- frappe.get_doc({
- "supplier_group": "_Test Supplier Group",
- "supplier_name": name,
- "doctype": "Supplier",
- }).insert()
+ frappe.get_doc(
+ {
+ "supplier_group": "_Test Supplier Group",
+ "supplier_name": name,
+ "doctype": "Supplier",
+ }
+ ).insert()
- for name in ['Test TCS Customer']:
- if frappe.db.exists('Customer', name):
+ for name in ["Test TCS Customer"]:
+ if frappe.db.exists("Customer", name):
continue
- frappe.get_doc({
- "customer_group": "_Test Customer Group",
- "customer_name": name,
- "doctype": "Customer"
- }).insert()
+ frappe.get_doc(
+ {"customer_group": "_Test Customer Group", "customer_name": name, "doctype": "Customer"}
+ ).insert()
# create item
- if not frappe.db.exists('Item', "TDS Item"):
- frappe.get_doc({
- "doctype": "Item",
- "item_code": "TDS Item",
- "item_name": "TDS Item",
- "item_group": "All Item Groups",
- "is_stock_item": 0,
- }).insert()
+ if not frappe.db.exists("Item", "TDS Item"):
+ frappe.get_doc(
+ {
+ "doctype": "Item",
+ "item_code": "TDS Item",
+ "item_name": "TDS Item",
+ "item_group": "All Item Groups",
+ "is_stock_item": 0,
+ }
+ ).insert()
- if not frappe.db.exists('Item', "TCS Item"):
- frappe.get_doc({
- "doctype": "Item",
- "item_code": "TCS Item",
- "item_name": "TCS Item",
- "item_group": "All Item Groups",
- "is_stock_item": 1
- }).insert()
+ if not frappe.db.exists("Item", "TCS Item"):
+ frappe.get_doc(
+ {
+ "doctype": "Item",
+ "item_code": "TCS Item",
+ "item_name": "TCS Item",
+ "item_group": "All Item Groups",
+ "is_stock_item": 1,
+ }
+ ).insert()
# create tds account
if not frappe.db.exists("Account", "TDS - _TC"):
- frappe.get_doc({
- 'doctype': 'Account',
- 'company': '_Test Company',
- 'account_name': 'TDS',
- 'parent_account': 'Tax Assets - _TC',
- 'report_type': 'Balance Sheet',
- 'root_type': 'Asset'
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Account",
+ "company": "_Test Company",
+ "account_name": "TDS",
+ "parent_account": "Tax Assets - _TC",
+ "report_type": "Balance Sheet",
+ "root_type": "Asset",
+ }
+ ).insert()
# create tcs account
if not frappe.db.exists("Account", "TCS - _TC"):
- frappe.get_doc({
- 'doctype': 'Account',
- 'company': '_Test Company',
- 'account_name': 'TCS',
- 'parent_account': 'Duties and Taxes - _TC',
- 'report_type': 'Balance Sheet',
- 'root_type': 'Liability'
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Account",
+ "company": "_Test Company",
+ "account_name": "TCS",
+ "parent_account": "Duties and Taxes - _TC",
+ "report_type": "Balance Sheet",
+ "root_type": "Liability",
+ }
+ ).insert()
+
def create_tax_with_holding_category():
fiscal_year = get_fiscal_year(today(), company="_Test Company")
# Cumulative threshold
if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TDS"):
- frappe.get_doc({
- "doctype": "Tax Withholding Category",
- "name": "Cumulative Threshold TDS",
- "category_name": "10% TDS",
- "rates": [{
- 'from_date': fiscal_year[1],
- 'to_date': fiscal_year[2],
- 'tax_withholding_rate': 10,
- 'single_threshold': 0,
- 'cumulative_threshold': 30000.00
- }],
- "accounts": [{
- 'company': '_Test Company',
- 'account': 'TDS - _TC'
- }]
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Tax Withholding Category",
+ "name": "Cumulative Threshold TDS",
+ "category_name": "10% TDS",
+ "rates": [
+ {
+ "from_date": fiscal_year[1],
+ "to_date": fiscal_year[2],
+ "tax_withholding_rate": 10,
+ "single_threshold": 0,
+ "cumulative_threshold": 30000.00,
+ }
+ ],
+ "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
+ }
+ ).insert()
if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TCS"):
- frappe.get_doc({
- "doctype": "Tax Withholding Category",
- "name": "Cumulative Threshold TCS",
- "category_name": "10% TCS",
- "rates": [{
- 'from_date': fiscal_year[1],
- 'to_date': fiscal_year[2],
- 'tax_withholding_rate': 10,
- 'single_threshold': 0,
- 'cumulative_threshold': 30000.00
- }],
- "accounts": [{
- 'company': '_Test Company',
- 'account': 'TCS - _TC'
- }]
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Tax Withholding Category",
+ "name": "Cumulative Threshold TCS",
+ "category_name": "10% TCS",
+ "rates": [
+ {
+ "from_date": fiscal_year[1],
+ "to_date": fiscal_year[2],
+ "tax_withholding_rate": 10,
+ "single_threshold": 0,
+ "cumulative_threshold": 30000.00,
+ }
+ ],
+ "accounts": [{"company": "_Test Company", "account": "TCS - _TC"}],
+ }
+ ).insert()
# Single thresold
if not frappe.db.exists("Tax Withholding Category", "Single Threshold TDS"):
- frappe.get_doc({
- "doctype": "Tax Withholding Category",
- "name": "Single Threshold TDS",
- "category_name": "10% TDS",
- "rates": [{
- 'from_date': fiscal_year[1],
- 'to_date': fiscal_year[2],
- 'tax_withholding_rate': 10,
- 'single_threshold': 20000.00,
- 'cumulative_threshold': 0
- }],
- "accounts": [{
- 'company': '_Test Company',
- 'account': 'TDS - _TC'
- }]
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Tax Withholding Category",
+ "name": "Single Threshold TDS",
+ "category_name": "10% TDS",
+ "rates": [
+ {
+ "from_date": fiscal_year[1],
+ "to_date": fiscal_year[2],
+ "tax_withholding_rate": 10,
+ "single_threshold": 20000.00,
+ "cumulative_threshold": 0,
+ }
+ ],
+ "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
+ }
+ ).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": [{
- 'from_date': fiscal_year[1],
- 'to_date': fiscal_year[2],
- 'tax_withholding_rate': 10,
- 'single_threshold': 0,
- 'cumulative_threshold': 30000
- }],
- "accounts": [{
- 'company': '_Test Company',
- 'account': 'TDS - _TC'
- }]
- }).insert()
+ 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": [
+ {
+ "from_date": fiscal_year[1],
+ "to_date": fiscal_year[2],
+ "tax_withholding_rate": 10,
+ "single_threshold": 0,
+ "cumulative_threshold": 30000,
+ }
+ ],
+ "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
+ }
+ ).insert()
if not frappe.db.exists("Tax Withholding Category", "Test Service Category"):
- frappe.get_doc({
- "doctype": "Tax Withholding Category",
- "name": "Test Service Category",
- "category_name": "Test Service Category",
- "rates": [{
- 'from_date': fiscal_year[1],
- 'to_date': fiscal_year[2],
- 'tax_withholding_rate': 10,
- 'single_threshold': 2000,
- 'cumulative_threshold': 2000
- }],
- "accounts": [{
- 'company': '_Test Company',
- 'account': 'TDS - _TC'
- }]
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Tax Withholding Category",
+ "name": "Test Service Category",
+ "category_name": "Test Service Category",
+ "rates": [
+ {
+ "from_date": fiscal_year[1],
+ "to_date": fiscal_year[2],
+ "tax_withholding_rate": 10,
+ "single_threshold": 2000,
+ "cumulative_threshold": 2000,
+ }
+ ],
+ "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
+ }
+ ).insert()
if not frappe.db.exists("Tax Withholding Category", "Test Goods Category"):
- frappe.get_doc({
- "doctype": "Tax Withholding Category",
- "name": "Test Goods Category",
- "category_name": "Test Goods Category",
- "rates": [{
- 'from_date': fiscal_year[1],
- 'to_date': fiscal_year[2],
- 'tax_withholding_rate': 10,
- 'single_threshold': 2000,
- 'cumulative_threshold': 2000
- }],
- "accounts": [{
- 'company': '_Test Company',
- 'account': 'TDS - _TC'
- }]
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Tax Withholding Category",
+ "name": "Test Goods Category",
+ "category_name": "Test Goods Category",
+ "rates": [
+ {
+ "from_date": fiscal_year[1],
+ "to_date": fiscal_year[2],
+ "tax_withholding_rate": 10,
+ "single_threshold": 2000,
+ "cumulative_threshold": 2000,
+ }
+ ],
+ "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
+ }
+ ).insert()
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index fd5173fd659..47e2e0761b6 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -14,9 +14,18 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
-class ClosedAccountingPeriod(frappe.ValidationError): pass
+class ClosedAccountingPeriod(frappe.ValidationError):
+ pass
-def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False):
+
+def make_gl_entries(
+ gl_map,
+ cancel=False,
+ adv_adj=False,
+ merge_entries=True,
+ update_outstanding="Yes",
+ from_repost=False,
+):
if gl_map:
if not cancel:
validate_accounting_period(gl_map)
@@ -25,12 +34,18 @@ def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, upd
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
# Post GL Map proccess there may no be any GL Entries
elif gl_map:
- frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction."))
+ frappe.throw(
+ _(
+ "Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction."
+ )
+ )
else:
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
+
def validate_accounting_period(gl_map):
- accounting_periods = frappe.db.sql(""" SELECT
+ accounting_periods = frappe.db.sql(
+ """ SELECT
ap.name as name
FROM
`tabAccounting Period` ap, `tabClosed Document` cd
@@ -40,15 +55,23 @@ def validate_accounting_period(gl_map):
AND cd.closed = 1
AND cd.document_type = %(voucher_type)s
AND %(date)s between ap.start_date and ap.end_date
- """, {
- 'date': gl_map[0].posting_date,
- 'company': gl_map[0].company,
- 'voucher_type': gl_map[0].voucher_type
- }, as_dict=1)
+ """,
+ {
+ "date": gl_map[0].posting_date,
+ "company": gl_map[0].company,
+ "voucher_type": gl_map[0].voucher_type,
+ },
+ as_dict=1,
+ )
if accounting_periods:
- frappe.throw(_("You cannot create or cancel any accounting entries with in the closed Accounting Period {0}")
- .format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod)
+ frappe.throw(
+ _(
+ "You cannot create or cancel any accounting entries with in the closed Accounting Period {0}"
+ ).format(frappe.bold(accounting_periods[0].name)),
+ ClosedAccountingPeriod,
+ )
+
def process_gl_map(gl_map, merge_entries=True, precision=None):
if merge_entries:
@@ -60,8 +83,9 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
entry.debit = 0.0
if flt(entry.debit_in_account_currency) < 0:
- entry.credit_in_account_currency = \
- flt(entry.credit_in_account_currency) - flt(entry.debit_in_account_currency)
+ entry.credit_in_account_currency = flt(entry.credit_in_account_currency) - flt(
+ entry.debit_in_account_currency
+ )
entry.debit_in_account_currency = 0.0
if flt(entry.credit) < 0:
@@ -69,32 +93,37 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
entry.credit = 0.0
if flt(entry.credit_in_account_currency) < 0:
- entry.debit_in_account_currency = \
- flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency)
+ entry.debit_in_account_currency = flt(entry.debit_in_account_currency) - flt(
+ entry.credit_in_account_currency
+ )
entry.credit_in_account_currency = 0.0
update_net_values(entry)
return gl_map
+
def update_net_values(entry):
# In some scenarios net value needs to be shown in the ledger
# This method updates net values as debit or credit
if entry.post_net_value and entry.debit and entry.credit:
if entry.debit > entry.credit:
entry.debit = entry.debit - entry.credit
- entry.debit_in_account_currency = entry.debit_in_account_currency \
- - entry.credit_in_account_currency
+ entry.debit_in_account_currency = (
+ entry.debit_in_account_currency - entry.credit_in_account_currency
+ )
entry.credit = 0
entry.credit_in_account_currency = 0
else:
entry.credit = entry.credit - entry.debit
- entry.credit_in_account_currency = entry.credit_in_account_currency \
- - entry.debit_in_account_currency
+ entry.credit_in_account_currency = (
+ entry.credit_in_account_currency - entry.debit_in_account_currency
+ )
entry.debit = 0
entry.debit_in_account_currency = 0
+
def merge_similar_entries(gl_map, precision=None):
merged_gl_map = []
accounting_dimensions = get_accounting_dimensions()
@@ -103,12 +132,14 @@ def merge_similar_entries(gl_map, precision=None):
# to that entry
same_head = check_if_in_list(entry, merged_gl_map, accounting_dimensions)
if same_head:
- same_head.debit = flt(same_head.debit) + flt(entry.debit)
- same_head.debit_in_account_currency = \
- flt(same_head.debit_in_account_currency) + flt(entry.debit_in_account_currency)
+ same_head.debit = flt(same_head.debit) + flt(entry.debit)
+ same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
+ entry.debit_in_account_currency
+ )
same_head.credit = flt(same_head.credit) + flt(entry.credit)
- same_head.credit_in_account_currency = \
- flt(same_head.credit_in_account_currency) + flt(entry.credit_in_account_currency)
+ same_head.credit_in_account_currency = flt(same_head.credit_in_account_currency) + flt(
+ entry.credit_in_account_currency
+ )
else:
merged_gl_map.append(entry)
@@ -119,14 +150,25 @@ def merge_similar_entries(gl_map, precision=None):
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
# filter zero debit and credit entries
- merged_gl_map = filter(lambda x: flt(x.debit, precision)!=0 or flt(x.credit, precision)!=0, merged_gl_map)
+ merged_gl_map = filter(
+ lambda x: flt(x.debit, precision) != 0 or flt(x.credit, precision) != 0, merged_gl_map
+ )
merged_gl_map = list(merged_gl_map)
return merged_gl_map
+
def check_if_in_list(gle, gl_map, dimensions=None):
- account_head_fieldnames = ['voucher_detail_no', 'party', 'against_voucher',
- 'cost_center', 'against_voucher_type', 'party_type', 'project', 'finance_book']
+ account_head_fieldnames = [
+ "voucher_detail_no",
+ "party",
+ "against_voucher",
+ "cost_center",
+ "against_voucher_type",
+ "party_type",
+ "project",
+ "finance_book",
+ ]
if dimensions:
account_head_fieldnames = account_head_fieldnames + dimensions
@@ -145,6 +187,7 @@ def check_if_in_list(gle, gl_map, dimensions=None):
if same_head:
return e
+
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
if not from_repost:
validate_cwip_accounts(gl_map)
@@ -157,36 +200,52 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
for entry in gl_map:
make_entry(entry, adv_adj, update_outstanding, from_repost)
+
def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle = frappe.new_doc("GL Entry")
gle.update(args)
gle.flags.ignore_permissions = 1
gle.flags.from_repost = from_repost
gle.flags.adv_adj = adv_adj
- gle.flags.update_outstanding = update_outstanding or 'Yes'
+ gle.flags.update_outstanding = update_outstanding or "Yes"
gle.submit()
if not from_repost:
validate_expense_against_budget(args)
+
def validate_cwip_accounts(gl_map):
"""Validate that CWIP account are not used in Journal Entry"""
if gl_map and gl_map[0].voucher_type != "Journal Entry":
return
- cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category", "enable_cwip_accounting"))
+ cwip_enabled = any(
+ cint(ac.enable_cwip_accounting)
+ for ac in frappe.db.get_all("Asset Category", "enable_cwip_accounting")
+ )
if cwip_enabled:
- cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount
- where account_type = 'Capital Work in Progress' and is_group=0""")]
+ cwip_accounts = [
+ d[0]
+ for d in frappe.db.sql(
+ """select name from tabAccount
+ where account_type = 'Capital Work in Progress' and is_group=0"""
+ )
+ ]
for entry in gl_map:
if entry.account in cwip_accounts:
frappe.throw(
- _("Account: {0} is capital Work in progress and can not be updated by Journal Entry").format(entry.account))
+ _(
+ "Account: {0} is capital Work in progress and can not be updated by Journal Entry"
+ ).format(entry.account)
+ )
+
def round_off_debit_credit(gl_map):
- precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),
- currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency"))
+ precision = get_field_precision(
+ frappe.get_meta("GL Entry").get_field("debit"),
+ currency=frappe.get_cached_value("Company", gl_map[0].company, "default_currency"),
+ )
debit_credit_diff = 0.0
for entry in gl_map:
@@ -199,17 +258,23 @@ def round_off_debit_credit(gl_map):
if gl_map[0]["voucher_type"] in ("Journal Entry", "Payment Entry"):
allowance = 5.0 / (10**precision)
else:
- allowance = .5
+ allowance = 0.5
if abs(debit_credit_diff) > allowance:
- frappe.throw(_("Debit and Credit not equal for {0} #{1}. Difference is {2}.")
- .format(gl_map[0].voucher_type, gl_map[0].voucher_no, debit_credit_diff))
+ frappe.throw(
+ _("Debit and Credit not equal for {0} #{1}. Difference is {2}.").format(
+ gl_map[0].voucher_type, gl_map[0].voucher_no, debit_credit_diff
+ )
+ )
elif abs(debit_credit_diff) >= (1.0 / (10**precision)):
make_round_off_gle(gl_map, debit_credit_diff, precision)
+
def make_round_off_gle(gl_map, debit_credit_diff, precision):
- round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(gl_map[0].company)
+ round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
+ gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
+ )
round_off_account_exists = False
round_off_gle = frappe._dict()
for d in gl_map:
@@ -226,30 +291,62 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
return
if not round_off_gle:
- for k in ["voucher_type", "voucher_no", "company",
- "posting_date", "remarks"]:
- round_off_gle[k] = gl_map[0][k]
+ for k in ["voucher_type", "voucher_no", "company", "posting_date", "remarks"]:
+ round_off_gle[k] = gl_map[0][k]
- round_off_gle.update({
- "account": round_off_account,
- "debit_in_account_currency": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
- "credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0,
- "debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
- "credit": debit_credit_diff if debit_credit_diff > 0 else 0,
- "cost_center": round_off_cost_center,
- "party_type": None,
- "party": None,
- "is_opening": "No",
- "against_voucher_type": None,
- "against_voucher": None
- })
+ round_off_gle.update(
+ {
+ "account": round_off_account,
+ "debit_in_account_currency": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
+ "credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0,
+ "debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
+ "credit": debit_credit_diff if debit_credit_diff > 0 else 0,
+ "cost_center": round_off_cost_center,
+ "party_type": None,
+ "party": None,
+ "is_opening": "No",
+ "against_voucher_type": None,
+ "against_voucher": None,
+ }
+ )
+
+ update_accounting_dimensions(round_off_gle)
if not round_off_account_exists:
gl_map.append(round_off_gle)
-def get_round_off_account_and_cost_center(company):
- round_off_account, round_off_cost_center = frappe.get_cached_value('Company', company,
- ["round_off_account", "round_off_cost_center"]) or [None, None]
+
+def update_accounting_dimensions(round_off_gle):
+ dimensions = get_accounting_dimensions()
+ meta = frappe.get_meta(round_off_gle["voucher_type"])
+ has_all_dimensions = True
+
+ for dimension in dimensions:
+ if not meta.has_field(dimension):
+ has_all_dimensions = False
+
+ if dimensions and has_all_dimensions:
+ dimension_values = frappe.db.get_value(
+ round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions, as_dict=1
+ )
+
+ for dimension in dimensions:
+ round_off_gle[dimension] = dimension_values.get(dimension)
+
+
+def get_round_off_account_and_cost_center(company, voucher_type, voucher_no):
+ round_off_account, round_off_cost_center = frappe.get_cached_value(
+ "Company", company, ["round_off_account", "round_off_cost_center"]
+ ) or [None, None]
+
+ meta = frappe.get_meta(voucher_type)
+
+ # Give first preference to parent cost center for round off GLE
+ if meta.has_field("cost_center"):
+ parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center")
+ if parent_cost_center:
+ round_off_cost_center = parent_cost_center
+
if not round_off_account:
frappe.throw(_("Please mention Round Off Account in Company"))
@@ -258,68 +355,78 @@ def get_round_off_account_and_cost_center(company):
return round_off_account, round_off_cost_center
-def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None,
- adv_adj=False, update_outstanding="Yes"):
+
+def make_reverse_gl_entries(
+ gl_entries=None, voucher_type=None, voucher_no=None, adv_adj=False, update_outstanding="Yes"
+):
"""
- Get original gl entries of the voucher
- and make reverse gl entries by swapping debit and credit
+ Get original gl entries of the voucher
+ and make reverse gl entries by swapping debit and credit
"""
if not gl_entries:
- gl_entries = frappe.get_all("GL Entry",
- fields = ["*"],
- filters = {
- "voucher_type": voucher_type,
- "voucher_no": voucher_no,
- "is_cancelled": 0
- })
+ gl_entries = frappe.get_all(
+ "GL Entry",
+ fields=["*"],
+ filters={"voucher_type": voucher_type, "voucher_no": voucher_no, "is_cancelled": 0},
+ )
if gl_entries:
validate_accounting_period(gl_entries)
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
- set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no'])
+ set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
for entry in gl_entries:
- entry['name'] = None
- debit = entry.get('debit', 0)
- credit = entry.get('credit', 0)
+ entry["name"] = None
+ debit = entry.get("debit", 0)
+ credit = entry.get("credit", 0)
- debit_in_account_currency = entry.get('debit_in_account_currency', 0)
- credit_in_account_currency = entry.get('credit_in_account_currency', 0)
+ debit_in_account_currency = entry.get("debit_in_account_currency", 0)
+ credit_in_account_currency = entry.get("credit_in_account_currency", 0)
- entry['debit'] = credit
- entry['credit'] = debit
- entry['debit_in_account_currency'] = credit_in_account_currency
- entry['credit_in_account_currency'] = debit_in_account_currency
+ entry["debit"] = credit
+ entry["credit"] = debit
+ entry["debit_in_account_currency"] = credit_in_account_currency
+ entry["credit_in_account_currency"] = debit_in_account_currency
- entry['remarks'] = "On cancellation of " + entry['voucher_no']
- entry['is_cancelled'] = 1
+ entry["remarks"] = "On cancellation of " + entry["voucher_no"]
+ entry["is_cancelled"] = 1
- if entry['debit'] or entry['credit']:
+ if entry["debit"] or entry["credit"]:
make_entry(entry, adv_adj, "Yes")
def check_freezing_date(posting_date, adv_adj=False):
"""
- Nobody can do GL Entries where posting date is before freezing date
- except authorized person
+ Nobody can do GL Entries where posting date is before freezing date
+ except authorized person
- Administrator has all the roles so this check will be bypassed if any role is allowed to post
- Hence stop admin to bypass if accounts are freezed
+ Administrator has all the roles so this check will be bypassed if any role is allowed to post
+ Hence stop admin to bypass if accounts are freezed
"""
if not adv_adj:
- acc_frozen_upto = frappe.db.get_value('Accounts Settings', None, 'acc_frozen_upto')
+ acc_frozen_upto = frappe.db.get_value("Accounts Settings", None, "acc_frozen_upto")
if acc_frozen_upto:
- frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,'frozen_accounts_modifier')
- if getdate(posting_date) <= getdate(acc_frozen_upto) \
- and (frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == 'Administrator'):
- frappe.throw(_("You are not authorized to add or update entries before {0}").format(formatdate(acc_frozen_upto)))
+ frozen_accounts_modifier = frappe.db.get_value(
+ "Accounts Settings", None, "frozen_accounts_modifier"
+ )
+ if getdate(posting_date) <= getdate(acc_frozen_upto) and (
+ frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator"
+ ):
+ frappe.throw(
+ _("You are not authorized to add or update entries before {0}").format(
+ formatdate(acc_frozen_upto)
+ )
+ )
+
def set_as_cancel(voucher_type, voucher_no):
"""
- Set is_cancelled=1 in all original gl entries for the voucher
+ Set is_cancelled=1 in all original gl entries for the voucher
"""
- frappe.db.sql("""UPDATE `tabGL Entry` SET is_cancelled = 1,
+ frappe.db.sql(
+ """UPDATE `tabGL Entry` SET is_cancelled = 1,
modified=%s, modified_by=%s
where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
- (now(), frappe.session.user, voucher_type, voucher_no))
+ (now(), frappe.session.user, voucher_type, voucher_no),
+ )
diff --git a/erpnext/accounts/notification/notification_for_new_fiscal_year/notification_for_new_fiscal_year.py b/erpnext/accounts/notification/notification_for_new_fiscal_year/notification_for_new_fiscal_year.py
index 19b550feea7..02e3e933330 100644
--- a/erpnext/accounts/notification/notification_for_new_fiscal_year/notification_for_new_fiscal_year.py
+++ b/erpnext/accounts/notification/notification_for_new_fiscal_year/notification_for_new_fiscal_year.py
@@ -1,5 +1,3 @@
-
-
def get_context(context):
# do your magic here
pass
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 907964720ff..b76ce29b505 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -34,125 +34,230 @@ from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
-class DuplicatePartyAccountError(frappe.ValidationError): pass
+class DuplicatePartyAccountError(frappe.ValidationError):
+ pass
+
@frappe.whitelist()
-def get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None,
- bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False, fetch_payment_terms_template=True,
- party_address=None, company_address=None, shipping_address=None, pos_profile=None):
+def get_party_details(
+ party=None,
+ account=None,
+ party_type="Customer",
+ company=None,
+ posting_date=None,
+ bill_date=None,
+ price_list=None,
+ currency=None,
+ doctype=None,
+ ignore_permissions=False,
+ fetch_payment_terms_template=True,
+ party_address=None,
+ company_address=None,
+ shipping_address=None,
+ pos_profile=None,
+):
if not party:
return {}
if not frappe.db.exists(party_type, party):
frappe.throw(_("{0}: {1} does not exists").format(party_type, party))
- return _get_party_details(party, account, party_type,
- company, posting_date, bill_date, price_list, currency, doctype, ignore_permissions,
- fetch_payment_terms_template, party_address, company_address, shipping_address, pos_profile)
+ return _get_party_details(
+ party,
+ account,
+ party_type,
+ company,
+ posting_date,
+ bill_date,
+ price_list,
+ currency,
+ doctype,
+ ignore_permissions,
+ fetch_payment_terms_template,
+ party_address,
+ company_address,
+ shipping_address,
+ pos_profile,
+ )
-def _get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None,
- bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False,
- fetch_payment_terms_template=True, party_address=None, company_address=None, shipping_address=None, pos_profile=None):
- party_details = frappe._dict(set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype))
+
+def _get_party_details(
+ party=None,
+ account=None,
+ party_type="Customer",
+ company=None,
+ posting_date=None,
+ bill_date=None,
+ price_list=None,
+ currency=None,
+ doctype=None,
+ ignore_permissions=False,
+ fetch_payment_terms_template=True,
+ party_address=None,
+ company_address=None,
+ shipping_address=None,
+ pos_profile=None,
+):
+ party_details = frappe._dict(
+ set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)
+ )
party = party_details[party_type.lower()]
- if not ignore_permissions and not (frappe.has_permission(party_type, "read", party) or frappe.has_permission(party_type, "select", party)):
+ if not ignore_permissions and not (
+ frappe.has_permission(party_type, "read", party)
+ or frappe.has_permission(party_type, "select", party)
+ ):
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
party = frappe.get_doc(party_type, party)
currency = party.get("default_currency") or currency or get_company_currency(company)
- party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address)
+ party_address, shipping_address = set_address_details(
+ party_details,
+ party,
+ party_type,
+ doctype,
+ company,
+ party_address,
+ company_address,
+ shipping_address,
+ )
set_contact_details(party_details, party, party_type)
set_other_values(party_details, party, party_type)
set_price_list(party_details, party, party_type, price_list, pos_profile)
- party_details["tax_category"] = get_address_tax_category(party.get("tax_category"),
- party_address, shipping_address if party_type != "Supplier" else party_address)
+ party_details["tax_category"] = get_address_tax_category(
+ party.get("tax_category"),
+ party_address,
+ shipping_address if party_type != "Supplier" else party_address,
+ )
- tax_template = set_taxes(party.name, party_type, posting_date, company,
- customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category,
- billing_address=party_address, shipping_address=shipping_address)
+ tax_template = set_taxes(
+ party.name,
+ party_type,
+ posting_date,
+ company,
+ customer_group=party_details.customer_group,
+ supplier_group=party_details.supplier_group,
+ tax_category=party_details.tax_category,
+ billing_address=party_address,
+ shipping_address=shipping_address,
+ )
if tax_template:
- party_details['taxes_and_charges'] = tax_template
+ party_details["taxes_and_charges"] = tax_template
if cint(fetch_payment_terms_template):
- party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company)
+ party_details["payment_terms_template"] = get_payment_terms_template(
+ party.name, party_type, company
+ )
if not party_details.get("currency"):
party_details["currency"] = currency
# sales team
- if party_type=="Customer":
- party_details["sales_team"] = [{
- "sales_person": d.sales_person,
- "allocated_percentage": d.allocated_percentage or None,
- "commission_rate": d.commission_rate
- } for d in party.get("sales_team")]
+ if party_type == "Customer":
+ party_details["sales_team"] = [
+ {
+ "sales_person": d.sales_person,
+ "allocated_percentage": d.allocated_percentage or None,
+ "commission_rate": d.commission_rate,
+ }
+ for d in party.get("sales_team")
+ ]
# supplier tax withholding category
if party_type == "Supplier" and party:
- party_details["supplier_tds"] = frappe.get_value(party_type, party.name, "tax_withholding_category")
+ party_details["supplier_tds"] = frappe.get_value(
+ party_type, party.name, "tax_withholding_category"
+ )
return party_details
-def set_address_details(party_details, party, party_type, doctype=None, company=None, party_address=None, company_address=None, shipping_address=None):
- billing_address_field = "customer_address" if party_type == "Lead" \
- else party_type.lower() + "_address"
- party_details[billing_address_field] = party_address or get_default_address(party_type, party.name)
+
+def set_address_details(
+ party_details,
+ party,
+ party_type,
+ doctype=None,
+ company=None,
+ party_address=None,
+ company_address=None,
+ shipping_address=None,
+):
+ billing_address_field = (
+ "customer_address" if party_type == "Lead" else party_type.lower() + "_address"
+ )
+ party_details[billing_address_field] = party_address or get_default_address(
+ party_type, party.name
+ )
if doctype:
- party_details.update(get_fetch_values(doctype, billing_address_field, party_details[billing_address_field]))
+ party_details.update(
+ get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])
+ )
# address display
party_details.address_display = get_address_display(party_details[billing_address_field])
# shipping address
if party_type in ["Customer", "Lead"]:
- party_details.shipping_address_name = shipping_address or get_party_shipping_address(party_type, party.name)
+ party_details.shipping_address_name = shipping_address or get_party_shipping_address(
+ party_type, party.name
+ )
party_details.shipping_address = get_address_display(party_details["shipping_address_name"])
if doctype:
- party_details.update(get_fetch_values(doctype, 'shipping_address_name', party_details.shipping_address_name))
+ party_details.update(
+ get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name)
+ )
if company_address:
- party_details.update({'company_address': company_address})
+ party_details.update({"company_address": company_address})
else:
party_details.update(get_company_address(company))
- if doctype and doctype in ['Delivery Note', 'Sales Invoice', 'Sales Order']:
+ if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order"]:
if party_details.company_address:
- party_details.update(get_fetch_values(doctype, 'company_address', party_details.company_address))
+ party_details.update(
+ get_fetch_values(doctype, "company_address", party_details.company_address)
+ )
get_regional_address_details(party_details, doctype, company)
elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]:
if party_details.company_address:
party_details["shipping_address"] = shipping_address or party_details["company_address"]
party_details.shipping_address_display = get_address_display(party_details["shipping_address"])
- party_details.update(get_fetch_values(doctype, 'shipping_address', party_details.shipping_address))
+ party_details.update(
+ get_fetch_values(doctype, "shipping_address", party_details.shipping_address)
+ )
get_regional_address_details(party_details, doctype, company)
return party_details.get(billing_address_field), party_details.shipping_address_name
+
@erpnext.allow_regional
def get_regional_address_details(party_details, doctype, company):
pass
+
def set_contact_details(party_details, party, party_type):
party_details.contact_person = get_default_contact(party_type, party.name)
if not party_details.contact_person:
- party_details.update({
- "contact_person": None,
- "contact_display": None,
- "contact_email": None,
- "contact_mobile": None,
- "contact_phone": None,
- "contact_designation": None,
- "contact_department": None
- })
+ party_details.update(
+ {
+ "contact_person": None,
+ "contact_display": None,
+ "contact_email": None,
+ "contact_mobile": None,
+ "contact_phone": None,
+ "contact_designation": None,
+ "contact_department": None,
+ }
+ )
else:
party_details.update(get_contact_details(party_details.contact_person))
+
def set_other_values(party_details, party, party_type):
# copy
- if party_type=="Customer":
+ if party_type == "Customer":
to_copy = ["customer_name", "customer_group", "territory", "language"]
else:
to_copy = ["supplier_name", "supplier_group", "language"]
@@ -160,112 +265,121 @@ def set_other_values(party_details, party, party_type):
party_details[f] = party.get(f)
# fields prepended with default in Customer doctype
- for f in ['currency'] \
- + (['sales_partner', 'commission_rate'] if party_type=="Customer" else []):
+ for f in ["currency"] + (
+ ["sales_partner", "commission_rate"] if party_type == "Customer" else []
+ ):
if party.get("default_" + f):
party_details[f] = party.get("default_" + f)
+
def get_default_price_list(party):
"""Return default price list for party (Document object)"""
if party.get("default_price_list"):
return party.default_price_list
if party.doctype == "Customer":
- price_list = frappe.get_cached_value("Customer Group",
- party.customer_group, "default_price_list")
- if price_list:
- return price_list
+ return frappe.db.get_value("Customer Group", party.customer_group, "default_price_list")
- return None
def set_price_list(party_details, party, party_type, given_price_list, pos=None):
# price list
- price_list = get_permitted_documents('Price List')
+ price_list = get_permitted_documents("Price List")
# if there is only one permitted document based on user permissions, set it
if price_list and len(price_list) == 1:
price_list = price_list[0]
- elif pos and party_type == 'Customer':
- customer_price_list = frappe.get_value('Customer', party.name, 'default_price_list')
+ elif pos and party_type == "Customer":
+ customer_price_list = frappe.get_value("Customer", party.name, "default_price_list")
if customer_price_list:
price_list = customer_price_list
else:
- pos_price_list = frappe.get_value('POS Profile', pos, 'selling_price_list')
+ pos_price_list = frappe.get_value("POS Profile", pos, "selling_price_list")
price_list = pos_price_list or given_price_list
else:
price_list = get_default_price_list(party) or given_price_list
if price_list:
- party_details.price_list_currency = frappe.db.get_value("Price List", price_list, "currency", cache=True)
+ party_details.price_list_currency = frappe.db.get_value(
+ "Price List", price_list, "currency", cache=True
+ )
- party_details["selling_price_list" if party.doctype=="Customer" else "buying_price_list"] = price_list
+ party_details[
+ "selling_price_list" if party.doctype == "Customer" else "buying_price_list"
+ ] = price_list
-def set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype):
+def set_account_and_due_date(
+ party, account, party_type, company, posting_date, bill_date, doctype
+):
if doctype not in ["POS Invoice", "Sales Invoice", "Purchase Invoice"]:
# not an invoice
- return {
- party_type.lower(): party
- }
+ return {party_type.lower(): party}
if party:
account = get_party_account(party_type, party, company)
- account_fieldname = "debit_to" if party_type=="Customer" else "credit_to"
+ account_fieldname = "debit_to" if party_type == "Customer" else "credit_to"
out = {
party_type.lower(): party,
- account_fieldname : account,
- "due_date": get_due_date(posting_date, party_type, party, company, bill_date)
+ account_fieldname: account,
+ "due_date": get_due_date(posting_date, party_type, party, company, bill_date),
}
return out
+
@frappe.whitelist()
def get_party_account(party_type, party=None, company=None):
"""Returns the account for the given `party`.
- Will first search in party (Customer / Supplier) record, if not found,
- will search in group (Customer Group / Supplier Group),
- finally will return default."""
+ Will first search in party (Customer / Supplier) record, if not found,
+ will search in group (Customer Group / Supplier Group),
+ finally will return default."""
if not company:
frappe.throw(_("Please select a Company"))
- if not party and party_type in ['Customer', 'Supplier']:
- default_account_name = "default_receivable_account" \
- if party_type=="Customer" else "default_payable_account"
+ if not party and party_type in ["Customer", "Supplier"]:
+ default_account_name = (
+ "default_receivable_account" if party_type == "Customer" else "default_payable_account"
+ )
- return frappe.get_cached_value('Company', company, default_account_name)
+ return frappe.get_cached_value("Company", company, default_account_name)
- account = frappe.db.get_value("Party Account",
- {"parenttype": party_type, "parent": party, "company": company}, "account")
+ account = frappe.db.get_value(
+ "Party Account", {"parenttype": party_type, "parent": party, "company": company}, "account"
+ )
- if not account and party_type in ['Customer', 'Supplier']:
- party_group_doctype = "Customer Group" if party_type=="Customer" else "Supplier Group"
+ if not account and party_type in ["Customer", "Supplier"]:
+ party_group_doctype = "Customer Group" if party_type == "Customer" else "Supplier Group"
group = frappe.get_cached_value(party_type, party, scrub(party_group_doctype))
- account = frappe.db.get_value("Party Account",
- {"parenttype": party_group_doctype, "parent": group, "company": company}, "account")
+ account = frappe.db.get_value(
+ "Party Account",
+ {"parenttype": party_group_doctype, "parent": group, "company": company},
+ "account",
+ )
- if not account and party_type in ['Customer', 'Supplier']:
- default_account_name = "default_receivable_account" \
- if party_type=="Customer" else "default_payable_account"
- account = frappe.get_cached_value('Company', company, default_account_name)
+ if not account and party_type in ["Customer", "Supplier"]:
+ default_account_name = (
+ "default_receivable_account" if party_type == "Customer" else "default_payable_account"
+ )
+ account = frappe.get_cached_value("Company", company, default_account_name)
existing_gle_currency = get_party_gle_currency(party_type, party, company)
if existing_gle_currency:
if account:
account_currency = frappe.db.get_value("Account", account, "account_currency", cache=True)
if (account and account_currency != existing_gle_currency) or not account:
- account = get_party_gle_account(party_type, party, company)
+ account = get_party_gle_account(party_type, party, company)
return account
+
@frappe.whitelist()
def get_party_bank_account(party_type, party):
- return frappe.db.get_value('Bank Account', {
- 'party_type': party_type,
- 'party': party,
- 'is_default': 1
- })
+ return frappe.db.get_value(
+ "Bank Account", {"party_type": party_type, "party": party, "is_default": 1}
+ )
+
def get_party_account_currency(party_type, party, company):
def generator():
@@ -274,27 +388,38 @@ def get_party_account_currency(party_type, party, company):
return frappe.local_cache("party_account_currency", (party_type, party, company), generator)
+
def get_party_gle_currency(party_type, party, company):
def generator():
- existing_gle_currency = frappe.db.sql("""select account_currency from `tabGL Entry`
+ existing_gle_currency = frappe.db.sql(
+ """select account_currency from `tabGL Entry`
where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s
- limit 1""", { "company": company, "party_type": party_type, "party": party })
+ limit 1""",
+ {"company": company, "party_type": party_type, "party": party},
+ )
return existing_gle_currency[0][0] if existing_gle_currency else None
- return frappe.local_cache("party_gle_currency", (party_type, party, company), generator,
- regenerate_if_none=True)
+ return frappe.local_cache(
+ "party_gle_currency", (party_type, party, company), generator, regenerate_if_none=True
+ )
+
def get_party_gle_account(party_type, party, company):
def generator():
- existing_gle_account = frappe.db.sql("""select account from `tabGL Entry`
+ existing_gle_account = frappe.db.sql(
+ """select account from `tabGL Entry`
where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s
- limit 1""", { "company": company, "party_type": party_type, "party": party })
+ limit 1""",
+ {"company": company, "party_type": party_type, "party": party},
+ )
return existing_gle_account[0][0] if existing_gle_account else None
- return frappe.local_cache("party_gle_account", (party_type, party, company), generator,
- regenerate_if_none=True)
+ return frappe.local_cache(
+ "party_gle_account", (party_type, party, company), generator, regenerate_if_none=True
+ )
+
def validate_party_gle_currency(party_type, party, company, party_account_currency=None):
"""Validate party account currency with existing GL Entry's currency"""
@@ -304,32 +429,55 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren
existing_gle_currency = get_party_gle_currency(party_type, party, company)
if existing_gle_currency and party_account_currency != existing_gle_currency:
- frappe.throw(_("{0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.")
- .format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency)
+ frappe.throw(
+ _(
+ "{0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}."
+ ).format(
+ frappe.bold(party_type),
+ frappe.bold(party),
+ frappe.bold(existing_gle_currency),
+ frappe.bold(company),
+ ),
+ InvalidAccountCurrency,
+ )
+
def validate_party_accounts(doc):
from erpnext.controllers.accounts_controller import validate_account_head
+
companies = []
for account in doc.get("accounts"):
if account.company in companies:
- frappe.throw(_("There can only be 1 Account per Company in {0} {1}")
- .format(doc.doctype, doc.name), DuplicatePartyAccountError)
+ frappe.throw(
+ _("There can only be 1 Account per Company in {0} {1}").format(doc.doctype, doc.name),
+ DuplicatePartyAccountError,
+ )
else:
companies.append(account.company)
- party_account_currency = frappe.db.get_value("Account", account.account, "account_currency", cache=True)
+ party_account_currency = frappe.db.get_value(
+ "Account", account.account, "account_currency", cache=True
+ )
if frappe.db.get_default("Company"):
- company_default_currency = frappe.get_cached_value('Company',
- frappe.db.get_default("Company"), "default_currency")
+ company_default_currency = frappe.get_cached_value(
+ "Company", frappe.db.get_default("Company"), "default_currency"
+ )
else:
- company_default_currency = frappe.db.get_value('Company', account.company, "default_currency")
+ company_default_currency = frappe.db.get_value("Company", account.company, "default_currency")
validate_party_gle_currency(doc.doctype, doc.name, account.company, party_account_currency)
if doc.get("default_currency") and party_account_currency and company_default_currency:
- if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency:
- frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency"))
+ if (
+ doc.default_currency != party_account_currency
+ and doc.default_currency != company_default_currency
+ ):
+ frappe.throw(
+ _(
+ "Billing currency must be equal to either default company's currency or party account currency"
+ )
+ )
# validate if account is mapped for same company
validate_account_head(account.idx, account.account, account.company)
@@ -344,18 +492,23 @@ def get_due_date(posting_date, party_type, party, company=None, bill_date=None):
template_name = get_payment_terms_template(party, party_type, company)
if template_name:
- due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d")
+ due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime(
+ "%Y-%m-%d"
+ )
else:
if party_type == "Supplier":
supplier_group = frappe.get_cached_value(party_type, party, "supplier_group")
template_name = frappe.get_cached_value("Supplier Group", supplier_group, "payment_terms")
if template_name:
- due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d")
+ due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime(
+ "%Y-%m-%d"
+ )
# If due date is calculated from bill_date, check this condition
if getdate(due_date) < getdate(posting_date):
due_date = posting_date
return due_date
+
def get_due_date_from_template(template_name, posting_date, bill_date):
"""
Inspects all `Payment Term`s from the a `Payment Terms Template` and returns the due
@@ -365,40 +518,55 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
"""
due_date = getdate(bill_date or posting_date)
- template = frappe.get_doc('Payment Terms Template', template_name)
+ template = frappe.get_doc("Payment Terms Template", template_name)
for term in template.terms:
- if term.due_date_based_on == 'Day(s) after invoice date':
+ if term.due_date_based_on == "Day(s) after invoice date":
due_date = max(due_date, add_days(due_date, term.credit_days))
- elif term.due_date_based_on == 'Day(s) after the end of the invoice month':
+ elif term.due_date_based_on == "Day(s) after the end of the invoice month":
due_date = max(due_date, add_days(get_last_day(due_date), term.credit_days))
else:
due_date = max(due_date, add_months(get_last_day(due_date), term.credit_months))
return due_date
-def validate_due_date(posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None):
+
+def validate_due_date(
+ posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None
+):
if getdate(due_date) < getdate(posting_date):
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
else:
- if not template_name: return
+ if not template_name:
+ return
- default_due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d")
+ default_due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime(
+ "%Y-%m-%d"
+ )
if not default_due_date:
return
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
- is_credit_controller = frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles()
+ is_credit_controller = (
+ frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles()
+ )
if is_credit_controller:
- msgprint(_("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)")
- .format(date_diff(due_date, default_due_date)))
+ msgprint(
+ _("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)").format(
+ date_diff(due_date, default_due_date)
+ )
+ )
else:
- frappe.throw(_("Due / Reference Date cannot be after {0}")
- .format(formatdate(default_due_date)))
+ frappe.throw(
+ _("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date))
+ )
+
@frappe.whitelist()
def get_address_tax_category(tax_category=None, billing_address=None, shipping_address=None):
- addr_tax_category_from = frappe.db.get_single_value("Accounts Settings", "determine_address_tax_category_from")
+ addr_tax_category_from = frappe.db.get_single_value(
+ "Accounts Settings", "determine_address_tax_category_from"
+ )
if addr_tax_category_from == "Shipping Address":
if shipping_address:
tax_category = frappe.db.get_value("Address", shipping_address, "tax_category") or tax_category
@@ -408,36 +576,48 @@ def get_address_tax_category(tax_category=None, billing_address=None, shipping_a
return cstr(tax_category)
+
@frappe.whitelist()
-def set_taxes(party, party_type, posting_date, company, customer_group=None, supplier_group=None, tax_category=None,
- billing_address=None, shipping_address=None, use_for_shopping_cart=None):
+def set_taxes(
+ party,
+ party_type,
+ posting_date,
+ company,
+ customer_group=None,
+ supplier_group=None,
+ tax_category=None,
+ billing_address=None,
+ shipping_address=None,
+ use_for_shopping_cart=None,
+):
from erpnext.accounts.doctype.tax_rule.tax_rule import get_party_details, get_tax_template
- args = {
- party_type.lower(): party,
- "company": company
- }
+
+ args = {party_type.lower(): party, "company": company}
if tax_category:
- args['tax_category'] = tax_category
+ args["tax_category"] = tax_category
if customer_group:
- args['customer_group'] = customer_group
+ args["customer_group"] = customer_group
if supplier_group:
- args['supplier_group'] = supplier_group
+ args["supplier_group"] = supplier_group
if billing_address or shipping_address:
- args.update(get_party_details(party, party_type, {"billing_address": billing_address, \
- "shipping_address": shipping_address }))
+ args.update(
+ get_party_details(
+ party, party_type, {"billing_address": billing_address, "shipping_address": shipping_address}
+ )
+ )
else:
args.update(get_party_details(party, party_type))
if party_type in ("Customer", "Lead"):
args.update({"tax_type": "Sales"})
- if party_type=='Lead':
- args['customer'] = None
- del args['lead']
+ if party_type == "Lead":
+ args["customer"] = None
+ del args["lead"]
else:
args.update({"tax_type": "Purchase"})
@@ -452,25 +632,27 @@ def get_payment_terms_template(party_name, party_type, company=None):
if party_type not in ("Customer", "Supplier"):
return
template = None
- if party_type == 'Customer':
- customer = frappe.get_cached_value("Customer", party_name,
- fieldname=['payment_terms', "customer_group"], as_dict=1)
+ if party_type == "Customer":
+ customer = frappe.get_cached_value(
+ "Customer", party_name, fieldname=["payment_terms", "customer_group"], as_dict=1
+ )
template = customer.payment_terms
if not template and customer.customer_group:
- template = frappe.get_cached_value("Customer Group",
- customer.customer_group, 'payment_terms')
+ template = frappe.get_cached_value("Customer Group", customer.customer_group, "payment_terms")
else:
- supplier = frappe.get_cached_value("Supplier", party_name,
- fieldname=['payment_terms', "supplier_group"], as_dict=1)
+ supplier = frappe.get_cached_value(
+ "Supplier", party_name, fieldname=["payment_terms", "supplier_group"], as_dict=1
+ )
template = supplier.payment_terms
if not template and supplier.supplier_group:
- template = frappe.get_cached_value("Supplier Group", supplier.supplier_group, 'payment_terms')
+ template = frappe.get_cached_value("Supplier Group", supplier.supplier_group, "payment_terms")
if not template and company:
- template = frappe.get_cached_value('Company', company, fieldname='payment_terms')
+ template = frappe.get_cached_value("Company", company, fieldname="payment_terms")
return template
+
def validate_party_frozen_disabled(party_type, party_name):
if frappe.flags.ignore_party_validation:
@@ -482,7 +664,9 @@ def validate_party_frozen_disabled(party_type, party_name):
if party.disabled:
frappe.throw(_("{0} {1} is disabled").format(party_type, party_name), PartyDisabled)
elif party.get("is_frozen"):
- frozen_accounts_modifier = frappe.db.get_single_value( 'Accounts Settings', 'frozen_accounts_modifier')
+ frozen_accounts_modifier = frappe.db.get_single_value(
+ "Accounts Settings", "frozen_accounts_modifier"
+ )
if not frozen_accounts_modifier in frappe.get_roles():
frappe.throw(_("{0} {1} is frozen").format(party_type, party_name), PartyFrozen)
@@ -490,99 +674,124 @@ def validate_party_frozen_disabled(party_type, party_name):
if frappe.db.get_value("Employee", party_name, "status") != "Active":
frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True)
+
def get_timeline_data(doctype, name):
- '''returns timeline data for the past one year'''
+ """returns timeline data for the past one year"""
from frappe.desk.form.load import get_communication_data
out = {}
- fields = 'creation, count(*)'
- after = add_years(None, -1).strftime('%Y-%m-%d')
- group_by='group by Date(creation)'
+ fields = "creation, count(*)"
+ after = add_years(None, -1).strftime("%Y-%m-%d")
+ group_by = "group by Date(creation)"
- data = get_communication_data(doctype, name, after=after, group_by='group by creation',
- fields='C.creation as creation, count(C.name)',as_dict=False)
+ data = get_communication_data(
+ doctype,
+ name,
+ after=after,
+ group_by="group by creation",
+ fields="C.creation as creation, count(C.name)",
+ as_dict=False,
+ )
# fetch and append data from Activity Log
- data += frappe.db.sql("""select {fields}
+ data += frappe.db.sql(
+ """select {fields}
from `tabActivity Log`
where (reference_doctype=%(doctype)s and reference_name=%(name)s)
or (timeline_doctype in (%(doctype)s) and timeline_name=%(name)s)
or (reference_doctype in ("Quotation", "Opportunity") and timeline_name=%(name)s)
and status!='Success' and creation > {after}
{group_by} order by creation desc
- """.format(fields=fields, group_by=group_by, after=after), {
- "doctype": doctype,
- "name": name
- }, as_dict=False)
+ """.format(
+ fields=fields, group_by=group_by, after=after
+ ),
+ {"doctype": doctype, "name": name},
+ as_dict=False,
+ )
timeline_items = dict(data)
for date, count in iteritems(timeline_items):
timestamp = get_timestamp(date)
- out.update({ timestamp: count })
+ out.update({timestamp: count})
return out
+
def get_dashboard_info(party_type, party, loyalty_program=None):
current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True)
- doctype = "Sales Invoice" if party_type=="Customer" else "Purchase Invoice"
+ doctype = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
- companies = frappe.get_all(doctype, filters={
- 'docstatus': 1,
- party_type.lower(): party
- }, distinct=1, fields=['company'])
+ companies = frappe.get_all(
+ doctype, filters={"docstatus": 1, party_type.lower(): party}, distinct=1, fields=["company"]
+ )
company_wise_info = []
- company_wise_grand_total = frappe.get_all(doctype,
+ company_wise_grand_total = frappe.get_all(
+ doctype,
filters={
- 'docstatus': 1,
+ "docstatus": 1,
party_type.lower(): party,
- 'posting_date': ('between', [current_fiscal_year.year_start_date, current_fiscal_year.year_end_date])
- },
- group_by="company",
- fields=["company", "sum(grand_total) as grand_total", "sum(base_grand_total) as base_grand_total"]
- )
+ "posting_date": (
+ "between",
+ [current_fiscal_year.year_start_date, current_fiscal_year.year_end_date],
+ ),
+ },
+ group_by="company",
+ fields=[
+ "company",
+ "sum(grand_total) as grand_total",
+ "sum(base_grand_total) as base_grand_total",
+ ],
+ )
loyalty_point_details = []
if party_type == "Customer":
- loyalty_point_details = frappe._dict(frappe.get_all("Loyalty Point Entry",
- filters={
- 'customer': party,
- 'expiry_date': ('>=', getdate()),
+ loyalty_point_details = frappe._dict(
+ frappe.get_all(
+ "Loyalty Point Entry",
+ filters={
+ "customer": party,
+ "expiry_date": (">=", getdate()),
},
group_by="company",
fields=["company", "sum(loyalty_points) as loyalty_points"],
- as_list =1
- ))
+ as_list=1,
+ )
+ )
company_wise_billing_this_year = frappe._dict()
for d in company_wise_grand_total:
company_wise_billing_this_year.setdefault(
- d.company,{
- "grand_total": d.grand_total,
- "base_grand_total": d.base_grand_total
- })
+ d.company, {"grand_total": d.grand_total, "base_grand_total": d.base_grand_total}
+ )
-
- company_wise_total_unpaid = frappe._dict(frappe.db.sql("""
+ company_wise_total_unpaid = frappe._dict(
+ frappe.db.sql(
+ """
select company, sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry`
where party_type = %s and party=%s
and is_cancelled = 0
- group by company""", (party_type, party)))
+ group by company""",
+ (party_type, party),
+ )
+ )
for d in companies:
- company_default_currency = frappe.db.get_value("Company", d.company, 'default_currency')
+ company_default_currency = frappe.db.get_value("Company", d.company, "default_currency")
party_account_currency = get_party_account_currency(party_type, party, d.company)
- if party_account_currency==company_default_currency:
- billing_this_year = flt(company_wise_billing_this_year.get(d.company,{}).get("base_grand_total"))
+ if party_account_currency == company_default_currency:
+ billing_this_year = flt(
+ company_wise_billing_this_year.get(d.company, {}).get("base_grand_total")
+ )
else:
- billing_this_year = flt(company_wise_billing_this_year.get(d.company,{}).get("grand_total"))
+ billing_this_year = flt(company_wise_billing_this_year.get(d.company, {}).get("grand_total"))
total_unpaid = flt(company_wise_total_unpaid.get(d.company))
@@ -605,6 +814,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
return company_wise_info
+
def get_party_shipping_address(doctype, name):
"""
Returns an Address name (best guess) for the given doctype and name for which `address_type == 'Shipping'` is true.
@@ -617,50 +827,59 @@ def get_party_shipping_address(doctype, name):
:return: String
"""
out = frappe.db.sql(
- 'SELECT dl.parent '
- 'from `tabDynamic Link` dl join `tabAddress` ta on dl.parent=ta.name '
- 'where '
- 'dl.link_doctype=%s '
- 'and dl.link_name=%s '
+ "SELECT dl.parent "
+ "from `tabDynamic Link` dl join `tabAddress` ta on dl.parent=ta.name "
+ "where "
+ "dl.link_doctype=%s "
+ "and dl.link_name=%s "
'and dl.parenttype="Address" '
- 'and ifnull(ta.disabled, 0) = 0 and'
+ "and ifnull(ta.disabled, 0) = 0 and"
'(ta.address_type="Shipping" or ta.is_shipping_address=1) '
- 'order by ta.is_shipping_address desc, ta.address_type desc limit 1',
- (doctype, name)
+ "order by ta.is_shipping_address desc, ta.address_type desc limit 1",
+ (doctype, name),
)
if out:
return out[0][0]
else:
- return ''
+ return ""
-def get_partywise_advanced_payment_amount(party_type, posting_date = None, future_payment=0, company=None):
+
+def get_partywise_advanced_payment_amount(
+ party_type, posting_date=None, future_payment=0, company=None
+):
cond = "1=1"
if posting_date:
if future_payment:
- cond = "posting_date <= '{0}' OR DATE(creation) <= '{0}' """.format(posting_date)
+ cond = "posting_date <= '{0}' OR DATE(creation) <= '{0}' " "".format(posting_date)
else:
cond = "posting_date <= '{0}'".format(posting_date)
if company:
cond += "and company = {0}".format(frappe.db.escape(company))
- data = frappe.db.sql(""" SELECT party, sum({0}) as amount
+ data = frappe.db.sql(
+ """ SELECT party, sum({0}) as amount
FROM `tabGL Entry`
WHERE
party_type = %s and against_voucher is null
and is_cancelled = 0
- and {1} GROUP BY party"""
- .format(("credit") if party_type == "Customer" else "debit", cond) , party_type)
+ and {1} GROUP BY party""".format(
+ ("credit") if party_type == "Customer" else "debit", cond
+ ),
+ party_type,
+ )
if data:
return frappe._dict(data)
+
def get_default_contact(doctype, name):
"""
- Returns default contact for the given doctype and name.
- Can be ordered by `contact_type` to either is_primary_contact or is_billing_contact.
+ Returns default contact for the given doctype and name.
+ Can be ordered by `contact_type` to either is_primary_contact or is_billing_contact.
"""
- out = frappe.db.sql("""
+ out = frappe.db.sql(
+ """
SELECT dl.parent, c.is_primary_contact, c.is_billing_contact
FROM `tabDynamic Link` dl
INNER JOIN tabContact c ON c.name = dl.parent
@@ -669,7 +888,9 @@ def get_default_contact(doctype, name):
dl.link_name=%s AND
dl.parenttype = "Contact"
ORDER BY is_primary_contact DESC, is_billing_contact DESC
- """, (doctype, name))
+ """,
+ (doctype, name),
+ )
if out:
try:
return out[0][0]
diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
index e6580493095..605ce8383e4 100644
--- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
+++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
@@ -1,7 +1,8 @@
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
-{%- set einvoice = json.loads(doc.signed_einvoice) -%}
+ {% if doc.signed_einvoice %}
+ {%- set einvoice = json.loads(doc.signed_einvoice) -%}
{% if letter_head and not no_letterhead %}
{{ letter_head }}
@@ -170,4 +171,10 @@
+ {% else %}
+
+ You must generate IRN before you can preview GST E-Invoice.
+
- """.format(table_head, table_row)
+ """.format(
+ table_head, table_row
+ )
continue
# on any other field type add label and value to html
- if not df.hidden and not df.print_hide and doc.get(df.fieldname) and df.fieldname not in exclude_fields:
+ if (
+ not df.hidden
+ and not df.print_hide
+ and doc.get(df.fieldname)
+ and df.fieldname not in exclude_fields
+ ):
formatted_value = format_value(doc.get(df.fieldname), meta.get_field(df.fieldname), doc)
html += " {0} : {1}".format(df.label or df.fieldname, formatted_value)
- if not has_data : has_data = True
+ if not has_data:
+ has_data = True
if sec_on and col_on and has_data:
doc_html += section_html + html + ""
@@ -751,49 +852,53 @@ def render_doc_as_html(doctype, docname, exclude_fields = None):
{0} {1}
- """.format(section_html, html)
+ """.format(
+ section_html, html
+ )
return {"html": doc_html}
def update_address_links(address, method):
- '''
+ """
Hook validate Address
If Patient is linked in Address, also link the associated Customer
- '''
- if 'Healthcare' not in frappe.get_active_domains():
+ """
+ if "Healthcare" not in frappe.get_active_domains():
return
- patient_links = list(filter(lambda link: link.get('link_doctype') == 'Patient', address.links))
+ patient_links = list(filter(lambda link: link.get("link_doctype") == "Patient", address.links))
for link in patient_links:
- customer = frappe.db.get_value('Patient', link.get('link_name'), 'customer')
- if customer and not address.has_link('Customer', customer):
- address.append('links', dict(link_doctype = 'Customer', link_name = customer))
+ customer = frappe.db.get_value("Patient", link.get("link_name"), "customer")
+ if customer and not address.has_link("Customer", customer):
+ address.append("links", dict(link_doctype="Customer", link_name=customer))
def update_patient_email_and_phone_numbers(contact, method):
- '''
+ """
Hook validate Contact
Update linked Patients' primary mobile and phone numbers
- '''
- if 'Healthcare' not in frappe.get_active_domains() or contact.flags.skip_patient_update:
+ """
+ if "Healthcare" not in frappe.get_active_domains() or contact.flags.skip_patient_update:
return
if contact.is_primary_contact and (contact.email_id or contact.mobile_no or contact.phone):
- patient_links = list(filter(lambda link: link.get('link_doctype') == 'Patient', contact.links))
+ patient_links = list(filter(lambda link: link.get("link_doctype") == "Patient", contact.links))
for link in patient_links:
- contact_details = frappe.db.get_value('Patient', link.get('link_name'), ['email', 'mobile', 'phone'], as_dict=1)
+ contact_details = frappe.db.get_value(
+ "Patient", link.get("link_name"), ["email", "mobile", "phone"], as_dict=1
+ )
new_contact_details = {}
- if contact.email_id and contact.email_id != contact_details.get('email'):
- new_contact_details.update({'email': contact.email_id})
- if contact.mobile_no and contact.mobile_no != contact_details.get('mobile'):
- new_contact_details.update({'mobile': contact.mobile_no})
- if contact.phone and contact.phone != contact_details.get('phone'):
- new_contact_details.update({'phone': contact.phone})
+ if contact.email_id and contact.email_id != contact_details.get("email"):
+ new_contact_details.update({"email": contact.email_id})
+ if contact.mobile_no and contact.mobile_no != contact_details.get("mobile"):
+ new_contact_details.update({"mobile": contact.mobile_no})
+ if contact.phone and contact.phone != contact_details.get("phone"):
+ new_contact_details.update({"phone": contact.phone})
if new_contact_details:
- frappe.db.set_value('Patient', link.get('link_name'), new_contact_details)
+ frappe.db.set_value("Patient", link.get("link_name"), new_contact_details)
diff --git a/erpnext/healthcare/web_form/lab_test/lab_test.py b/erpnext/healthcare/web_form/lab_test/lab_test.py
index 94ffbcfa2f2..1bccbd4f4e7 100644
--- a/erpnext/healthcare/web_form/lab_test/lab_test.py
+++ b/erpnext/healthcare/web_form/lab_test/lab_test.py
@@ -1,22 +1,31 @@
-
import frappe
def get_context(context):
context.read_only = 1
+
def get_list_context(context):
context.row_template = "erpnext/templates/includes/healthcare/lab_test_row_template.html"
context.get_list = get_lab_test_list
-def get_lab_test_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by='modified desc'):
+
+def get_lab_test_list(
+ doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified desc"
+):
patient = get_patient()
- lab_tests = frappe.db.sql("""select * from `tabLab Test`
- where patient = %s order by result_date""", patient, as_dict = True)
+ lab_tests = frappe.db.sql(
+ """select * from `tabLab Test`
+ where patient = %s order by result_date""",
+ patient,
+ as_dict=True,
+ )
return lab_tests
+
def get_patient():
- return frappe.get_value("Patient",{"email": frappe.session.user}, "name")
+ return frappe.get_value("Patient", {"email": frappe.session.user}, "name")
+
def has_website_permission(doc, ptype, user, verbose=False):
if doc.patient == get_patient():
diff --git a/erpnext/healthcare/web_form/patient_appointments/patient_appointments.py b/erpnext/healthcare/web_form/patient_appointments/patient_appointments.py
index 9f0903cece7..89e8eb168f3 100644
--- a/erpnext/healthcare/web_form/patient_appointments/patient_appointments.py
+++ b/erpnext/healthcare/web_form/patient_appointments/patient_appointments.py
@@ -1,22 +1,31 @@
-
import frappe
def get_context(context):
context.read_only = 1
+
def get_list_context(context):
context.row_template = "erpnext/templates/includes/healthcare/appointment_row_template.html"
context.get_list = get_appointment_list
-def get_appointment_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by='modified desc'):
+
+def get_appointment_list(
+ doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified desc"
+):
patient = get_patient()
- lab_tests = frappe.db.sql("""select * from `tabPatient Appointment`
- where patient = %s and (status = 'Open' or status = 'Scheduled') order by appointment_date""", patient, as_dict = True)
+ lab_tests = frappe.db.sql(
+ """select * from `tabPatient Appointment`
+ where patient = %s and (status = 'Open' or status = 'Scheduled') order by appointment_date""",
+ patient,
+ as_dict=True,
+ )
return lab_tests
+
def get_patient():
- return frappe.get_value("Patient",{"email": frappe.session.user}, "name")
+ return frappe.get_value("Patient", {"email": frappe.session.user}, "name")
+
def has_website_permission(doc, ptype, user, verbose=False):
if doc.patient == get_patient():
diff --git a/erpnext/healthcare/web_form/patient_registration/patient_registration.py b/erpnext/healthcare/web_form/patient_registration/patient_registration.py
index 19b550feea7..02e3e933330 100644
--- a/erpnext/healthcare/web_form/patient_registration/patient_registration.py
+++ b/erpnext/healthcare/web_form/patient_registration/patient_registration.py
@@ -1,5 +1,3 @@
-
-
def get_context(context):
# do your magic here
pass
diff --git a/erpnext/healthcare/web_form/personal_details/personal_details.py b/erpnext/healthcare/web_form/personal_details/personal_details.py
index fc8e8c0a5b7..dfbd73eeec1 100644
--- a/erpnext/healthcare/web_form/personal_details/personal_details.py
+++ b/erpnext/healthcare/web_form/personal_details/personal_details.py
@@ -1,23 +1,25 @@
-
import frappe
from frappe import _
no_cache = 1
+
def get_context(context):
- if frappe.session.user=='Guest':
+ if frappe.session.user == "Guest":
frappe.throw(_("You need to be logged in to access this page"), frappe.PermissionError)
- context.show_sidebar=True
+ context.show_sidebar = True
- if frappe.db.exists("Patient", {'email': frappe.session.user}):
- patient = frappe.get_doc("Patient", {'email': frappe.session.user})
+ if frappe.db.exists("Patient", {"email": frappe.session.user}):
+ patient = frappe.get_doc("Patient", {"email": frappe.session.user})
context.doc = patient
frappe.form_dict.new = 0
frappe.form_dict.name = patient.name
+
def get_patient():
- return frappe.get_value("Patient",{"email": frappe.session.user}, "name")
+ return frappe.get_value("Patient", {"email": frappe.session.user}, "name")
+
def has_website_permission(doc, ptype, user, verbose=False):
if doc.name == get_patient():
diff --git a/erpnext/healthcare/web_form/prescription/prescription.py b/erpnext/healthcare/web_form/prescription/prescription.py
index 0e1bc3d5dd6..44caeb00b59 100644
--- a/erpnext/healthcare/web_form/prescription/prescription.py
+++ b/erpnext/healthcare/web_form/prescription/prescription.py
@@ -1,22 +1,31 @@
-
import frappe
def get_context(context):
context.read_only = 1
+
def get_list_context(context):
context.row_template = "erpnext/templates/includes/healthcare/prescription_row_template.html"
context.get_list = get_encounter_list
-def get_encounter_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by='modified desc'):
+
+def get_encounter_list(
+ doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified desc"
+):
patient = get_patient()
- encounters = frappe.db.sql("""select * from `tabPatient Encounter`
- where patient = %s order by creation desc""", patient, as_dict = True)
+ encounters = frappe.db.sql(
+ """select * from `tabPatient Encounter`
+ where patient = %s order by creation desc""",
+ patient,
+ as_dict=True,
+ )
return encounters
+
def get_patient():
- return frappe.get_value("Patient",{"email": frappe.session.user}, "name")
+ return frappe.get_value("Patient", {"email": frappe.session.user}, "name")
+
def has_website_permission(doc, ptype, user, verbose=False):
if doc.patient == get_patient():
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 00e46393590..ff120a52926 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -1,4 +1,3 @@
-
from frappe import _
app_name = "erpnext"
@@ -13,7 +12,7 @@ source_link = "https://github.com/frappe/erpnext"
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
-develop_version = '13.x.x-develop'
+develop_version = "13.x.x-develop"
app_include_js = "/assets/js/erpnext.min.js"
app_include_css = "/assets/css/erpnext.css"
@@ -25,12 +24,10 @@ doctype_js = {
"Communication": "public/js/communication.js",
"Event": "public/js/event.js",
"Newsletter": "public/js/newsletter.js",
- "Contact": "public/js/contact.js"
+ "Contact": "public/js/contact.js",
}
-override_doctype_class = {
- 'Address': 'erpnext.accounts.custom.address.ERPNextAddress'
-}
+override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
welcome_email = "erpnext.setup.utils.welcome_email"
@@ -51,146 +48,266 @@ additional_print_settings = "erpnext.controllers.print_settings.get_print_settin
on_session_creation = [
"erpnext.portal.utils.create_customer_or_supplier",
- "erpnext.e_commerce.shopping_cart.utils.set_cart_count"
+ "erpnext.e_commerce.shopping_cart.utils.set_cart_count",
]
on_logout = "erpnext.e_commerce.shopping_cart.utils.clear_cart_count"
-treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Group', 'Sales Person', 'Territory', 'Assessment Group', 'Department']
+treeviews = [
+ "Account",
+ "Cost Center",
+ "Warehouse",
+ "Item Group",
+ "Customer Group",
+ "Sales Person",
+ "Territory",
+ "Assessment Group",
+ "Department",
+]
# website
-update_website_context = ["erpnext.e_commerce.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"]
+update_website_context = [
+ "erpnext.e_commerce.shopping_cart.utils.update_website_context",
+ "erpnext.education.doctype.education_settings.education_settings.update_website_context",
+]
my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context"
webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context"
-calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"]
+calendars = [
+ "Task",
+ "Work Order",
+ "Leave Application",
+ "Sales Order",
+ "Holiday List",
+ "Course Schedule",
+]
domains = {
- 'Agriculture': 'erpnext.domains.agriculture',
- 'Distribution': 'erpnext.domains.distribution',
- 'Education': 'erpnext.domains.education',
- 'Healthcare': 'erpnext.domains.healthcare',
- 'Hospitality': 'erpnext.domains.hospitality',
- 'Manufacturing': 'erpnext.domains.manufacturing',
- 'Non Profit': 'erpnext.domains.non_profit',
- 'Retail': 'erpnext.domains.retail',
- 'Services': 'erpnext.domains.services',
+ "Agriculture": "erpnext.domains.agriculture",
+ "Distribution": "erpnext.domains.distribution",
+ "Education": "erpnext.domains.education",
+ "Healthcare": "erpnext.domains.healthcare",
+ "Hospitality": "erpnext.domains.hospitality",
+ "Manufacturing": "erpnext.domains.manufacturing",
+ "Non Profit": "erpnext.domains.non_profit",
+ "Retail": "erpnext.domains.retail",
+ "Services": "erpnext.domains.services",
}
-website_generators = ["Item Group", "Website Item", "BOM", "Sales Partner",
- "Job Opening", "Student Admission"]
+website_generators = [
+ "Item Group",
+ "Website Item",
+ "BOM",
+ "Sales Partner",
+ "Job Opening",
+ "Student Admission",
+]
website_context = {
- "favicon": "/assets/erpnext/images/erpnext-favicon.svg",
- "splash_image": "/assets/erpnext/images/erpnext-logo.svg"
+ "favicon": "/assets/erpnext/images/erpnext-favicon.svg",
+ "splash_image": "/assets/erpnext/images/erpnext-logo.svg",
}
website_route_rules = [
{"from_route": "/orders", "to_route": "Sales Order"},
- {"from_route": "/orders/", "to_route": "order",
- "defaults": {
- "doctype": "Sales Order",
- "parents": [{"label": _("Orders"), "route": "orders"}]
- }
+ {
+ "from_route": "/orders/",
+ "to_route": "order",
+ "defaults": {"doctype": "Sales Order", "parents": [{"label": _("Orders"), "route": "orders"}]},
},
{"from_route": "/invoices", "to_route": "Sales Invoice"},
- {"from_route": "/invoices/", "to_route": "order",
+ {
+ "from_route": "/invoices/",
+ "to_route": "order",
"defaults": {
"doctype": "Sales Invoice",
- "parents": [{"label": _("Invoices"), "route": "invoices"}]
- }
+ "parents": [{"label": _("Invoices"), "route": "invoices"}],
+ },
},
{"from_route": "/supplier-quotations", "to_route": "Supplier Quotation"},
- {"from_route": "/supplier-quotations/", "to_route": "order",
+ {
+ "from_route": "/supplier-quotations/",
+ "to_route": "order",
"defaults": {
"doctype": "Supplier Quotation",
- "parents": [{"label": _("Supplier Quotation"), "route": "supplier-quotations"}]
- }
+ "parents": [{"label": _("Supplier Quotation"), "route": "supplier-quotations"}],
+ },
},
{"from_route": "/purchase-orders", "to_route": "Purchase Order"},
- {"from_route": "/purchase-orders/", "to_route": "order",
+ {
+ "from_route": "/purchase-orders/",
+ "to_route": "order",
"defaults": {
"doctype": "Purchase Order",
- "parents": [{"label": _("Purchase Order"), "route": "purchase-orders"}]
- }
+ "parents": [{"label": _("Purchase Order"), "route": "purchase-orders"}],
+ },
},
{"from_route": "/purchase-invoices", "to_route": "Purchase Invoice"},
- {"from_route": "/purchase-invoices/", "to_route": "order",
+ {
+ "from_route": "/purchase-invoices/",
+ "to_route": "order",
"defaults": {
"doctype": "Purchase Invoice",
- "parents": [{"label": _("Purchase Invoice"), "route": "purchase-invoices"}]
- }
+ "parents": [{"label": _("Purchase Invoice"), "route": "purchase-invoices"}],
+ },
},
{"from_route": "/quotations", "to_route": "Quotation"},
- {"from_route": "/quotations/", "to_route": "order",
+ {
+ "from_route": "/quotations/",
+ "to_route": "order",
"defaults": {
"doctype": "Quotation",
- "parents": [{"label": _("Quotations"), "route": "quotations"}]
- }
+ "parents": [{"label": _("Quotations"), "route": "quotations"}],
+ },
},
{"from_route": "/shipments", "to_route": "Delivery Note"},
- {"from_route": "/shipments/", "to_route": "order",
+ {
+ "from_route": "/shipments/",
+ "to_route": "order",
"defaults": {
"doctype": "Delivery Note",
- "parents": [{"label": _("Shipments"), "route": "shipments"}]
- }
+ "parents": [{"label": _("Shipments"), "route": "shipments"}],
+ },
},
{"from_route": "/rfq", "to_route": "Request for Quotation"},
- {"from_route": "/rfq/", "to_route": "rfq",
+ {
+ "from_route": "/rfq/",
+ "to_route": "rfq",
"defaults": {
"doctype": "Request for Quotation",
- "parents": [{"label": _("Request for Quotation"), "route": "rfq"}]
- }
+ "parents": [{"label": _("Request for Quotation"), "route": "rfq"}],
+ },
},
{"from_route": "/addresses", "to_route": "Address"},
- {"from_route": "/addresses/", "to_route": "addresses",
- "defaults": {
- "doctype": "Address",
- "parents": [{"label": _("Addresses"), "route": "addresses"}]
- }
+ {
+ "from_route": "/addresses/",
+ "to_route": "addresses",
+ "defaults": {"doctype": "Address", "parents": [{"label": _("Addresses"), "route": "addresses"}]},
},
{"from_route": "/jobs", "to_route": "Job Opening"},
{"from_route": "/admissions", "to_route": "Student Admission"},
{"from_route": "/boms", "to_route": "BOM"},
{"from_route": "/timesheets", "to_route": "Timesheet"},
{"from_route": "/material-requests", "to_route": "Material Request"},
- {"from_route": "/material-requests/", "to_route": "material_request_info",
+ {
+ "from_route": "/material-requests/",
+ "to_route": "material_request_info",
"defaults": {
"doctype": "Material Request",
- "parents": [{"label": _("Material Request"), "route": "material-requests"}]
- }
+ "parents": [{"label": _("Material Request"), "route": "material-requests"}],
+ },
},
- {"from_route": "/project", "to_route": "Project"}
+ {"from_route": "/project", "to_route": "Project"},
]
standard_portal_menu_items = [
- {"title": _("Personal Details"), "route": "/personal-details", "reference_doctype": "Patient", "role": "Patient"},
+ {
+ "title": _("Personal Details"),
+ "route": "/personal-details",
+ "reference_doctype": "Patient",
+ "role": "Patient",
+ },
{"title": _("Projects"), "route": "/project", "reference_doctype": "Project"},
- {"title": _("Request for Quotations"), "route": "/rfq", "reference_doctype": "Request for Quotation", "role": "Supplier"},
- {"title": _("Supplier Quotation"), "route": "/supplier-quotations", "reference_doctype": "Supplier Quotation", "role": "Supplier"},
- {"title": _("Purchase Orders"), "route": "/purchase-orders", "reference_doctype": "Purchase Order", "role": "Supplier"},
- {"title": _("Purchase Invoices"), "route": "/purchase-invoices", "reference_doctype": "Purchase Invoice", "role": "Supplier"},
- {"title": _("Quotations"), "route": "/quotations", "reference_doctype": "Quotation", "role":"Customer"},
- {"title": _("Orders"), "route": "/orders", "reference_doctype": "Sales Order", "role":"Customer"},
- {"title": _("Invoices"), "route": "/invoices", "reference_doctype": "Sales Invoice", "role":"Customer"},
- {"title": _("Shipments"), "route": "/shipments", "reference_doctype": "Delivery Note", "role":"Customer"},
- {"title": _("Issues"), "route": "/issues", "reference_doctype": "Issue", "role":"Customer"},
+ {
+ "title": _("Request for Quotations"),
+ "route": "/rfq",
+ "reference_doctype": "Request for Quotation",
+ "role": "Supplier",
+ },
+ {
+ "title": _("Supplier Quotation"),
+ "route": "/supplier-quotations",
+ "reference_doctype": "Supplier Quotation",
+ "role": "Supplier",
+ },
+ {
+ "title": _("Purchase Orders"),
+ "route": "/purchase-orders",
+ "reference_doctype": "Purchase Order",
+ "role": "Supplier",
+ },
+ {
+ "title": _("Purchase Invoices"),
+ "route": "/purchase-invoices",
+ "reference_doctype": "Purchase Invoice",
+ "role": "Supplier",
+ },
+ {
+ "title": _("Quotations"),
+ "route": "/quotations",
+ "reference_doctype": "Quotation",
+ "role": "Customer",
+ },
+ {
+ "title": _("Orders"),
+ "route": "/orders",
+ "reference_doctype": "Sales Order",
+ "role": "Customer",
+ },
+ {
+ "title": _("Invoices"),
+ "route": "/invoices",
+ "reference_doctype": "Sales Invoice",
+ "role": "Customer",
+ },
+ {
+ "title": _("Shipments"),
+ "route": "/shipments",
+ "reference_doctype": "Delivery Note",
+ "role": "Customer",
+ },
+ {"title": _("Issues"), "route": "/issues", "reference_doctype": "Issue", "role": "Customer"},
{"title": _("Addresses"), "route": "/addresses", "reference_doctype": "Address"},
- {"title": _("Timesheets"), "route": "/timesheets", "reference_doctype": "Timesheet", "role":"Customer"},
- {"title": _("Lab Test"), "route": "/lab-test", "reference_doctype": "Lab Test", "role":"Patient"},
- {"title": _("Prescription"), "route": "/prescription", "reference_doctype": "Patient Encounter", "role":"Patient"},
- {"title": _("Patient Appointment"), "route": "/patient-appointments", "reference_doctype": "Patient Appointment", "role":"Patient"},
- {"title": _("Fees"), "route": "/fees", "reference_doctype": "Fees", "role":"Student"},
+ {
+ "title": _("Timesheets"),
+ "route": "/timesheets",
+ "reference_doctype": "Timesheet",
+ "role": "Customer",
+ },
+ {
+ "title": _("Lab Test"),
+ "route": "/lab-test",
+ "reference_doctype": "Lab Test",
+ "role": "Patient",
+ },
+ {
+ "title": _("Prescription"),
+ "route": "/prescription",
+ "reference_doctype": "Patient Encounter",
+ "role": "Patient",
+ },
+ {
+ "title": _("Patient Appointment"),
+ "route": "/patient-appointments",
+ "reference_doctype": "Patient Appointment",
+ "role": "Patient",
+ },
+ {"title": _("Fees"), "route": "/fees", "reference_doctype": "Fees", "role": "Student"},
{"title": _("Newsletter"), "route": "/newsletters", "reference_doctype": "Newsletter"},
- {"title": _("Admission"), "route": "/admissions", "reference_doctype": "Student Admission", "role": "Student"},
- {"title": _("Certification"), "route": "/certification", "reference_doctype": "Certification Application", "role": "Non Profit Portal User"},
- {"title": _("Material Request"), "route": "/material-requests", "reference_doctype": "Material Request", "role": "Customer"},
+ {
+ "title": _("Admission"),
+ "route": "/admissions",
+ "reference_doctype": "Student Admission",
+ "role": "Student",
+ },
+ {
+ "title": _("Certification"),
+ "route": "/certification",
+ "reference_doctype": "Certification Application",
+ "role": "Non Profit Portal User",
+ },
+ {
+ "title": _("Material Request"),
+ "route": "/material-requests",
+ "reference_doctype": "Material Request",
+ "role": "Customer",
+ },
{"title": _("Appointment Booking"), "route": "/book_appointment"},
]
default_roles = [
- {'role': 'Customer', 'doctype':'Contact', 'email_field': 'email_id'},
- {'role': 'Supplier', 'doctype':'Contact', 'email_field': 'email_id'},
- {'role': 'Student', 'doctype':'Student', 'email_field': 'student_email_id'},
+ {"role": "Customer", "doctype": "Contact", "email_field": "email_id"},
+ {"role": "Supplier", "doctype": "Contact", "email_field": "email_id"},
+ {"role": "Student", "doctype": "Student", "email_field": "student_email_id"},
]
sounds = [
@@ -198,9 +315,7 @@ sounds = [
{"name": "call-disconnect", "src": "/assets/erpnext/sounds/call-disconnect.mp3", "volume": 0.2},
]
-has_upload_permission = {
- "Employee": "erpnext.hr.doctype.employee.employee.has_upload_permission"
-}
+has_upload_permission = {"Employee": "erpnext.hr.doctype.employee.employee.has_upload_permission"}
has_website_permission = {
"Sales Order": "erpnext.controllers.website_list_for_contact.has_website_permission",
@@ -216,7 +331,7 @@ has_website_permission = {
"Lab Test": "erpnext.healthcare.web_form.lab_test.lab_test.has_website_permission",
"Patient Encounter": "erpnext.healthcare.web_form.prescription.prescription.has_website_permission",
"Patient Appointment": "erpnext.healthcare.web_form.patient_appointments.patient_appointments.has_website_permission",
- "Patient": "erpnext.healthcare.web_form.personal_details.personal_details.has_website_permission"
+ "Patient": "erpnext.healthcare.web_form.personal_details.personal_details.has_website_permission",
}
dump_report_map = "erpnext.startup.report_data_map.data_map"
@@ -225,112 +340,115 @@ before_tests = "erpnext.setup.utils.before_tests"
standard_queries = {
"Customer": "erpnext.selling.doctype.customer.customer.get_customer_list",
- "Healthcare Practitioner": "erpnext.healthcare.doctype.healthcare_practitioner.healthcare_practitioner.get_practitioner_list"
+ "Healthcare Practitioner": "erpnext.healthcare.doctype.healthcare_practitioner.healthcare_practitioner.get_practitioner_list",
}
doc_events = {
"*": {
"on_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.create_medical_record",
"on_update_after_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.update_medical_record",
- "on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.delete_medical_record"
+ "on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.delete_medical_record",
},
"Stock Entry": {
"on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty",
- "on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty"
+ "on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty",
},
"User": {
"after_insert": "frappe.contacts.doctype.contact.contact.update_contact",
"validate": "erpnext.hr.doctype.employee.employee.validate_employee_role",
- "on_update": ["erpnext.hr.doctype.employee.employee.update_user_permissions",
- "erpnext.portal.utils.set_default_role"]
- },
- "Communication": {
"on_update": [
- "erpnext.support.doctype.issue.issue.set_first_response_time"
- ]
+ "erpnext.hr.doctype.employee.employee.update_user_permissions",
+ "erpnext.portal.utils.set_default_role",
+ ],
},
+ "Communication": {"on_update": ["erpnext.support.doctype.issue.issue.set_first_response_time"]},
"Sales Taxes and Charges Template": {
"on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings"
},
- "Tax Category": {
- "validate": "erpnext.regional.india.utils.validate_tax_category"
- },
+ "Tax Category": {"validate": "erpnext.regional.india.utils.validate_tax_category"},
"Sales Invoice": {
"on_submit": [
"erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit",
"erpnext.regional.saudi_arabia.utils.create_qr_code",
- "erpnext.erpnext_integrations.taxjar_integration.create_transaction"
+ "erpnext.erpnext_integrations.taxjar_integration.create_transaction",
],
"on_cancel": [
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
"erpnext.erpnext_integrations.taxjar_integration.delete_transaction",
- "erpnext.regional.saudi_arabia.utils.delete_qr_code_file"
+ "erpnext.regional.saudi_arabia.utils.delete_qr_code_file",
],
"on_trash": "erpnext.regional.check_deletion_permission",
"validate": [
"erpnext.regional.india.utils.validate_document_name",
- "erpnext.regional.india.utils.update_taxable_values"
- ]
- },
- "POS Invoice": {
- "on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]
+ "erpnext.regional.india.utils.update_taxable_values",
+ ],
},
+ "POS Invoice": {"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]},
"Purchase Invoice": {
"validate": [
"erpnext.regional.india.utils.validate_reverse_charge_transaction",
"erpnext.regional.india.utils.update_itc_availed_fields",
"erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm",
"erpnext.regional.united_arab_emirates.utils.validate_returns",
- "erpnext.regional.india.utils.update_taxable_values"
+ "erpnext.regional.india.utils.update_taxable_values",
]
},
"Payment Entry": {
"validate": "erpnext.regional.india.utils.update_place_of_supply",
- "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
- "on_trash": "erpnext.regional.check_deletion_permission"
+ "on_submit": [
+ "erpnext.regional.create_transaction_log",
+ "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status",
+ "erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
+ ],
+ "on_trash": "erpnext.regional.check_deletion_permission",
},
- 'Address': {
- 'validate': [
- 'erpnext.regional.india.utils.validate_gstin_for_india',
- 'erpnext.regional.italy.utils.set_state_code',
- 'erpnext.regional.india.utils.update_gst_category',
- 'erpnext.healthcare.utils.update_address_links'
+ "Address": {
+ "validate": [
+ "erpnext.regional.india.utils.validate_gstin_for_india",
+ "erpnext.regional.italy.utils.set_state_code",
+ "erpnext.regional.india.utils.update_gst_category",
+ "erpnext.healthcare.utils.update_address_links",
],
},
- 'Supplier': {
- 'validate': 'erpnext.regional.india.utils.validate_pan_for_india'
- },
- ('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): {
- 'validate': ['erpnext.regional.india.utils.set_place_of_supply']
- },
+ "Supplier": {"validate": "erpnext.regional.india.utils.validate_pan_for_india"},
+ (
+ "Sales Invoice",
+ "Sales Order",
+ "Delivery Note",
+ "Purchase Invoice",
+ "Purchase Order",
+ "Purchase Receipt",
+ ): {"validate": ["erpnext.regional.india.utils.set_place_of_supply"]},
"Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations",
- "validate": ["erpnext.crm.utils.update_lead_phone_numbers", "erpnext.healthcare.utils.update_patient_email_and_phone_numbers"]
+ "validate": [
+ "erpnext.crm.utils.update_lead_phone_numbers",
+ "erpnext.healthcare.utils.update_patient_email_and_phone_numbers",
+ ],
},
"Email Unsubscribe": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"
},
- ('Quotation', 'Sales Order', 'Sales Invoice'): {
- 'validate': ["erpnext.erpnext_integrations.taxjar_integration.set_sales_tax"]
+ ("Quotation", "Sales Order", "Sales Invoice"): {
+ "validate": ["erpnext.erpnext_integrations.taxjar_integration.set_sales_tax"]
},
"Company": {
- "on_trash": ["erpnext.regional.india.utils.delete_gst_settings_for_company",
- "erpnext.regional.saudi_arabia.utils.delete_vat_settings_for_company"]
+ "on_trash": [
+ "erpnext.regional.india.utils.delete_gst_settings_for_company",
+ "erpnext.regional.saudi_arabia.utils.delete_vat_settings_for_company",
+ ]
},
"Integration Request": {
"validate": "erpnext.accounts.doctype.payment_request.payment_request.validate_payment"
- }
+ },
}
# On cancel event Payment Entry will be exempted and all linked submittable doctype will get cancelled.
# to maintain data integrity we exempted payment entry. it will un-link when sales invoice get cancelled.
# if payment entry not in auto cancel exempted doctypes it will cancel payment entry.
-auto_cancel_exempted_doctypes= [
- "Payment Entry",
- "Inpatient Medication Entry"
-]
+auto_cancel_exempted_doctypes = ["Payment Entry", "Inpatient Medication Entry"]
after_migrate = ["erpnext.setup.install.update_select_perm_after_install"]
@@ -344,10 +462,10 @@ scheduler_events = {
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder",
"erpnext.hr.doctype.interview.interview.send_interview_reminder",
- "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts"
+ "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts",
],
"hourly": [
- 'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails',
+ "erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails",
"erpnext.accounts.doctype.subscription.subscription.process_all",
"erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details",
"erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs",
@@ -356,7 +474,7 @@ scheduler_events = {
"erpnext.projects.doctype.project.project.collect_project_status",
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
"erpnext.support.doctype.issue.issue.set_service_level_agreement_variance",
- "erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders"
+ "erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
],
"hourly_long": [
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
@@ -389,28 +507,24 @@ scheduler_events = {
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
"erpnext.non_profit.doctype.membership.membership.set_expired_status",
- "erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder"
+ "erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder",
],
"daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send",
- "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms",
+ "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms",
"erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation",
"erpnext.hr.utils.generate_leave_encashment",
"erpnext.hr.utils.allocate_earned_leaves",
"erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
- "erpnext.crm.doctype.lead.lead.daily_open_lead"
- ],
- "weekly": [
- "erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly"
- ],
- "monthly": [
- "erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly"
+ "erpnext.crm.doctype.lead.lead.daily_open_lead",
],
+ "weekly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly"],
+ "monthly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly"],
"monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
- "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans"
- ]
+ "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans",
+ ],
}
email_brand_image = "assets/erpnext/images/erpnext-logo.jpg"
@@ -429,63 +543,99 @@ get_translated_dict = {
}
bot_parsers = [
- 'erpnext.utilities.bot.FindItemBot',
+ "erpnext.utilities.bot.FindItemBot",
]
-get_site_info = 'erpnext.utilities.get_site_info'
+get_site_info = "erpnext.utilities.get_site_info"
payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account"
communication_doctypes = ["Customer", "Supplier"]
-accounting_dimension_doctypes = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset",
- "Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note",
- "Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item",
- "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
- "Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
- "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
- "Subscription Plan", "POS Invoice", "POS Invoice Item"
+accounting_dimension_doctypes = [
+ "GL Entry",
+ "Sales Invoice",
+ "Purchase Invoice",
+ "Payment Entry",
+ "Asset",
+ "Expense Claim",
+ "Expense Claim Detail",
+ "Expense Taxes and Charges",
+ "Stock Entry",
+ "Budget",
+ "Payroll Entry",
+ "Delivery Note",
+ "Sales Invoice Item",
+ "Purchase Invoice Item",
+ "Purchase Order Item",
+ "Journal Entry Account",
+ "Material Request Item",
+ "Delivery Note Item",
+ "Purchase Receipt Item",
+ "Stock Entry Detail",
+ "Payment Entry Deduction",
+ "Sales Taxes and Charges",
+ "Purchase Taxes and Charges",
+ "Shipping Rule",
+ "Landed Cost Item",
+ "Asset Value Adjustment",
+ "Loyalty Program",
+ "Fee Schedule",
+ "Fee Structure",
+ "Stock Reconciliation",
+ "Travel Request",
+ "Fees",
+ "POS Profile",
+ "Opening Invoice Creation Tool",
+ "Opening Invoice Creation Tool Item",
+ "Subscription",
+ "Subscription Plan",
+ "POS Invoice",
+ "POS Invoice Item",
+ "Purchase Order",
+ "Purchase Receipt",
+ "Sales Order",
]
regional_overrides = {
- 'France': {
- 'erpnext.tests.test_regional.test_method': 'erpnext.regional.france.utils.test_method'
+ "France": {
+ "erpnext.tests.test_regional.test_method": "erpnext.regional.france.utils.test_method"
},
- 'India': {
- 'erpnext.tests.test_regional.test_method': 'erpnext.regional.india.utils.test_method',
- 'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_header': 'erpnext.regional.india.utils.get_itemised_tax_breakup_header',
- 'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_data': 'erpnext.regional.india.utils.get_itemised_tax_breakup_data',
- 'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details',
- 'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts',
- 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
- 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
- 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields',
- 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount',
- 'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code'
+ "India": {
+ "erpnext.tests.test_regional.test_method": "erpnext.regional.india.utils.test_method",
+ "erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_header": "erpnext.regional.india.utils.get_itemised_tax_breakup_header",
+ "erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_data": "erpnext.regional.india.utils.get_itemised_tax_breakup_data",
+ "erpnext.accounts.party.get_regional_address_details": "erpnext.regional.india.utils.get_regional_address_details",
+ "erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts": "erpnext.regional.india.utils.get_regional_round_off_accounts",
+ "erpnext.hr.utils.calculate_annual_eligible_hra_exemption": "erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption",
+ "erpnext.hr.utils.calculate_hra_exemption_for_period": "erpnext.regional.india.utils.calculate_hra_exemption_for_period",
+ "erpnext.controllers.accounts_controller.validate_einvoice_fields": "erpnext.regional.india.e_invoice.utils.validate_einvoice_fields",
+ "erpnext.assets.doctype.asset.asset.get_depreciation_amount": "erpnext.regional.india.utils.get_depreciation_amount",
+ "erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code": "erpnext.regional.india.utils.set_item_tax_from_hsn_code",
},
- 'United Arab Emirates': {
- 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data',
- 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.united_arab_emirates.utils.make_regional_gl_entries',
+ "United Arab Emirates": {
+ "erpnext.controllers.taxes_and_totals.update_itemised_tax_data": "erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data",
+ "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries": "erpnext.regional.united_arab_emirates.utils.make_regional_gl_entries",
},
- 'Saudi Arabia': {
- 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data'
+ "Saudi Arabia": {
+ "erpnext.controllers.taxes_and_totals.update_itemised_tax_data": "erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data"
+ },
+ "Italy": {
+ "erpnext.controllers.taxes_and_totals.update_itemised_tax_data": "erpnext.regional.italy.utils.update_itemised_tax_data",
+ "erpnext.controllers.accounts_controller.validate_regional": "erpnext.regional.italy.utils.sales_invoice_validate",
},
- 'Italy': {
- 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.italy.utils.update_itemised_tax_data',
- 'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.italy.utils.sales_invoice_validate',
- }
}
user_privacy_documents = [
{
- 'doctype': 'Lead',
- 'match_field': 'email_id',
- 'personal_fields': ['phone', 'mobile_no', 'fax', 'website', 'lead_name'],
+ "doctype": "Lead",
+ "match_field": "email_id",
+ "personal_fields": ["phone", "mobile_no", "fax", "website", "lead_name"],
},
{
- 'doctype': 'Opportunity',
- 'match_field': 'contact_email',
- 'personal_fields': ['contact_mobile', 'contact_display', 'customer_name'],
- }
+ "doctype": "Opportunity",
+ "match_field": "contact_email",
+ "personal_fields": ["contact_mobile", "contact_display", "customer_name"],
+ },
]
# ERPNext doctypes for Global Search
@@ -541,107 +691,107 @@ global_search_doctypes = {
{"doctype": "Warranty Claim", "index": 47},
],
"Healthcare": [
- {'doctype': 'Patient', 'index': 1},
- {'doctype': 'Medical Department', 'index': 2},
- {'doctype': 'Vital Signs', 'index': 3},
- {'doctype': 'Healthcare Practitioner', 'index': 4},
- {'doctype': 'Patient Appointment', 'index': 5},
- {'doctype': 'Healthcare Service Unit', 'index': 6},
- {'doctype': 'Patient Encounter', 'index': 7},
- {'doctype': 'Antibiotic', 'index': 8},
- {'doctype': 'Diagnosis', 'index': 9},
- {'doctype': 'Lab Test', 'index': 10},
- {'doctype': 'Clinical Procedure', 'index': 11},
- {'doctype': 'Inpatient Record', 'index': 12},
- {'doctype': 'Sample Collection', 'index': 13},
- {'doctype': 'Patient Medical Record', 'index': 14},
- {'doctype': 'Appointment Type', 'index': 15},
- {'doctype': 'Fee Validity', 'index': 16},
- {'doctype': 'Practitioner Schedule', 'index': 17},
- {'doctype': 'Dosage Form', 'index': 18},
- {'doctype': 'Lab Test Sample', 'index': 19},
- {'doctype': 'Prescription Duration', 'index': 20},
- {'doctype': 'Prescription Dosage', 'index': 21},
- {'doctype': 'Sensitivity', 'index': 22},
- {'doctype': 'Complaint', 'index': 23},
- {'doctype': 'Medical Code', 'index': 24},
+ {"doctype": "Patient", "index": 1},
+ {"doctype": "Medical Department", "index": 2},
+ {"doctype": "Vital Signs", "index": 3},
+ {"doctype": "Healthcare Practitioner", "index": 4},
+ {"doctype": "Patient Appointment", "index": 5},
+ {"doctype": "Healthcare Service Unit", "index": 6},
+ {"doctype": "Patient Encounter", "index": 7},
+ {"doctype": "Antibiotic", "index": 8},
+ {"doctype": "Diagnosis", "index": 9},
+ {"doctype": "Lab Test", "index": 10},
+ {"doctype": "Clinical Procedure", "index": 11},
+ {"doctype": "Inpatient Record", "index": 12},
+ {"doctype": "Sample Collection", "index": 13},
+ {"doctype": "Patient Medical Record", "index": 14},
+ {"doctype": "Appointment Type", "index": 15},
+ {"doctype": "Fee Validity", "index": 16},
+ {"doctype": "Practitioner Schedule", "index": 17},
+ {"doctype": "Dosage Form", "index": 18},
+ {"doctype": "Lab Test Sample", "index": 19},
+ {"doctype": "Prescription Duration", "index": 20},
+ {"doctype": "Prescription Dosage", "index": 21},
+ {"doctype": "Sensitivity", "index": 22},
+ {"doctype": "Complaint", "index": 23},
+ {"doctype": "Medical Code", "index": 24},
],
"Education": [
- {'doctype': 'Article', 'index': 1},
- {'doctype': 'Video', 'index': 2},
- {'doctype': 'Topic', 'index': 3},
- {'doctype': 'Course', 'index': 4},
- {'doctype': 'Program', 'index': 5},
- {'doctype': 'Quiz', 'index': 6},
- {'doctype': 'Question', 'index': 7},
- {'doctype': 'Fee Schedule', 'index': 8},
- {'doctype': 'Fee Structure', 'index': 9},
- {'doctype': 'Fees', 'index': 10},
- {'doctype': 'Student Group', 'index': 11},
- {'doctype': 'Student', 'index': 12},
- {'doctype': 'Instructor', 'index': 13},
- {'doctype': 'Course Activity', 'index': 14},
- {'doctype': 'Quiz Activity', 'index': 15},
- {'doctype': 'Course Enrollment', 'index': 16},
- {'doctype': 'Program Enrollment', 'index': 17},
- {'doctype': 'Student Language', 'index': 18},
- {'doctype': 'Student Applicant', 'index': 19},
- {'doctype': 'Assessment Result', 'index': 20},
- {'doctype': 'Assessment Plan', 'index': 21},
- {'doctype': 'Grading Scale', 'index': 22},
- {'doctype': 'Guardian', 'index': 23},
- {'doctype': 'Student Leave Application', 'index': 24},
- {'doctype': 'Student Log', 'index': 25},
- {'doctype': 'Room', 'index': 26},
- {'doctype': 'Course Schedule', 'index': 27},
- {'doctype': 'Student Attendance', 'index': 28},
- {'doctype': 'Announcement', 'index': 29},
- {'doctype': 'Student Category', 'index': 30},
- {'doctype': 'Assessment Group', 'index': 31},
- {'doctype': 'Student Batch Name', 'index': 32},
- {'doctype': 'Assessment Criteria', 'index': 33},
- {'doctype': 'Academic Year', 'index': 34},
- {'doctype': 'Academic Term', 'index': 35},
- {'doctype': 'School House', 'index': 36},
- {'doctype': 'Student Admission', 'index': 37},
- {'doctype': 'Fee Category', 'index': 38},
- {'doctype': 'Assessment Code', 'index': 39},
- {'doctype': 'Discussion', 'index': 40},
+ {"doctype": "Article", "index": 1},
+ {"doctype": "Video", "index": 2},
+ {"doctype": "Topic", "index": 3},
+ {"doctype": "Course", "index": 4},
+ {"doctype": "Program", "index": 5},
+ {"doctype": "Quiz", "index": 6},
+ {"doctype": "Question", "index": 7},
+ {"doctype": "Fee Schedule", "index": 8},
+ {"doctype": "Fee Structure", "index": 9},
+ {"doctype": "Fees", "index": 10},
+ {"doctype": "Student Group", "index": 11},
+ {"doctype": "Student", "index": 12},
+ {"doctype": "Instructor", "index": 13},
+ {"doctype": "Course Activity", "index": 14},
+ {"doctype": "Quiz Activity", "index": 15},
+ {"doctype": "Course Enrollment", "index": 16},
+ {"doctype": "Program Enrollment", "index": 17},
+ {"doctype": "Student Language", "index": 18},
+ {"doctype": "Student Applicant", "index": 19},
+ {"doctype": "Assessment Result", "index": 20},
+ {"doctype": "Assessment Plan", "index": 21},
+ {"doctype": "Grading Scale", "index": 22},
+ {"doctype": "Guardian", "index": 23},
+ {"doctype": "Student Leave Application", "index": 24},
+ {"doctype": "Student Log", "index": 25},
+ {"doctype": "Room", "index": 26},
+ {"doctype": "Course Schedule", "index": 27},
+ {"doctype": "Student Attendance", "index": 28},
+ {"doctype": "Announcement", "index": 29},
+ {"doctype": "Student Category", "index": 30},
+ {"doctype": "Assessment Group", "index": 31},
+ {"doctype": "Student Batch Name", "index": 32},
+ {"doctype": "Assessment Criteria", "index": 33},
+ {"doctype": "Academic Year", "index": 34},
+ {"doctype": "Academic Term", "index": 35},
+ {"doctype": "School House", "index": 36},
+ {"doctype": "Student Admission", "index": 37},
+ {"doctype": "Fee Category", "index": 38},
+ {"doctype": "Assessment Code", "index": 39},
+ {"doctype": "Discussion", "index": 40},
],
"Agriculture": [
- {'doctype': 'Weather', 'index': 1},
- {'doctype': 'Soil Texture', 'index': 2},
- {'doctype': 'Water Analysis', 'index': 3},
- {'doctype': 'Soil Analysis', 'index': 4},
- {'doctype': 'Plant Analysis', 'index': 5},
- {'doctype': 'Agriculture Analysis Criteria', 'index': 6},
- {'doctype': 'Disease', 'index': 7},
- {'doctype': 'Crop', 'index': 8},
- {'doctype': 'Fertilizer', 'index': 9},
- {'doctype': 'Crop Cycle', 'index': 10}
+ {"doctype": "Weather", "index": 1},
+ {"doctype": "Soil Texture", "index": 2},
+ {"doctype": "Water Analysis", "index": 3},
+ {"doctype": "Soil Analysis", "index": 4},
+ {"doctype": "Plant Analysis", "index": 5},
+ {"doctype": "Agriculture Analysis Criteria", "index": 6},
+ {"doctype": "Disease", "index": 7},
+ {"doctype": "Crop", "index": 8},
+ {"doctype": "Fertilizer", "index": 9},
+ {"doctype": "Crop Cycle", "index": 10},
],
"Non Profit": [
- {'doctype': 'Certified Consultant', 'index': 1},
- {'doctype': 'Certification Application', 'index': 2},
- {'doctype': 'Volunteer', 'index': 3},
- {'doctype': 'Membership', 'index': 4},
- {'doctype': 'Member', 'index': 5},
- {'doctype': 'Donor', 'index': 6},
- {'doctype': 'Chapter', 'index': 7},
- {'doctype': 'Grant Application', 'index': 8},
- {'doctype': 'Volunteer Type', 'index': 9},
- {'doctype': 'Donor Type', 'index': 10},
- {'doctype': 'Membership Type', 'index': 11}
+ {"doctype": "Certified Consultant", "index": 1},
+ {"doctype": "Certification Application", "index": 2},
+ {"doctype": "Volunteer", "index": 3},
+ {"doctype": "Membership", "index": 4},
+ {"doctype": "Member", "index": 5},
+ {"doctype": "Donor", "index": 6},
+ {"doctype": "Chapter", "index": 7},
+ {"doctype": "Grant Application", "index": 8},
+ {"doctype": "Volunteer Type", "index": 9},
+ {"doctype": "Donor Type", "index": 10},
+ {"doctype": "Membership Type", "index": 11},
],
"Hospitality": [
- {'doctype': 'Hotel Room', 'index': 0},
- {'doctype': 'Hotel Room Reservation', 'index': 1},
- {'doctype': 'Hotel Room Pricing', 'index': 2},
- {'doctype': 'Hotel Room Package', 'index': 3},
- {'doctype': 'Hotel Room Type', 'index': 4}
- ]
+ {"doctype": "Hotel Room", "index": 0},
+ {"doctype": "Hotel Room Reservation", "index": 1},
+ {"doctype": "Hotel Room Pricing", "index": 2},
+ {"doctype": "Hotel Room Package", "index": 3},
+ {"doctype": "Hotel Room Type", "index": 4},
+ ],
}
additional_timeline_content = {
- '*': ['erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs']
+ "*": ["erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs"]
}
diff --git a/erpnext/hotels/doctype/hotel_room/hotel_room.py b/erpnext/hotels/doctype/hotel_room/hotel_room.py
index e4bd1c88462..b4f826f6487 100644
--- a/erpnext/hotels/doctype/hotel_room/hotel_room.py
+++ b/erpnext/hotels/doctype/hotel_room/hotel_room.py
@@ -9,5 +9,6 @@ from frappe.model.document import Document
class HotelRoom(Document):
def validate(self):
if not self.capacity:
- self.capacity, self.extra_bed_capacity = frappe.db.get_value('Hotel Room Type',
- self.hotel_room_type, ['capacity', 'extra_bed_capacity'])
+ self.capacity, self.extra_bed_capacity = frappe.db.get_value(
+ "Hotel Room Type", self.hotel_room_type, ["capacity", "extra_bed_capacity"]
+ )
diff --git a/erpnext/hotels/doctype/hotel_room/test_hotel_room.py b/erpnext/hotels/doctype/hotel_room/test_hotel_room.py
index 95efe2c6068..0fa211c40cc 100644
--- a/erpnext/hotels/doctype/hotel_room/test_hotel_room.py
+++ b/erpnext/hotels/doctype/hotel_room/test_hotel_room.py
@@ -5,19 +5,14 @@ import unittest
test_dependencies = ["Hotel Room Package"]
test_records = [
- dict(doctype="Hotel Room", name="1001",
- hotel_room_type="Basic Room"),
- dict(doctype="Hotel Room", name="1002",
- hotel_room_type="Basic Room"),
- dict(doctype="Hotel Room", name="1003",
- hotel_room_type="Basic Room"),
- dict(doctype="Hotel Room", name="1004",
- hotel_room_type="Basic Room"),
- dict(doctype="Hotel Room", name="1005",
- hotel_room_type="Basic Room"),
- dict(doctype="Hotel Room", name="1006",
- hotel_room_type="Basic Room")
+ dict(doctype="Hotel Room", name="1001", hotel_room_type="Basic Room"),
+ dict(doctype="Hotel Room", name="1002", hotel_room_type="Basic Room"),
+ dict(doctype="Hotel Room", name="1003", hotel_room_type="Basic Room"),
+ dict(doctype="Hotel Room", name="1004", hotel_room_type="Basic Room"),
+ dict(doctype="Hotel Room", name="1005", hotel_room_type="Basic Room"),
+ dict(doctype="Hotel Room", name="1006", hotel_room_type="Basic Room"),
]
+
class TestHotelRoom(unittest.TestCase):
pass
diff --git a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py b/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py
index aedc83a8468..160a1d368ad 100644
--- a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py
+++ b/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py
@@ -9,12 +9,10 @@ from frappe.model.document import Document
class HotelRoomPackage(Document):
def validate(self):
if not self.item:
- item = frappe.get_doc(dict(
- doctype = 'Item',
- item_code = self.name,
- item_group = 'Products',
- is_stock_item = 0,
- stock_uom = 'Unit'
- ))
+ item = frappe.get_doc(
+ dict(
+ doctype="Item", item_code=self.name, item_group="Products", is_stock_item=0, stock_uom="Unit"
+ )
+ )
item.insert()
self.item = item.name
diff --git a/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py b/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py
index 749731f4918..06f992106f3 100644
--- a/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py
+++ b/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py
@@ -4,44 +4,44 @@
import unittest
test_records = [
- dict(doctype='Item', item_code='Breakfast',
- item_group='Products', is_stock_item=0),
- dict(doctype='Item', item_code='Lunch',
- item_group='Products', is_stock_item=0),
- dict(doctype='Item', item_code='Dinner',
- item_group='Products', is_stock_item=0),
- dict(doctype='Item', item_code='WiFi',
- item_group='Products', is_stock_item=0),
- dict(doctype='Hotel Room Type', name="Delux Room",
+ dict(doctype="Item", item_code="Breakfast", item_group="Products", is_stock_item=0),
+ dict(doctype="Item", item_code="Lunch", item_group="Products", is_stock_item=0),
+ dict(doctype="Item", item_code="Dinner", item_group="Products", is_stock_item=0),
+ dict(doctype="Item", item_code="WiFi", item_group="Products", is_stock_item=0),
+ dict(
+ doctype="Hotel Room Type",
+ name="Delux Room",
capacity=4,
extra_bed_capacity=2,
- amenities = [
- dict(item='WiFi', billable=0)
- ]),
- dict(doctype='Hotel Room Type', name="Basic Room",
+ amenities=[dict(item="WiFi", billable=0)],
+ ),
+ dict(
+ doctype="Hotel Room Type",
+ name="Basic Room",
capacity=4,
extra_bed_capacity=2,
- amenities = [
- dict(item='Breakfast', billable=0)
- ]),
- dict(doctype="Hotel Room Package", name="Basic Room with Breakfast",
+ amenities=[dict(item="Breakfast", billable=0)],
+ ),
+ dict(
+ doctype="Hotel Room Package",
+ name="Basic Room with Breakfast",
hotel_room_type="Basic Room",
- amenities = [
- dict(item="Breakfast", billable=0)
- ]),
- dict(doctype="Hotel Room Package", name="Basic Room with Lunch",
+ amenities=[dict(item="Breakfast", billable=0)],
+ ),
+ dict(
+ doctype="Hotel Room Package",
+ name="Basic Room with Lunch",
hotel_room_type="Basic Room",
- amenities = [
- dict(item="Breakfast", billable=0),
- dict(item="Lunch", billable=0)
- ]),
- dict(doctype="Hotel Room Package", name="Basic Room with Dinner",
+ amenities=[dict(item="Breakfast", billable=0), dict(item="Lunch", billable=0)],
+ ),
+ dict(
+ doctype="Hotel Room Package",
+ name="Basic Room with Dinner",
hotel_room_type="Basic Room",
- amenities = [
- dict(item="Breakfast", billable=0),
- dict(item="Dinner", billable=0)
- ])
+ amenities=[dict(item="Breakfast", billable=0), dict(item="Dinner", billable=0)],
+ ),
]
+
class TestHotelRoomPackage(unittest.TestCase):
pass
diff --git a/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py b/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py
index 34550096dd9..15752b5b1af 100644
--- a/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py
+++ b/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py
@@ -5,15 +5,20 @@ import unittest
test_dependencies = ["Hotel Room Package"]
test_records = [
- dict(doctype="Hotel Room Pricing", enabled=1,
+ dict(
+ doctype="Hotel Room Pricing",
+ enabled=1,
name="Winter 2017",
- from_date="2017-01-01", to_date="2017-01-10",
- items = [
+ from_date="2017-01-01",
+ to_date="2017-01-10",
+ items=[
dict(item="Basic Room with Breakfast", rate=10000),
dict(item="Basic Room with Lunch", rate=11000),
- dict(item="Basic Room with Dinner", rate=12000)
- ])
+ dict(item="Basic Room with Dinner", rate=12000),
+ ],
+ )
]
+
class TestHotelRoomPricing(unittest.TestCase):
pass
diff --git a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py b/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py
index 7725955396b..eaad6898a09 100644
--- a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py
+++ b/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py
@@ -10,8 +10,13 @@ from frappe.model.document import Document
from frappe.utils import add_days, date_diff, flt
-class HotelRoomUnavailableError(frappe.ValidationError): pass
-class HotelRoomPricingNotSetError(frappe.ValidationError): pass
+class HotelRoomUnavailableError(frappe.ValidationError):
+ pass
+
+
+class HotelRoomPricingNotSetError(frappe.ValidationError):
+ pass
+
class HotelRoomReservation(Document):
def validate(self):
@@ -28,27 +33,39 @@ class HotelRoomReservation(Document):
if not d.item in self.rooms_booked:
self.rooms_booked[d.item] = 0
- room_type = frappe.db.get_value("Hotel Room Package",
- d.item, 'hotel_room_type')
- rooms_booked = get_rooms_booked(room_type, day, exclude_reservation=self.name) \
- + d.qty + self.rooms_booked.get(d.item)
+ room_type = frappe.db.get_value("Hotel Room Package", d.item, "hotel_room_type")
+ rooms_booked = (
+ get_rooms_booked(room_type, day, exclude_reservation=self.name)
+ + d.qty
+ + self.rooms_booked.get(d.item)
+ )
total_rooms = self.get_total_rooms(d.item)
if total_rooms < rooms_booked:
- frappe.throw(_("Hotel Rooms of type {0} are unavailable on {1}").format(d.item,
- frappe.format(day, dict(fieldtype="Date"))), exc=HotelRoomUnavailableError)
+ frappe.throw(
+ _("Hotel Rooms of type {0} are unavailable on {1}").format(
+ d.item, frappe.format(day, dict(fieldtype="Date"))
+ ),
+ exc=HotelRoomUnavailableError,
+ )
self.rooms_booked[d.item] += rooms_booked
def get_total_rooms(self, item):
if not item in self.total_rooms:
- self.total_rooms[item] = frappe.db.sql("""
+ self.total_rooms[item] = (
+ frappe.db.sql(
+ """
select count(*)
from
`tabHotel Room Package` package
inner join
`tabHotel Room` room on package.hotel_room_type = room.hotel_room_type
where
- package.item = %s""", item)[0][0] or 0
+ package.item = %s""",
+ item,
+ )[0][0]
+ or 0
+ )
return self.total_rooms[item]
@@ -60,7 +77,8 @@ class HotelRoomReservation(Document):
day = add_days(self.from_date, i)
if not d.item:
continue
- day_rate = frappe.db.sql("""
+ day_rate = frappe.db.sql(
+ """
select
item.rate
from
@@ -70,18 +88,22 @@ class HotelRoomReservation(Document):
item.parent = pricing.name
and item.item = %s
and %s between pricing.from_date
- and pricing.to_date""", (d.item, day))
+ and pricing.to_date""",
+ (d.item, day),
+ )
if day_rate:
net_rate += day_rate[0][0]
else:
frappe.throw(
- _("Please set Hotel Room Rate on {}").format(
- frappe.format(day, dict(fieldtype="Date"))), exc=HotelRoomPricingNotSetError)
+ _("Please set Hotel Room Rate on {}").format(frappe.format(day, dict(fieldtype="Date"))),
+ exc=HotelRoomPricingNotSetError,
+ )
d.rate = net_rate
d.amount = net_rate * flt(d.qty)
self.net_total += d.amount
+
@frappe.whitelist()
def get_room_rate(hotel_room_reservation):
"""Calculate rate for each day as it may belong to different Hotel Room Pricing Item"""
@@ -89,12 +111,15 @@ def get_room_rate(hotel_room_reservation):
doc.set_rates()
return doc.as_dict()
-def get_rooms_booked(room_type, day, exclude_reservation=None):
- exclude_condition = ''
- if exclude_reservation:
- exclude_condition = 'and reservation.name != {0}'.format(frappe.db.escape(exclude_reservation))
- return frappe.db.sql("""
+def get_rooms_booked(room_type, day, exclude_reservation=None):
+ exclude_condition = ""
+ if exclude_reservation:
+ exclude_condition = "and reservation.name != {0}".format(frappe.db.escape(exclude_reservation))
+
+ return (
+ frappe.db.sql(
+ """
select sum(item.qty)
from
`tabHotel Room Package` room_package,
@@ -107,5 +132,10 @@ def get_rooms_booked(room_type, day, exclude_reservation=None):
and reservation.docstatus = 1
{exclude_condition}
and %s between reservation.from_date
- and reservation.to_date""".format(exclude_condition=exclude_condition),
- (room_type, day))[0][0] or 0
+ and reservation.to_date""".format(
+ exclude_condition=exclude_condition
+ ),
+ (room_type, day),
+ )[0][0]
+ or 0
+ )
diff --git a/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py b/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py
index bb32a27fa7c..52e7d69f79f 100644
--- a/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py
+++ b/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py
@@ -12,6 +12,7 @@ from erpnext.hotels.doctype.hotel_room_reservation.hotel_room_reservation import
test_dependencies = ["Hotel Room Package", "Hotel Room Pricing", "Hotel Room"]
+
class TestHotelRoomReservation(unittest.TestCase):
def setUp(self):
frappe.db.sql("delete from `tabHotel Room Reservation`")
@@ -19,22 +20,14 @@ class TestHotelRoomReservation(unittest.TestCase):
def test_reservation(self):
reservation = make_reservation(
- from_date="2017-01-01",
- to_date="2017-01-03",
- items=[
- dict(item="Basic Room with Dinner", qty=2)
- ]
+ from_date="2017-01-01", to_date="2017-01-03", items=[dict(item="Basic Room with Dinner", qty=2)]
)
reservation.insert()
self.assertEqual(reservation.net_total, 48000)
def test_price_not_set(self):
reservation = make_reservation(
- from_date="2016-01-01",
- to_date="2016-01-03",
- items=[
- dict(item="Basic Room with Dinner", qty=2)
- ]
+ from_date="2016-01-01", to_date="2016-01-03", items=[dict(item="Basic Room with Dinner", qty=2)]
)
self.assertRaises(HotelRoomPricingNotSetError, reservation.insert)
@@ -44,7 +37,7 @@ class TestHotelRoomReservation(unittest.TestCase):
to_date="2017-01-03",
items=[
dict(item="Basic Room with Dinner", qty=2),
- ]
+ ],
)
reservation.insert()
@@ -53,10 +46,11 @@ class TestHotelRoomReservation(unittest.TestCase):
to_date="2017-01-03",
items=[
dict(item="Basic Room with Dinner", qty=20),
- ]
+ ],
)
self.assertRaises(HotelRoomUnavailableError, reservation.insert)
+
def make_reservation(**kwargs):
kwargs["doctype"] = "Hotel Room Reservation"
if not "guest_name" in kwargs:
diff --git a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py b/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py
index c43589d2a8d..ada5332bce0 100644
--- a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py
+++ b/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py
@@ -14,16 +14,18 @@ def execute(filters=None):
data = get_data(filters)
return columns, data
+
def get_columns(filters):
columns = [
dict(label=_("Room Type"), fieldname="room_type"),
- dict(label=_("Rooms Booked"), fieldtype="Int")
+ dict(label=_("Rooms Booked"), fieldtype="Int"),
]
return columns
+
def get_data(filters):
out = []
- for room_type in frappe.get_all('Hotel Room Type'):
+ for room_type in frappe.get_all("Hotel Room Type"):
total_booked = 0
for i in range(date_diff(filters.to_date, filters.from_date)):
day = add_days(filters.from_date, i)
diff --git a/erpnext/hr/doctype/appointment_letter/appointment_letter.py b/erpnext/hr/doctype/appointment_letter/appointment_letter.py
index 71327bf1b01..a58589af210 100644
--- a/erpnext/hr/doctype/appointment_letter/appointment_letter.py
+++ b/erpnext/hr/doctype/appointment_letter/appointment_letter.py
@@ -9,18 +9,21 @@ from frappe.model.document import Document
class AppointmentLetter(Document):
pass
+
@frappe.whitelist()
def get_appointment_letter_details(template):
body = []
- intro = frappe.get_list('Appointment Letter Template',
- fields=['introduction', 'closing_notes'],
- filters={'name': template}
+ intro = frappe.get_list(
+ "Appointment Letter Template",
+ fields=["introduction", "closing_notes"],
+ filters={"name": template},
)[0]
- content = frappe.get_all('Appointment Letter content',
- fields=['title', 'description'],
- filters={'parent': template},
- order_by='idx'
+ content = frappe.get_all(
+ "Appointment Letter content",
+ fields=["title", "description"],
+ filters={"parent": template},
+ order_by="idx",
)
body.append(intro)
- body.append({'description': content})
+ body.append({"description": content})
return body
diff --git a/erpnext/hr/doctype/appraisal/appraisal.py b/erpnext/hr/doctype/appraisal/appraisal.py
index 83273f86544..382c643abae 100644
--- a/erpnext/hr/doctype/appraisal/appraisal.py
+++ b/erpnext/hr/doctype/appraisal/appraisal.py
@@ -34,46 +34,61 @@ class Appraisal(Document):
frappe.throw(_("End Date can not be less than Start Date"))
def validate_existing_appraisal(self):
- chk = frappe.db.sql("""select name from `tabAppraisal` where employee=%s
+ chk = frappe.db.sql(
+ """select name from `tabAppraisal` where employee=%s
and (status='Submitted' or status='Completed')
and ((start_date>=%s and start_date<=%s)
or (end_date>=%s and end_date<=%s))""",
- (self.employee,self.start_date,self.end_date,self.start_date,self.end_date))
+ (self.employee, self.start_date, self.end_date, self.start_date, self.end_date),
+ )
if chk:
- frappe.throw(_("Appraisal {0} created for Employee {1} in the given date range").format(chk[0][0], self.employee_name))
+ frappe.throw(
+ _("Appraisal {0} created for Employee {1} in the given date range").format(
+ chk[0][0], self.employee_name
+ )
+ )
def calculate_total(self):
- total, total_w = 0, 0
- for d in self.get('goals'):
+ total, total_w = 0, 0
+ for d in self.get("goals"):
if d.score:
d.score_earned = flt(d.score) * flt(d.per_weightage) / 100
total = total + d.score_earned
total_w += flt(d.per_weightage)
if int(total_w) != 100:
- frappe.throw(_("Total weightage assigned should be 100%. It is {0}").format(str(total_w) + "%"))
+ frappe.throw(
+ _("Total weightage assigned should be 100%. It is {0}").format(str(total_w) + "%")
+ )
- if frappe.db.get_value("Employee", self.employee, "user_id") != \
- frappe.session.user and total == 0:
+ if (
+ frappe.db.get_value("Employee", self.employee, "user_id") != frappe.session.user and total == 0
+ ):
frappe.throw(_("Total cannot be zero"))
self.total_score = total
def on_submit(self):
- frappe.db.set(self, 'status', 'Submitted')
+ frappe.db.set(self, "status", "Submitted")
def on_cancel(self):
- frappe.db.set(self, 'status', 'Cancelled')
+ frappe.db.set(self, "status", "Cancelled")
+
@frappe.whitelist()
def fetch_appraisal_template(source_name, target_doc=None):
- target_doc = get_mapped_doc("Appraisal Template", source_name, {
- "Appraisal Template": {
- "doctype": "Appraisal",
+ target_doc = get_mapped_doc(
+ "Appraisal Template",
+ source_name,
+ {
+ "Appraisal Template": {
+ "doctype": "Appraisal",
+ },
+ "Appraisal Template Goal": {
+ "doctype": "Appraisal Goal",
+ },
},
- "Appraisal Template Goal": {
- "doctype": "Appraisal Goal",
- }
- }, target_doc)
+ target_doc,
+ )
return target_doc
diff --git a/erpnext/hr/doctype/appraisal/test_appraisal.py b/erpnext/hr/doctype/appraisal/test_appraisal.py
index 90c30ef3475..13a39f38200 100644
--- a/erpnext/hr/doctype/appraisal/test_appraisal.py
+++ b/erpnext/hr/doctype/appraisal/test_appraisal.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Appraisal')
+
class TestAppraisal(unittest.TestCase):
pass
diff --git a/erpnext/hr/doctype/appraisal_template/appraisal_template_dashboard.py b/erpnext/hr/doctype/appraisal_template/appraisal_template_dashboard.py
index f52e2b027ca..476de4f51b4 100644
--- a/erpnext/hr/doctype/appraisal_template/appraisal_template_dashboard.py
+++ b/erpnext/hr/doctype/appraisal_template/appraisal_template_dashboard.py
@@ -1,11 +1,7 @@
-
-
def get_data():
- return {
- 'fieldname': 'kra_template',
- 'transactions': [
- {
- 'items': ['Appraisal']
- },
- ],
- }
+ return {
+ "fieldname": "kra_template",
+ "transactions": [
+ {"items": ["Appraisal"]},
+ ],
+ }
diff --git a/erpnext/hr/doctype/appraisal_template/test_appraisal_template.py b/erpnext/hr/doctype/appraisal_template/test_appraisal_template.py
index d0e81a7dc5b..560e992e8a1 100644
--- a/erpnext/hr/doctype/appraisal_template/test_appraisal_template.py
+++ b/erpnext/hr/doctype/appraisal_template/test_appraisal_template.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Appraisal Template')
+
class TestAppraisalTemplate(unittest.TestCase):
pass
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index b1eaaf8b587..7f4bd836854 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -13,6 +13,7 @@ from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_emp
class Attendance(Document):
def validate(self):
from erpnext.controllers.status_updater import validate_status
+
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
validate_active_employee(self.employee)
self.validate_attendance_date()
@@ -24,62 +25,84 @@ class Attendance(Document):
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
# leaves can be marked for future dates
- if self.status != 'On Leave' and not self.leave_application and getdate(self.attendance_date) > getdate(nowdate()):
+ if (
+ self.status != "On Leave"
+ and not self.leave_application
+ and getdate(self.attendance_date) > getdate(nowdate())
+ ):
frappe.throw(_("Attendance can not be marked for future dates"))
elif date_of_joining and getdate(self.attendance_date) < getdate(date_of_joining):
frappe.throw(_("Attendance date can not be less than employee's joining date"))
def validate_duplicate_record(self):
- res = frappe.db.sql("""
+ res = frappe.db.sql(
+ """
select name from `tabAttendance`
where employee = %s
and attendance_date = %s
and name != %s
and docstatus != 2
- """, (self.employee, getdate(self.attendance_date), self.name))
+ """,
+ (self.employee, getdate(self.attendance_date), self.name),
+ )
if res:
- frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format(
- frappe.bold(self.employee), frappe.bold(self.attendance_date)))
+ frappe.throw(
+ _("Attendance for employee {0} is already marked for the date {1}").format(
+ frappe.bold(self.employee), frappe.bold(self.attendance_date)
+ )
+ )
def validate_employee_status(self):
if frappe.db.get_value("Employee", self.employee, "status") == "Inactive":
frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee))
def check_leave_record(self):
- leave_record = frappe.db.sql("""
+ leave_record = frappe.db.sql(
+ """
select leave_type, half_day, half_day_date
from `tabLeave Application`
where employee = %s
and %s between from_date and to_date
and status = 'Approved'
and docstatus = 1
- """, (self.employee, self.attendance_date), as_dict=True)
+ """,
+ (self.employee, self.attendance_date),
+ as_dict=True,
+ )
if leave_record:
for d in leave_record:
self.leave_type = d.leave_type
if d.half_day_date == getdate(self.attendance_date):
- self.status = 'Half Day'
- frappe.msgprint(_("Employee {0} on Half day on {1}")
- .format(self.employee, formatdate(self.attendance_date)))
+ self.status = "Half Day"
+ frappe.msgprint(
+ _("Employee {0} on Half day on {1}").format(self.employee, formatdate(self.attendance_date))
+ )
else:
- self.status = 'On Leave'
- frappe.msgprint(_("Employee {0} is on Leave on {1}")
- .format(self.employee, formatdate(self.attendance_date)))
+ self.status = "On Leave"
+ frappe.msgprint(
+ _("Employee {0} is on Leave on {1}").format(self.employee, formatdate(self.attendance_date))
+ )
if self.status in ("On Leave", "Half Day"):
if not leave_record:
- frappe.msgprint(_("No leave record found for employee {0} on {1}")
- .format(self.employee, formatdate(self.attendance_date)), alert=1)
+ frappe.msgprint(
+ _("No leave record found for employee {0} on {1}").format(
+ self.employee, formatdate(self.attendance_date)
+ ),
+ alert=1,
+ )
elif self.leave_type:
self.leave_type = None
self.leave_application = None
def validate_employee(self):
- emp = frappe.db.sql("select name from `tabEmployee` where name = %s and status = 'Active'",
- self.employee)
+ emp = frappe.db.sql(
+ "select name from `tabEmployee` where name = %s and status = 'Active'", self.employee
+ )
if not emp:
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
+
@frappe.whitelist()
def get_events(start, end, filters=None):
events = []
@@ -90,10 +113,12 @@ def get_events(start, end, filters=None):
return events
from frappe.desk.reportview import get_filters_cond
+
conditions = get_filters_cond("Attendance", filters, [])
add_attendance(events, start, end, conditions=conditions)
return events
+
def add_attendance(events, start, end, conditions=None):
query = """select name, attendance_date, status
from `tabAttendance` where
@@ -102,93 +127,121 @@ def add_attendance(events, start, end, conditions=None):
if conditions:
query += conditions
- for d in frappe.db.sql(query, {"from_date":start, "to_date":end}, as_dict=True):
+ for d in frappe.db.sql(query, {"from_date": start, "to_date": end}, as_dict=True):
e = {
"name": d.name,
"doctype": "Attendance",
"start": d.attendance_date,
"end": d.attendance_date,
"title": cstr(d.status),
- "docstatus": d.docstatus
+ "docstatus": d.docstatus,
}
if e not in events:
events.append(e)
-def mark_attendance(employee, attendance_date, status, shift=None, leave_type=None, ignore_validate=False):
- if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}):
- company = frappe.db.get_value('Employee', employee, 'company')
- attendance = frappe.get_doc({
- 'doctype': 'Attendance',
- 'employee': employee,
- 'attendance_date': attendance_date,
- 'status': status,
- 'company': company,
- 'shift': shift,
- 'leave_type': leave_type
- })
+
+def mark_attendance(
+ employee, attendance_date, status, shift=None, leave_type=None, ignore_validate=False
+):
+ if not frappe.db.exists(
+ "Attendance",
+ {"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")},
+ ):
+ company = frappe.db.get_value("Employee", employee, "company")
+ attendance = frappe.get_doc(
+ {
+ "doctype": "Attendance",
+ "employee": employee,
+ "attendance_date": attendance_date,
+ "status": status,
+ "company": company,
+ "shift": shift,
+ "leave_type": leave_type,
+ }
+ )
attendance.flags.ignore_validate = ignore_validate
attendance.insert()
attendance.submit()
return attendance.name
+
@frappe.whitelist()
def mark_bulk_attendance(data):
import json
+
if isinstance(data, str):
data = json.loads(data)
data = frappe._dict(data)
- company = frappe.get_value('Employee', data.employee, 'company')
+ company = frappe.get_value("Employee", data.employee, "company")
if not data.unmarked_days:
frappe.throw(_("Please select a date."))
return
for date in data.unmarked_days:
doc_dict = {
- 'doctype': 'Attendance',
- 'employee': data.employee,
- 'attendance_date': get_datetime(date),
- 'status': data.status,
- 'company': company,
+ "doctype": "Attendance",
+ "employee": data.employee,
+ "attendance_date": get_datetime(date),
+ "status": data.status,
+ "company": company,
}
attendance = frappe.get_doc(doc_dict).insert()
attendance.submit()
def get_month_map():
- return frappe._dict({
- "January": 1,
- "February": 2,
- "March": 3,
- "April": 4,
- "May": 5,
- "June": 6,
- "July": 7,
- "August": 8,
- "September": 9,
- "October": 10,
- "November": 11,
- "December": 12
- })
+ return frappe._dict(
+ {
+ "January": 1,
+ "February": 2,
+ "March": 3,
+ "April": 4,
+ "May": 5,
+ "June": 6,
+ "July": 7,
+ "August": 8,
+ "September": 9,
+ "October": 10,
+ "November": 11,
+ "December": 12,
+ }
+ )
+
@frappe.whitelist()
def get_unmarked_days(employee, month, exclude_holidays=0):
import calendar
- month_map = get_month_map()
+ month_map = get_month_map()
today = get_datetime()
- dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(1, calendar.monthrange(today.year, month_map[month])[1] + 1)]
+ joining_date, relieving_date = frappe.get_cached_value(
+ "Employee", employee, ["date_of_joining", "relieving_date"]
+ )
+ start_day = 1
+ end_day = calendar.monthrange(today.year, month_map[month])[1] + 1
- length = len(dates_of_month)
- month_start, month_end = dates_of_month[0], dates_of_month[length-1]
+ if joining_date and joining_date.month == month_map[month]:
+ start_day = joining_date.day
+ if relieving_date and relieving_date.month == month_map[month]:
+ end_day = relieving_date.day + 1
- records = frappe.get_all("Attendance", fields = ['attendance_date', 'employee'] , filters = [
- ["attendance_date", ">=", month_start],
- ["attendance_date", "<=", month_end],
- ["employee", "=", employee],
- ["docstatus", "!=", 2]
- ])
+ dates_of_month = [
+ "{}-{}-{}".format(today.year, month_map[month], r) for r in range(start_day, end_day)
+ ]
+ month_start, month_end = dates_of_month[0], dates_of_month[-1]
+
+ records = frappe.get_all(
+ "Attendance",
+ fields=["attendance_date", "employee"],
+ filters=[
+ ["attendance_date", ">=", month_start],
+ ["attendance_date", "<=", month_end],
+ ["employee", "=", employee],
+ ["docstatus", "!=", 2],
+ ],
+ )
marked_days = [get_datetime(record.attendance_date) for record in records]
if cint(exclude_holidays):
@@ -200,7 +253,7 @@ def get_unmarked_days(employee, month, exclude_holidays=0):
for date in dates_of_month:
date_time = get_datetime(date)
- if today.day == date_time.day and today.month == date_time.month:
+ if today.day <= date_time.day and today.month <= date_time.month:
break
if date_time not in marked_days:
unmarked_days.append(date)
diff --git a/erpnext/hr/doctype/attendance/attendance_dashboard.py b/erpnext/hr/doctype/attendance/attendance_dashboard.py
index f466534d2c7..abed78fbbbe 100644
--- a/erpnext/hr/doctype/attendance/attendance_dashboard.py
+++ b/erpnext/hr/doctype/attendance/attendance_dashboard.py
@@ -1,12 +1,2 @@
-
-
def get_data():
- return {
- 'fieldname': 'attendance',
- 'transactions': [
- {
- 'label': '',
- 'items': ['Employee Checkin']
- }
- ]
- }
+ return {"fieldname": "attendance", "transactions": [{"label": "", "items": ["Employee Checkin"]}]}
diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py
index a770d70ffa9..058bc93d72a 100644
--- a/erpnext/hr/doctype/attendance/test_attendance.py
+++ b/erpnext/hr/doctype/attendance/test_attendance.py
@@ -1,20 +1,124 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt
-import unittest
-
import frappe
-from frappe.utils import nowdate
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, get_year_ending, get_year_start, getdate, now_datetime, nowdate
-test_records = frappe.get_test_records('Attendance')
+from erpnext.hr.doctype.attendance.attendance import (
+ get_month_map,
+ get_unmarked_days,
+ mark_attendance,
+)
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
+
+test_records = frappe.get_test_records("Attendance")
+
+
+class TestAttendance(FrappeTestCase):
+ def setUp(self):
+ from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
+
+ from_date = get_year_start(getdate())
+ to_date = get_year_ending(getdate())
+ self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
-class TestAttendance(unittest.TestCase):
def test_mark_absent(self):
- from erpnext.hr.doctype.employee.test_employee import make_employee
employee = make_employee("test_mark_absent@example.com")
date = nowdate()
- frappe.db.delete('Attendance', {'employee':employee, 'attendance_date':date})
- from erpnext.hr.doctype.attendance.attendance import mark_attendance
- attendance = mark_attendance(employee, date, 'Absent')
- fetch_attendance = frappe.get_value('Attendance', {'employee':employee, 'attendance_date':date, 'status':'Absent'})
+ frappe.db.delete("Attendance", {"employee": employee, "attendance_date": date})
+ attendance = mark_attendance(employee, date, "Absent")
+ fetch_attendance = frappe.get_value(
+ "Attendance", {"employee": employee, "attendance_date": date, "status": "Absent"}
+ )
self.assertEqual(attendance, fetch_attendance)
+
+ def test_unmarked_days(self):
+ now = now_datetime()
+ previous_month = now.month - 1
+ first_day = now.replace(day=1).replace(month=previous_month).date()
+
+ employee = make_employee(
+ "test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
+ )
+ frappe.db.delete("Attendance", {"employee": employee})
+ frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
+
+ first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
+ mark_attendance(employee, first_day, "Present")
+ month_name = get_month_name(first_day)
+
+ unmarked_days = get_unmarked_days(employee, month_name)
+ unmarked_days = [getdate(date) for date in unmarked_days]
+
+ # attendance already marked for the day
+ self.assertNotIn(first_day, unmarked_days)
+ # attendance unmarked
+ self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
+ # holiday considered in unmarked days
+ self.assertIn(first_sunday, unmarked_days)
+
+ def test_unmarked_days_excluding_holidays(self):
+ now = now_datetime()
+ previous_month = now.month - 1
+ first_day = now.replace(day=1).replace(month=previous_month).date()
+
+ employee = make_employee(
+ "test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
+ )
+ frappe.db.delete("Attendance", {"employee": employee})
+
+ frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
+
+ first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
+ mark_attendance(employee, first_day, "Present")
+ month_name = get_month_name(first_day)
+
+ unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
+ unmarked_days = [getdate(date) for date in unmarked_days]
+
+ # attendance already marked for the day
+ self.assertNotIn(first_day, unmarked_days)
+ # attendance unmarked
+ self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
+ # holidays not considered in unmarked days
+ self.assertNotIn(first_sunday, unmarked_days)
+
+ def test_unmarked_days_as_per_joining_and_relieving_dates(self):
+ now = now_datetime()
+ previous_month = now.month - 1
+ first_day = now.replace(day=1).replace(month=previous_month).date()
+
+ doj = add_days(first_day, 1)
+ relieving_date = add_days(first_day, 5)
+ employee = make_employee(
+ "test_unmarked_days_as_per_doj@example.com", date_of_joining=doj, relieving_date=relieving_date
+ )
+ frappe.db.delete("Attendance", {"employee": employee})
+
+ frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
+
+ attendance_date = add_days(first_day, 2)
+ mark_attendance(employee, attendance_date, "Present")
+ month_name = get_month_name(first_day)
+
+ unmarked_days = get_unmarked_days(employee, month_name)
+ unmarked_days = [getdate(date) for date in unmarked_days]
+
+ # attendance already marked for the day
+ self.assertNotIn(attendance_date, unmarked_days)
+ # date before doj not in unmarked days
+ self.assertNotIn(add_days(doj, -1), unmarked_days)
+ # date after relieving not in unmarked days
+ self.assertNotIn(add_days(relieving_date, 1), unmarked_days)
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+
+def get_month_name(date):
+ month_number = date.month
+ for month, number in get_month_map().items():
+ if number == month_number:
+ return month
diff --git a/erpnext/hr/doctype/attendance_request/attendance_request.py b/erpnext/hr/doctype/attendance_request/attendance_request.py
index 8fbe7c7a9ad..78652f669d7 100644
--- a/erpnext/hr/doctype/attendance_request/attendance_request.py
+++ b/erpnext/hr/doctype/attendance_request/attendance_request.py
@@ -16,17 +16,19 @@ class AttendanceRequest(Document):
validate_active_employee(self.employee)
validate_dates(self, self.from_date, self.to_date)
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):
frappe.throw(_("Half day date should be in between from date and to date"))
def on_submit(self):
self.create_attendance()
def on_cancel(self):
- attendance_list = frappe.get_list("Attendance", {'employee': self.employee, 'attendance_request': self.name})
+ attendance_list = frappe.get_list(
+ "Attendance", {"employee": self.employee, "attendance_request": self.name}
+ )
if attendance_list:
for attendance in attendance_list:
- attendance_obj = frappe.get_doc("Attendance", attendance['name'])
+ attendance_obj = frappe.get_doc("Attendance", attendance["name"])
attendance_obj.cancel()
def create_attendance(self):
@@ -53,15 +55,24 @@ class AttendanceRequest(Document):
def validate_if_attendance_not_applicable(self, attendance_date):
# Check if attendance_date is a Holiday
if is_holiday(self.employee, attendance_date):
- frappe.msgprint(_("Attendance not submitted for {0} as it is a Holiday.").format(attendance_date), alert=1)
+ frappe.msgprint(
+ _("Attendance not submitted for {0} as it is a Holiday.").format(attendance_date), alert=1
+ )
return True
# Check if employee on Leave
- leave_record = frappe.db.sql("""select half_day from `tabLeave Application`
+ leave_record = frappe.db.sql(
+ """select half_day from `tabLeave Application`
where employee = %s and %s between from_date and to_date
- and docstatus = 1""", (self.employee, attendance_date), as_dict=True)
+ and docstatus = 1""",
+ (self.employee, attendance_date),
+ as_dict=True,
+ )
if leave_record:
- frappe.msgprint(_("Attendance not submitted for {0} as {1} on leave.").format(attendance_date, self.employee), alert=1)
+ frappe.msgprint(
+ _("Attendance not submitted for {0} as {1} on leave.").format(attendance_date, self.employee),
+ alert=1,
+ )
return True
return False
diff --git a/erpnext/hr/doctype/attendance_request/attendance_request_dashboard.py b/erpnext/hr/doctype/attendance_request/attendance_request_dashboard.py
index b23e0fd93a2..059725cb44a 100644
--- a/erpnext/hr/doctype/attendance_request/attendance_request_dashboard.py
+++ b/erpnext/hr/doctype/attendance_request/attendance_request_dashboard.py
@@ -1,11 +1,2 @@
-
-
def get_data():
- return {
- 'fieldname': 'attendance_request',
- 'transactions': [
- {
- 'items': ['Attendance']
- }
- ]
- }
+ return {"fieldname": "attendance_request", "transactions": [{"items": ["Attendance"]}]}
diff --git a/erpnext/hr/doctype/attendance_request/test_attendance_request.py b/erpnext/hr/doctype/attendance_request/test_attendance_request.py
index 3f0442c7d69..ee436f50687 100644
--- a/erpnext/hr/doctype/attendance_request/test_attendance_request.py
+++ b/erpnext/hr/doctype/attendance_request/test_attendance_request.py
@@ -9,6 +9,7 @@ from frappe.utils import nowdate
test_dependencies = ["Employee"]
+
class TestAttendanceRequest(unittest.TestCase):
def setUp(self):
for doctype in ["Attendance Request", "Attendance"]:
@@ -34,10 +35,10 @@ class TestAttendanceRequest(unittest.TestCase):
"Attendance",
filters={
"attendance_request": attendance_request.name,
- "attendance_date": date(date.today().year, 1, 1)
+ "attendance_date": date(date.today().year, 1, 1),
},
fieldname=["status", "docstatus"],
- as_dict=True
+ as_dict=True,
)
self.assertEqual(attendance.status, "Present")
self.assertEqual(attendance.docstatus, 1)
@@ -51,9 +52,9 @@ class TestAttendanceRequest(unittest.TestCase):
"Attendance",
filters={
"attendance_request": attendance_request.name,
- "attendance_date": date(date.today().year, 1, 1)
+ "attendance_date": date(date.today().year, 1, 1),
},
- fieldname="docstatus"
+ fieldname="docstatus",
)
self.assertEqual(attendance_docstatus, 2)
@@ -74,11 +75,11 @@ class TestAttendanceRequest(unittest.TestCase):
"Attendance",
filters={
"attendance_request": attendance_request.name,
- "attendance_date": date(date.today().year, 1, 1)
+ "attendance_date": date(date.today().year, 1, 1),
},
- fieldname="status"
+ fieldname="status",
)
- self.assertEqual(attendance_status, 'Work From Home')
+ self.assertEqual(attendance_status, "Work From Home")
attendance_request.cancel()
@@ -88,11 +89,12 @@ class TestAttendanceRequest(unittest.TestCase):
"Attendance",
filters={
"attendance_request": attendance_request.name,
- "attendance_date": date(date.today().year, 1, 1)
+ "attendance_date": date(date.today().year, 1, 1),
},
- fieldname="docstatus"
+ fieldname="docstatus",
)
self.assertEqual(attendance_docstatus, 2)
+
def get_employee():
return frappe.get_doc("Employee", "_T-Employee-00001")
diff --git a/erpnext/hr/doctype/branch/test_branch.py b/erpnext/hr/doctype/branch/test_branch.py
index e84c6e4c60e..c14d4aad690 100644
--- a/erpnext/hr/doctype/branch/test_branch.py
+++ b/erpnext/hr/doctype/branch/test_branch.py
@@ -3,4 +3,4 @@
import frappe
-test_records = frappe.get_test_records('Branch')
+test_records = frappe.get_test_records("Branch")
diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
index 7d6051508ad..d233226e663 100644
--- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
+++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
@@ -18,14 +18,15 @@ from erpnext.hr.utils import (
class CompensatoryLeaveRequest(Document):
-
def validate(self):
validate_active_employee(self.employee)
validate_dates(self, self.work_from_date, self.work_end_date)
if self.half_day:
if not self.half_day_date:
frappe.throw(_("Half Day Date is mandatory"))
- if not getdate(self.work_from_date)<=getdate(self.half_day_date)<=getdate(self.work_end_date):
+ if (
+ not getdate(self.work_from_date) <= getdate(self.half_day_date) <= getdate(self.work_end_date)
+ ):
frappe.throw(_("Half Day Date should be in between Work From Date and Work End Date"))
validate_overlap(self, self.work_from_date, self.work_end_date)
self.validate_holidays()
@@ -34,13 +35,16 @@ class CompensatoryLeaveRequest(Document):
frappe.throw(_("Leave Type is madatory"))
def validate_attendance(self):
- attendance = frappe.get_all('Attendance',
+ attendance = frappe.get_all(
+ "Attendance",
filters={
- 'attendance_date': ['between', (self.work_from_date, self.work_end_date)],
- 'status': 'Present',
- 'docstatus': 1,
- 'employee': self.employee
- }, fields=['attendance_date', 'status'])
+ "attendance_date": ["between", (self.work_from_date, self.work_end_date)],
+ "status": "Present",
+ "docstatus": 1,
+ "employee": self.employee,
+ },
+ fields=["attendance_date", "status"],
+ )
if len(attendance) < date_diff(self.work_end_date, self.work_from_date) + 1:
frappe.throw(_("You are not present all day(s) between compensatory leave request days"))
@@ -49,7 +53,9 @@ class CompensatoryLeaveRequest(Document):
holidays = get_holiday_dates_for_employee(self.employee, self.work_from_date, self.work_end_date)
if len(holidays) < date_diff(self.work_end_date, self.work_from_date) + 1:
if date_diff(self.work_end_date, self.work_from_date):
- msg = _("The days between {0} to {1} are not valid holidays.").format(frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date)))
+ msg = _("The days between {0} to {1} are not valid holidays.").format(
+ frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date))
+ )
else:
msg = _("{0} is not a holiday.").format(frappe.bold(format_date(self.work_from_date)))
@@ -70,13 +76,19 @@ class CompensatoryLeaveRequest(Document):
leave_allocation.db_set("total_leaves_allocated", leave_allocation.total_leaves_allocated)
# generate additional ledger entry for the new compensatory leaves off
- create_additional_leave_ledger_entry(leave_allocation, date_difference, add_days(self.work_end_date, 1))
+ create_additional_leave_ledger_entry(
+ leave_allocation, date_difference, add_days(self.work_end_date, 1)
+ )
else:
leave_allocation = self.create_leave_allocation(leave_period, date_difference)
self.db_set("leave_allocation", leave_allocation.name)
else:
- frappe.throw(_("There is no leave period in between {0} and {1}").format(format_date(self.work_from_date), format_date(self.work_end_date)))
+ frappe.throw(
+ _("There is no leave period in between {0} and {1}").format(
+ format_date(self.work_from_date), format_date(self.work_end_date)
+ )
+ )
def on_cancel(self):
if self.leave_allocation:
@@ -93,10 +105,13 @@ class CompensatoryLeaveRequest(Document):
leave_allocation.db_set("total_leaves_allocated", leave_allocation.total_leaves_allocated)
# create reverse entry on cancelation
- create_additional_leave_ledger_entry(leave_allocation, date_difference * -1, add_days(self.work_end_date, 1))
+ create_additional_leave_ledger_entry(
+ leave_allocation, date_difference * -1, add_days(self.work_end_date, 1)
+ )
def get_existing_allocation_for_period(self, leave_period):
- leave_allocation = frappe.db.sql("""
+ leave_allocation = frappe.db.sql(
+ """
select name
from `tabLeave Allocation`
where employee=%(employee)s and leave_type=%(leave_type)s
@@ -104,12 +119,15 @@ class CompensatoryLeaveRequest(Document):
and (from_date between %(from_date)s and %(to_date)s
or to_date between %(from_date)s and %(to_date)s
or (from_date < %(from_date)s and to_date > %(to_date)s))
- """, {
- "from_date": leave_period[0].from_date,
- "to_date": leave_period[0].to_date,
- "employee": self.employee,
- "leave_type": self.leave_type
- }, as_dict=1)
+ """,
+ {
+ "from_date": leave_period[0].from_date,
+ "to_date": leave_period[0].to_date,
+ "employee": self.employee,
+ "leave_type": self.leave_type,
+ },
+ as_dict=1,
+ )
if leave_allocation:
return frappe.get_doc("Leave Allocation", leave_allocation[0].name)
@@ -118,18 +136,20 @@ class CompensatoryLeaveRequest(Document):
def create_leave_allocation(self, leave_period, date_difference):
is_carry_forward = frappe.db.get_value("Leave Type", self.leave_type, "is_carry_forward")
- allocation = frappe.get_doc(dict(
- doctype="Leave Allocation",
- employee=self.employee,
- employee_name=self.employee_name,
- leave_type=self.leave_type,
- from_date=add_days(self.work_end_date, 1),
- to_date=leave_period[0].to_date,
- carry_forward=cint(is_carry_forward),
- new_leaves_allocated=date_difference,
- total_leaves_allocated=date_difference,
- description=self.reason
- ))
+ allocation = frappe.get_doc(
+ dict(
+ doctype="Leave Allocation",
+ employee=self.employee,
+ employee_name=self.employee_name,
+ leave_type=self.leave_type,
+ from_date=add_days(self.work_end_date, 1),
+ to_date=leave_period[0].to_date,
+ carry_forward=cint(is_carry_forward),
+ new_leaves_allocated=date_difference,
+ total_leaves_allocated=date_difference,
+ description=self.reason,
+ )
+ )
allocation.insert(ignore_permissions=True)
allocation.submit()
return allocation
diff --git a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py
index 5e51879328b..7bbec293f41 100644
--- a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py
+++ b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py
@@ -12,12 +12,17 @@ from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_perio
test_dependencies = ["Employee"]
+
class TestCompensatoryLeaveRequest(unittest.TestCase):
def setUp(self):
- frappe.db.sql(''' delete from `tabCompensatory Leave Request`''')
- frappe.db.sql(''' delete from `tabLeave Ledger Entry`''')
- frappe.db.sql(''' delete from `tabLeave Allocation`''')
- frappe.db.sql(''' delete from `tabAttendance` where attendance_date in {0} '''.format((today(), add_days(today(), -1)))) #nosec
+ frappe.db.sql(""" delete from `tabCompensatory Leave Request`""")
+ frappe.db.sql(""" delete from `tabLeave Ledger Entry`""")
+ frappe.db.sql(""" delete from `tabLeave Allocation`""")
+ frappe.db.sql(
+ """ delete from `tabAttendance` where attendance_date in {0} """.format(
+ (today(), add_days(today(), -1))
+ )
+ ) # nosec
create_leave_period(add_months(today(), -3), add_months(today(), 3), "_Test Company")
create_holiday_list()
@@ -26,7 +31,7 @@ class TestCompensatoryLeaveRequest(unittest.TestCase):
employee.save()
def test_leave_balance_on_submit(self):
- ''' check creation of leave allocation on submission of compensatory leave request '''
+ """check creation of leave allocation on submission of compensatory leave request"""
employee = get_employee()
mark_attendance(employee)
compensatory_leave_request = get_compensatory_leave_request(employee.name)
@@ -34,18 +39,27 @@ class TestCompensatoryLeaveRequest(unittest.TestCase):
before = get_leave_balance_on(employee.name, compensatory_leave_request.leave_type, today())
compensatory_leave_request.submit()
- self.assertEqual(get_leave_balance_on(employee.name, compensatory_leave_request.leave_type, add_days(today(), 1)), before + 1)
+ self.assertEqual(
+ get_leave_balance_on(
+ employee.name, compensatory_leave_request.leave_type, add_days(today(), 1)
+ ),
+ before + 1,
+ )
def test_leave_allocation_update_on_submit(self):
employee = get_employee()
mark_attendance(employee, date=add_days(today(), -1))
- compensatory_leave_request = get_compensatory_leave_request(employee.name, leave_date=add_days(today(), -1))
+ compensatory_leave_request = get_compensatory_leave_request(
+ employee.name, leave_date=add_days(today(), -1)
+ )
compensatory_leave_request.submit()
# leave allocation creation on submit
- leaves_allocated = frappe.db.get_value('Leave Allocation', {
- 'name': compensatory_leave_request.leave_allocation
- }, ['total_leaves_allocated'])
+ leaves_allocated = frappe.db.get_value(
+ "Leave Allocation",
+ {"name": compensatory_leave_request.leave_allocation},
+ ["total_leaves_allocated"],
+ )
self.assertEqual(leaves_allocated, 1)
mark_attendance(employee)
@@ -53,20 +67,22 @@ class TestCompensatoryLeaveRequest(unittest.TestCase):
compensatory_leave_request.submit()
# leave allocation updates on submission of second compensatory leave request
- leaves_allocated = frappe.db.get_value('Leave Allocation', {
- 'name': compensatory_leave_request.leave_allocation
- }, ['total_leaves_allocated'])
+ leaves_allocated = frappe.db.get_value(
+ "Leave Allocation",
+ {"name": compensatory_leave_request.leave_allocation},
+ ["total_leaves_allocated"],
+ )
self.assertEqual(leaves_allocated, 2)
def test_creation_of_leave_ledger_entry_on_submit(self):
- ''' check creation of leave ledger entry on submission of leave request '''
+ """check creation of leave ledger entry on submission of leave request"""
employee = get_employee()
mark_attendance(employee)
compensatory_leave_request = get_compensatory_leave_request(employee.name)
compensatory_leave_request.submit()
filters = dict(transaction_name=compensatory_leave_request.leave_allocation)
- leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=filters)
+ leave_ledger_entry = frappe.get_all("Leave Ledger Entry", fields="*", filters=filters)
self.assertEqual(len(leave_ledger_entry), 1)
self.assertEqual(leave_ledger_entry[0].employee, compensatory_leave_request.employee)
@@ -75,60 +91,67 @@ class TestCompensatoryLeaveRequest(unittest.TestCase):
# check reverse leave ledger entry on cancellation
compensatory_leave_request.cancel()
- leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=filters, order_by = 'creation desc')
+ leave_ledger_entry = frappe.get_all(
+ "Leave Ledger Entry", fields="*", filters=filters, order_by="creation desc"
+ )
self.assertEqual(len(leave_ledger_entry), 2)
self.assertEqual(leave_ledger_entry[0].employee, compensatory_leave_request.employee)
self.assertEqual(leave_ledger_entry[0].leave_type, compensatory_leave_request.leave_type)
self.assertEqual(leave_ledger_entry[0].leaves, -1)
+
def get_compensatory_leave_request(employee, leave_date=today()):
- prev_comp_leave_req = frappe.db.get_value('Compensatory Leave Request',
- dict(leave_type='Compensatory Off',
+ prev_comp_leave_req = frappe.db.get_value(
+ "Compensatory Leave Request",
+ dict(
+ leave_type="Compensatory Off",
work_from_date=leave_date,
work_end_date=leave_date,
- employee=employee), 'name')
- if prev_comp_leave_req:
- return frappe.get_doc('Compensatory Leave Request', prev_comp_leave_req)
-
- return frappe.get_doc(dict(
- doctype='Compensatory Leave Request',
employee=employee,
- leave_type='Compensatory Off',
+ ),
+ "name",
+ )
+ if prev_comp_leave_req:
+ return frappe.get_doc("Compensatory Leave Request", prev_comp_leave_req)
+
+ return frappe.get_doc(
+ dict(
+ doctype="Compensatory Leave Request",
+ employee=employee,
+ leave_type="Compensatory Off",
work_from_date=leave_date,
work_end_date=leave_date,
- reason='test'
- )).insert()
+ reason="test",
+ )
+ ).insert()
-def mark_attendance(employee, date=today(), status='Present'):
- if not frappe.db.exists(dict(doctype='Attendance', employee=employee.name, attendance_date=date, status='Present')):
- attendance = frappe.get_doc({
- "doctype": "Attendance",
- "employee": employee.name,
- "attendance_date": date,
- "status": status
- })
+
+def mark_attendance(employee, date=today(), status="Present"):
+ if not frappe.db.exists(
+ dict(doctype="Attendance", employee=employee.name, attendance_date=date, status="Present")
+ ):
+ attendance = frappe.get_doc(
+ {"doctype": "Attendance", "employee": employee.name, "attendance_date": date, "status": status}
+ )
attendance.save()
attendance.submit()
+
def create_holiday_list():
if frappe.db.exists("Holiday List", "_Test Compensatory Leave"):
return
- holiday_list = frappe.get_doc({
- "doctype": "Holiday List",
- "from_date": add_months(today(), -3),
- "to_date": add_months(today(), 3),
- "holidays": [
- {
- "description": "Test Holiday",
- "holiday_date": today()
- },
- {
- "description": "Test Holiday 1",
- "holiday_date": add_days(today(), -1)
- }
- ],
- "holiday_list_name": "_Test Compensatory Leave"
- })
+ holiday_list = frappe.get_doc(
+ {
+ "doctype": "Holiday List",
+ "from_date": add_months(today(), -3),
+ "to_date": add_months(today(), 3),
+ "holidays": [
+ {"description": "Test Holiday", "holiday_date": today()},
+ {"description": "Test Holiday 1", "holiday_date": add_days(today(), -1)},
+ ],
+ "holiday_list_name": "_Test Compensatory Leave",
+ }
+ )
holiday_list.save()
diff --git a/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py b/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py
index 38e1f54ba96..d311188090d 100644
--- a/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py
+++ b/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py
@@ -12,53 +12,59 @@ from six import string_types
class DailyWorkSummary(Document):
def send_mails(self, dws_group, emails):
- '''Send emails to get daily work summary to all users \
- in selected daily work summary group'''
- incoming_email_account = frappe.db.get_value('Email Account',
- dict(enable_incoming=1, default_incoming=1),
- 'email_id')
+ """Send emails to get daily work summary to all users \
+ in selected daily work summary group"""
+ incoming_email_account = frappe.db.get_value(
+ "Email Account", dict(enable_incoming=1, default_incoming=1), "email_id"
+ )
- self.db_set('email_sent_to', '\n'.join(emails))
- frappe.sendmail(recipients=emails,
+ self.db_set("email_sent_to", "\n".join(emails))
+ frappe.sendmail(
+ recipients=emails,
message=dws_group.message,
subject=dws_group.subject,
reference_doctype=self.doctype,
reference_name=self.name,
- reply_to=incoming_email_account)
+ reply_to=incoming_email_account,
+ )
def send_summary(self):
- '''Send summary of all replies. Called at midnight'''
+ """Send summary of all replies. Called at midnight"""
args = self.get_message_details()
emails = get_user_emails_from_group(self.daily_work_summary_group)
- frappe.sendmail(recipients=emails,
- template='daily_work_summary',
+ frappe.sendmail(
+ recipients=emails,
+ template="daily_work_summary",
args=args,
subject=_(self.daily_work_summary_group),
reference_doctype=self.doctype,
- reference_name=self.name)
+ reference_name=self.name,
+ )
- self.db_set('status', 'Sent')
+ self.db_set("status", "Sent")
def get_message_details(self):
- '''Return args for template'''
- dws_group = frappe.get_doc('Daily Work Summary Group',
- self.daily_work_summary_group)
+ """Return args for template"""
+ dws_group = frappe.get_doc("Daily Work Summary Group", self.daily_work_summary_group)
- replies = frappe.get_all('Communication',
- fields=['content', 'text_content', 'sender'],
- filters=dict(reference_doctype=self.doctype,
+ replies = frappe.get_all(
+ "Communication",
+ fields=["content", "text_content", "sender"],
+ filters=dict(
+ reference_doctype=self.doctype,
reference_name=self.name,
- communication_type='Communication',
- sent_or_received='Received'),
- order_by='creation asc')
+ communication_type="Communication",
+ sent_or_received="Received",
+ ),
+ order_by="creation asc",
+ )
did_not_reply = self.email_sent_to.split()
for d in replies:
- user = frappe.db.get_values("User",
- {"email": d.sender},
- ["full_name", "user_image"],
- as_dict=True)
+ user = frappe.db.get_values(
+ "User", {"email": d.sender}, ["full_name", "user_image"], as_dict=True
+ )
d.sender_name = user[0].full_name if user else d.sender
d.image = user[0].image if user and user[0].image else None
@@ -67,17 +73,13 @@ class DailyWorkSummary(Document):
# make thumbnail image
try:
if original_image:
- file_name = frappe.get_list('File',
- {'file_url': original_image})
+ file_name = frappe.get_list("File", {"file_url": original_image})
if file_name:
file_name = file_name[0].name
- file_doc = frappe.get_doc('File', file_name)
+ file_doc = frappe.get_doc("File", file_name)
thumbnail_image = file_doc.make_thumbnail(
- set_as_thumbnail=False,
- width=100,
- height=100,
- crop=True
+ set_as_thumbnail=False, width=100, height=100, crop=True
)
d.image = thumbnail_image
except Exception:
@@ -86,34 +88,33 @@ class DailyWorkSummary(Document):
if d.sender in did_not_reply:
did_not_reply.remove(d.sender)
if d.text_content:
- d.content = frappe.utils.md_to_html(
- EmailReplyParser.parse_reply(d.text_content)
- )
+ d.content = frappe.utils.md_to_html(EmailReplyParser.parse_reply(d.text_content))
- did_not_reply = [(frappe.db.get_value("User", {"email": email}, "full_name") or email)
- for email in did_not_reply]
+ did_not_reply = [
+ (frappe.db.get_value("User", {"email": email}, "full_name") or email) for email in did_not_reply
+ ]
- return dict(replies=replies,
+ return dict(
+ replies=replies,
original_message=dws_group.message,
- title=_('Work Summary for {0}').format(
- global_date_format(self.creation)
- ),
- did_not_reply=', '.join(did_not_reply) or '',
- did_not_reply_title=_('No replies from'))
+ title=_("Work Summary for {0}").format(global_date_format(self.creation)),
+ did_not_reply=", ".join(did_not_reply) or "",
+ did_not_reply_title=_("No replies from"),
+ )
def get_user_emails_from_group(group):
- '''Returns list of email of enabled users from the given group
+ """Returns list of email of enabled users from the given group
- :param group: Daily Work Summary Group `name`'''
+ :param group: Daily Work Summary Group `name`"""
group_doc = group
if isinstance(group_doc, string_types):
- group_doc = frappe.get_doc('Daily Work Summary Group', group)
+ group_doc = frappe.get_doc("Daily Work Summary Group", group)
emails = get_users_email(group_doc)
return emails
+
def get_users_email(doc):
- return [d.email for d in doc.users
- if frappe.db.get_value("User", d.user, "enabled")]
+ return [d.email for d in doc.users if frappe.db.get_value("User", d.user, "enabled")]
diff --git a/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py b/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py
index 5edfb315564..703436529d0 100644
--- a/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py
+++ b/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py
@@ -9,82 +9,96 @@ import frappe.utils
# test_records = frappe.get_test_records('Daily Work Summary')
+
class TestDailyWorkSummary(unittest.TestCase):
def test_email_trigger(self):
self.setup_and_prepare_test()
for d in self.users:
# check that email is sent to users
if d.message:
- self.assertTrue(d.email in [d.recipient for d in self.emails
- if self.groups.subject in d.message])
+ self.assertTrue(
+ d.email in [d.recipient for d in self.emails if self.groups.subject in d.message]
+ )
def test_email_trigger_failed(self):
- hour = '00:00'
- if frappe.utils.nowtime().split(':')[0] == '00':
- hour = '01:00'
+ hour = "00:00"
+ if frappe.utils.nowtime().split(":")[0] == "00":
+ hour = "01:00"
self.setup_and_prepare_test(hour)
for d in self.users:
# check that email is not sent to users
- self.assertFalse(d.email in [d.recipient for d in self.emails
- if self.groups.subject in d.message])
+ self.assertFalse(
+ d.email in [d.recipient for d in self.emails if self.groups.subject in d.message]
+ )
def test_incoming(self):
# get test mail with message-id as in-reply-to
self.setup_and_prepare_test()
with open(os.path.join(os.path.dirname(__file__), "test_data", "test-reply.raw"), "r") as f:
- if not self.emails: return
- test_mails = [f.read().replace('{{ sender }}',
- self.users[-1].email).replace('{{ message_id }}',
- self.emails[-1].message_id)]
+ if not self.emails:
+ return
+ test_mails = [
+ f.read()
+ .replace("{{ sender }}", self.users[-1].email)
+ .replace("{{ message_id }}", self.emails[-1].message_id)
+ ]
# pull the mail
email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
- email_account.db_set('enable_incoming', 1)
+ email_account.db_set("enable_incoming", 1)
email_account.receive(test_mails=test_mails)
- daily_work_summary = frappe.get_doc('Daily Work Summary',
- frappe.get_all('Daily Work Summary')[0].name)
+ daily_work_summary = frappe.get_doc(
+ "Daily Work Summary", frappe.get_all("Daily Work Summary")[0].name
+ )
args = daily_work_summary.get_message_details()
- self.assertTrue('I built Daily Work Summary!' in args.get('replies')[0].content)
+ self.assertTrue("I built Daily Work Summary!" in args.get("replies")[0].content)
def setup_and_prepare_test(self, hour=None):
- frappe.db.sql('delete from `tabDaily Work Summary`')
- frappe.db.sql('delete from `tabEmail Queue`')
- frappe.db.sql('delete from `tabEmail Queue Recipient`')
- frappe.db.sql('delete from `tabCommunication`')
- frappe.db.sql('delete from `tabDaily Work Summary Group`')
+ frappe.db.sql("delete from `tabDaily Work Summary`")
+ frappe.db.sql("delete from `tabEmail Queue`")
+ frappe.db.sql("delete from `tabEmail Queue Recipient`")
+ frappe.db.sql("delete from `tabCommunication`")
+ frappe.db.sql("delete from `tabDaily Work Summary Group`")
- self.users = frappe.get_all('User',
- fields=['email'],
- filters=dict(email=('!=', 'test@example.com')))
+ self.users = frappe.get_all(
+ "User", fields=["email"], filters=dict(email=("!=", "test@example.com"))
+ )
self.setup_groups(hour)
from erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group import trigger_emails
+
trigger_emails()
# check if emails are created
- self.emails = frappe.db.sql("""select r.recipient, q.message, q.message_id \
+ self.emails = frappe.db.sql(
+ """select r.recipient, q.message, q.message_id \
from `tabEmail Queue` as q, `tabEmail Queue Recipient` as r \
- where q.name = r.parent""", as_dict=1)
-
+ where q.name = r.parent""",
+ as_dict=1,
+ )
def setup_groups(self, hour=None):
# setup email to trigger at this hour
if not hour:
- hour = frappe.utils.nowtime().split(':')[0]
- hour = hour+':00'
+ hour = frappe.utils.nowtime().split(":")[0]
+ hour = hour + ":00"
- groups = frappe.get_doc(dict(doctype="Daily Work Summary Group",
- name="Daily Work Summary",
- users=self.users,
- send_emails_at=hour,
- subject="this is a subject for testing summary emails",
- message='this is a message for testing summary emails'))
+ groups = frappe.get_doc(
+ dict(
+ doctype="Daily Work Summary Group",
+ name="Daily Work Summary",
+ users=self.users,
+ send_emails_at=hour,
+ subject="this is a subject for testing summary emails",
+ message="this is a message for testing summary emails",
+ )
+ )
groups.insert()
self.groups = groups
diff --git a/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py b/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py
index 306f43a418f..e8ec265464e 100644
--- a/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py
+++ b/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py
@@ -16,37 +16,41 @@ class DailyWorkSummaryGroup(Document):
def validate(self):
if self.users:
if not frappe.flags.in_test and not is_incoming_account_enabled():
- frappe.throw(_('Please enable default incoming account before creating Daily Work Summary Group'))
+ frappe.throw(
+ _("Please enable default incoming account before creating Daily Work Summary Group")
+ )
def trigger_emails():
- '''Send emails to Employees at the given hour asking
- them what did they work on today'''
+ """Send emails to Employees at the given hour asking
+ them what did they work on today"""
groups = frappe.get_all("Daily Work Summary Group")
for d in groups:
group_doc = frappe.get_doc("Daily Work Summary Group", d)
- if (is_current_hour(group_doc.send_emails_at)
+ if (
+ is_current_hour(group_doc.send_emails_at)
and not is_holiday(group_doc.holiday_list)
- and group_doc.enabled):
+ and group_doc.enabled
+ ):
emails = get_user_emails_from_group(group_doc)
# find emails relating to a company
if emails:
daily_work_summary = frappe.get_doc(
- dict(doctype='Daily Work Summary', daily_work_summary_group=group_doc.name)
+ dict(doctype="Daily Work Summary", daily_work_summary_group=group_doc.name)
).insert()
daily_work_summary.send_mails(group_doc, emails)
def is_current_hour(hour):
- return frappe.utils.nowtime().split(':')[0] == hour.split(':')[0]
+ return frappe.utils.nowtime().split(":")[0] == hour.split(":")[0]
def send_summary():
- '''Send summary to everyone'''
- for d in frappe.get_all('Daily Work Summary', dict(status='Open')):
- daily_work_summary = frappe.get_doc('Daily Work Summary', d.name)
+ """Send summary to everyone"""
+ for d in frappe.get_all("Daily Work Summary", dict(status="Open")):
+ daily_work_summary = frappe.get_doc("Daily Work Summary", d.name)
daily_work_summary.send_summary()
def is_incoming_account_enabled():
- return frappe.db.get_value('Email Account', dict(enable_incoming=1, default_incoming=1))
+ return frappe.db.get_value("Email Account", dict(enable_incoming=1, default_incoming=1))
diff --git a/erpnext/hr/doctype/department/department.py b/erpnext/hr/doctype/department/department.py
index ed0bfcf0d5a..a9806c529f6 100644
--- a/erpnext/hr/doctype/department/department.py
+++ b/erpnext/hr/doctype/department/department.py
@@ -9,7 +9,7 @@ from erpnext.utilities.transaction_base import delete_events
class Department(NestedSet):
- nsm_parent_field = 'parent_department'
+ nsm_parent_field = "parent_department"
def autoname(self):
root = get_root_of("Department")
@@ -26,7 +26,7 @@ class Department(NestedSet):
def before_rename(self, old, new, merge=False):
# renaming consistency with abbreviation
- if not frappe.get_cached_value('Company', self.company, 'abbr') in new:
+ if not frappe.get_cached_value("Company", self.company, "abbr") in new:
new = get_abbreviated_name(new, self.company)
return new
@@ -39,17 +39,20 @@ class Department(NestedSet):
super(Department, self).on_trash()
delete_events(self.doctype, self.name)
+
def on_doctype_update():
frappe.db.add_index("Department", ["lft", "rgt"])
+
def get_abbreviated_name(name, company):
- abbr = frappe.get_cached_value('Company', company, 'abbr')
- new_name = '{0} - {1}'.format(name, abbr)
+ abbr = frappe.get_cached_value("Company", company, "abbr")
+ new_name = "{0} - {1}".format(name, abbr)
return new_name
+
@frappe.whitelist()
def get_children(doctype, parent=None, company=None, is_root=False):
- condition = ''
+ condition = ""
var_dict = {
"name": get_root_of("Department"),
"parent": parent,
@@ -62,18 +65,26 @@ def get_children(doctype, parent=None, company=None, is_root=False):
else:
condition = "parent_department = %(parent)s"
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
select
name as value,
is_group as expandable
from `tab{doctype}`
where
{condition}
- order by name""".format(doctype=doctype, condition=condition), var_dict, as_dict=1)
+ order by name""".format(
+ doctype=doctype, condition=condition
+ ),
+ var_dict,
+ as_dict=1,
+ )
+
@frappe.whitelist()
def add_node():
from frappe.desk.treeview import make_tree_args
+
args = frappe.form_dict
args = make_tree_args(**args)
diff --git a/erpnext/hr/doctype/department/test_department.py b/erpnext/hr/doctype/department/test_department.py
index 95bf6635011..b8c043f0b2d 100644
--- a/erpnext/hr/doctype/department/test_department.py
+++ b/erpnext/hr/doctype/department/test_department.py
@@ -6,20 +6,26 @@ import unittest
import frappe
test_ignore = ["Leave Block List"]
+
+
class TestDepartment(unittest.TestCase):
- def test_remove_department_data(self):
- doc = create_department("Test Department")
- frappe.delete_doc('Department', doc.name)
+ def test_remove_department_data(self):
+ doc = create_department("Test Department")
+ frappe.delete_doc("Department", doc.name)
+
def create_department(department_name, parent_department=None):
- doc = frappe.get_doc({
- 'doctype': 'Department',
- 'is_group': 0,
- 'parent_department': parent_department,
- 'department_name': department_name,
- 'company': frappe.defaults.get_defaults().company
- }).insert()
+ doc = frappe.get_doc(
+ {
+ "doctype": "Department",
+ "is_group": 0,
+ "parent_department": parent_department,
+ "department_name": department_name,
+ "company": frappe.defaults.get_defaults().company,
+ }
+ ).insert()
- return doc
+ return doc
-test_records = frappe.get_test_records('Department')
+
+test_records = frappe.get_test_records("Department")
diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py
index 375ae7c70ae..d849900ef2d 100644
--- a/erpnext/hr/doctype/department_approver/department_approver.py
+++ b/erpnext/hr/doctype/department_approver/department_approver.py
@@ -10,6 +10,7 @@ from frappe.model.document import Document
class DepartmentApprover(Document):
pass
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_approvers(doctype, txt, searchfield, start, page_len, filters):
@@ -20,25 +21,44 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters):
approvers = []
department_details = {}
department_list = []
- employee = frappe.get_value("Employee", filters.get("employee"), ["employee_name","department", "leave_approver", "expense_approver", "shift_request_approver"], as_dict=True)
+ employee = frappe.get_value(
+ "Employee",
+ filters.get("employee"),
+ ["employee_name", "department", "leave_approver", "expense_approver", "shift_request_approver"],
+ as_dict=True,
+ )
employee_department = filters.get("department") or employee.department
if employee_department:
- department_details = frappe.db.get_value("Department", {"name": employee_department}, ["lft", "rgt"], as_dict=True)
+ department_details = frappe.db.get_value(
+ "Department", {"name": employee_department}, ["lft", "rgt"], as_dict=True
+ )
if department_details:
- department_list = frappe.db.sql("""select name from `tabDepartment` where lft <= %s
+ department_list = frappe.db.sql(
+ """select name from `tabDepartment` where lft <= %s
and rgt >= %s
and disabled=0
- order by lft desc""", (department_details.lft, department_details.rgt), as_list=True)
+ order by lft desc""",
+ (department_details.lft, department_details.rgt),
+ as_list=True,
+ )
if filters.get("doctype") == "Leave Application" and employee.leave_approver:
- approvers.append(frappe.db.get_value("User", employee.leave_approver, ['name', 'first_name', 'last_name']))
+ approvers.append(
+ frappe.db.get_value("User", employee.leave_approver, ["name", "first_name", "last_name"])
+ )
if filters.get("doctype") == "Expense Claim" and employee.expense_approver:
- approvers.append(frappe.db.get_value("User", employee.expense_approver, ['name', 'first_name', 'last_name']))
+ approvers.append(
+ frappe.db.get_value("User", employee.expense_approver, ["name", "first_name", "last_name"])
+ )
if filters.get("doctype") == "Shift Request" and employee.shift_request_approver:
- approvers.append(frappe.db.get_value("User", employee.shift_request_approver, ['name', 'first_name', 'last_name']))
+ approvers.append(
+ frappe.db.get_value(
+ "User", employee.shift_request_approver, ["name", "first_name", "last_name"]
+ )
+ )
if filters.get("doctype") == "Leave Application":
parentfield = "leave_approvers"
@@ -51,15 +71,21 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters):
field_name = "Shift Request Approver"
if department_list:
for d in department_list:
- approvers += frappe.db.sql("""select user.name, user.first_name, user.last_name from
+ approvers += frappe.db.sql(
+ """select user.name, user.first_name, user.last_name from
tabUser user, `tabDepartment Approver` approver where
approver.parent = %s
and user.name like %s
and approver.parentfield = %s
- and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True)
+ and approver.approver=user.name""",
+ (d, "%" + txt + "%", parentfield),
+ as_list=True,
+ )
if len(approvers) == 0:
- error_msg = _("Please set {0} for the Employee: {1}").format(field_name, frappe.bold(employee.employee_name))
+ error_msg = _("Please set {0} for the Employee: {1}").format(
+ field_name, frappe.bold(employee.employee_name)
+ )
if department_list:
error_msg += _(" or for Department: {0}").format(frappe.bold(employee_department))
frappe.throw(error_msg, title=_(field_name + " Missing"))
diff --git a/erpnext/hr/doctype/designation/test_designation.py b/erpnext/hr/doctype/designation/test_designation.py
index f2d6d36ff88..0840d13d1f3 100644
--- a/erpnext/hr/doctype/designation/test_designation.py
+++ b/erpnext/hr/doctype/designation/test_designation.py
@@ -5,15 +5,18 @@ import frappe
# test_records = frappe.get_test_records('Designation')
-def create_designation(**args):
- args = frappe._dict(args)
- if frappe.db.exists("Designation", args.designation_name or "_Test designation"):
- return frappe.get_doc("Designation", args.designation_name or "_Test designation")
- designation = frappe.get_doc({
- "doctype": "Designation",
- "designation_name": args.designation_name or "_Test designation",
- "description": args.description or "_Test description"
- })
- designation.save()
- return designation
+def create_designation(**args):
+ args = frappe._dict(args)
+ if frappe.db.exists("Designation", args.designation_name or "_Test designation"):
+ return frappe.get_doc("Designation", args.designation_name or "_Test designation")
+
+ designation = frappe.get_doc(
+ {
+ "doctype": "Designation",
+ "designation_name": args.designation_name or "_Test designation",
+ "description": args.description or "_Test description",
+ }
+ )
+ designation.save()
+ return designation
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index 8a2950696af..d24e7038422 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -18,22 +18,25 @@ from erpnext.utilities.transaction_base import delete_events
class EmployeeUserDisabledError(frappe.ValidationError):
pass
+
+
class InactiveEmployeeStatusError(frappe.ValidationError):
pass
+
class Employee(NestedSet):
- nsm_parent_field = 'reports_to'
+ nsm_parent_field = "reports_to"
def autoname(self):
naming_method = frappe.db.get_value("HR Settings", None, "emp_created_by")
if not naming_method:
throw(_("Please setup Employee Naming System in Human Resource > HR Settings"))
else:
- if naming_method == 'Naming Series':
+ if naming_method == "Naming Series":
set_name_by_naming_series(self)
- elif naming_method == 'Employee Number':
+ elif naming_method == "Employee Number":
self.name = self.employee_number
- elif naming_method == 'Full Name':
+ elif naming_method == "Full Name":
self.set_employee_name()
self.name = self.employee_name
@@ -41,6 +44,7 @@ class Employee(NestedSet):
def validate(self):
from erpnext.controllers.status_updater import validate_status
+
validate_status(self.status, ["Active", "Inactive", "Suspended", "Left"])
self.employee = self.name
@@ -58,19 +62,19 @@ class Employee(NestedSet):
else:
existing_user_id = frappe.db.get_value("Employee", self.name, "user_id")
if existing_user_id:
- remove_user_permission(
- "Employee", self.name, existing_user_id)
+ remove_user_permission("Employee", self.name, existing_user_id)
def after_rename(self, old, new, merge):
self.db_set("employee", new)
def set_employee_name(self):
- self.employee_name = ' '.join(filter(lambda x: x, [self.first_name, self.middle_name, self.last_name]))
+ self.employee_name = " ".join(
+ filter(lambda x: x, [self.first_name, self.middle_name, self.last_name])
+ )
def validate_user_details(self):
if self.user_id:
- data = frappe.db.get_value("User",
- self.user_id, ["enabled", "user_image"], as_dict=1)
+ data = frappe.db.get_value("User", self.user_id, ["enabled", "user_image"], as_dict=1)
if not data:
self.user_id = None
@@ -93,14 +97,14 @@ class Employee(NestedSet):
self.update_approver_role()
def update_user_permissions(self):
- if not self.create_user_permission: return
- if not has_permission('User Permission', ptype='write', raise_exception=False): return
+ if not self.create_user_permission:
+ return
+ if not has_permission("User Permission", ptype="write", raise_exception=False):
+ return
- employee_user_permission_exists = frappe.db.exists('User Permission', {
- 'allow': 'Employee',
- 'for_value': self.name,
- 'user': self.user_id
- })
+ employee_user_permission_exists = frappe.db.exists(
+ "User Permission", {"allow": "Employee", "for_value": self.name, "user": self.user_id}
+ )
if employee_user_permission_exists:
return
@@ -137,12 +141,14 @@ class Employee(NestedSet):
if not user.user_image:
user.user_image = self.image
try:
- frappe.get_doc({
- "doctype": "File",
- "file_url": self.image,
- "attached_to_doctype": "User",
- "attached_to_name": self.user_id
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "File",
+ "file_url": self.image,
+ "attached_to_doctype": "User",
+ "attached_to_name": self.user_id,
+ }
+ ).insert()
except frappe.DuplicateEntryError:
# already exists
pass
@@ -164,16 +170,32 @@ class Employee(NestedSet):
if self.date_of_birth and getdate(self.date_of_birth) > getdate(today()):
throw(_("Date of Birth cannot be greater than today."))
- if self.date_of_birth and self.date_of_joining and getdate(self.date_of_birth) >= getdate(self.date_of_joining):
+ if (
+ self.date_of_birth
+ and self.date_of_joining
+ and getdate(self.date_of_birth) >= getdate(self.date_of_joining)
+ ):
throw(_("Date of Joining must be greater than Date of Birth"))
- elif self.date_of_retirement and self.date_of_joining and (getdate(self.date_of_retirement) <= getdate(self.date_of_joining)):
+ elif (
+ self.date_of_retirement
+ and self.date_of_joining
+ and (getdate(self.date_of_retirement) <= getdate(self.date_of_joining))
+ ):
throw(_("Date Of Retirement must be greater than Date of Joining"))
- elif self.relieving_date and self.date_of_joining and (getdate(self.relieving_date) < getdate(self.date_of_joining)):
+ elif (
+ self.relieving_date
+ and self.date_of_joining
+ and (getdate(self.relieving_date) < getdate(self.date_of_joining))
+ ):
throw(_("Relieving Date must be greater than or equal to Date of Joining"))
- elif self.contract_end_date and self.date_of_joining and (getdate(self.contract_end_date) <= getdate(self.date_of_joining)):
+ elif (
+ self.contract_end_date
+ and self.date_of_joining
+ and (getdate(self.contract_end_date) <= getdate(self.date_of_joining))
+ ):
throw(_("Contract End Date must be greater than Date of Joining"))
def validate_email(self):
@@ -189,14 +211,20 @@ class Employee(NestedSet):
self.prefered_email = preferred_email
def validate_status(self):
- if self.status == 'Left':
- reports_to = frappe.db.get_all('Employee',
- filters={'reports_to': self.name, 'status': "Active"},
- fields=['name','employee_name']
+ if self.status == "Left":
+ reports_to = frappe.db.get_all(
+ "Employee",
+ filters={"reports_to": self.name, "status": "Active"},
+ fields=["name", "employee_name"],
)
if reports_to:
- link_to_employees = [frappe.utils.get_link_to_form('Employee', employee.name, label=employee.employee_name) for employee in reports_to]
- message = _("The following employees are currently still reporting to {0}:").format(frappe.bold(self.employee_name))
+ link_to_employees = [
+ frappe.utils.get_link_to_form("Employee", employee.name, label=employee.employee_name)
+ for employee in reports_to
+ ]
+ message = _("The following employees are currently still reporting to {0}:").format(
+ frappe.bold(self.employee_name)
+ )
message += "
" + "
".join(link_to_employees)
message += "
"
message += _("Please make sure the employees above report to another Active employee.")
@@ -205,7 +233,7 @@ class Employee(NestedSet):
throw(_("Please enter relieving date."))
def validate_for_enabled_user_id(self, enabled):
- if not self.status == 'Active':
+ if not self.status == "Active":
return
if enabled is None:
@@ -214,11 +242,16 @@ class Employee(NestedSet):
frappe.throw(_("User {0} is disabled").format(self.user_id), EmployeeUserDisabledError)
def validate_duplicate_user_id(self):
- employee = frappe.db.sql_list("""select name from `tabEmployee` where
- user_id=%s and status='Active' and name!=%s""", (self.user_id, self.name))
+ employee = frappe.db.sql_list(
+ """select name from `tabEmployee` where
+ user_id=%s and status='Active' and name!=%s""",
+ (self.user_id, self.name),
+ )
if employee:
- throw(_("User {0} is already assigned to Employee {1}").format(
- self.user_id, employee[0]), frappe.DuplicateEntryError)
+ throw(
+ _("User {0} is already assigned to Employee {1}").format(self.user_id, employee[0]),
+ frappe.DuplicateEntryError,
+ )
def validate_reports_to(self):
if self.reports_to == self.name:
@@ -227,17 +260,25 @@ class Employee(NestedSet):
def on_trash(self):
self.update_nsm_model()
delete_events(self.doctype, self.name)
- if frappe.db.exists("Employee Transfer", {'new_employee_id': self.name, 'docstatus': 1}):
- emp_transfer = frappe.get_doc("Employee Transfer", {'new_employee_id': self.name, 'docstatus': 1})
- emp_transfer.db_set("new_employee_id", '')
+ if frappe.db.exists("Employee Transfer", {"new_employee_id": self.name, "docstatus": 1}):
+ emp_transfer = frappe.get_doc(
+ "Employee Transfer", {"new_employee_id": self.name, "docstatus": 1}
+ )
+ emp_transfer.db_set("new_employee_id", "")
def validate_preferred_email(self):
if self.prefered_contact_email and not self.get(scrub(self.prefered_contact_email)):
frappe.msgprint(_("Please enter {0}").format(self.prefered_contact_email))
def validate_onboarding_process(self):
- employee_onboarding = frappe.get_all("Employee Onboarding",
- filters={"job_applicant": self.job_applicant, "docstatus": 1, "boarding_status": ("!=", "Completed")})
+ employee_onboarding = frappe.get_all(
+ "Employee Onboarding",
+ filters={
+ "job_applicant": self.job_applicant,
+ "docstatus": 1,
+ "boarding_status": ("!=", "Completed"),
+ },
+ )
if employee_onboarding:
doc = frappe.get_doc("Employee Onboarding", employee_onboarding[0].name)
doc.validate_employee_creation()
@@ -245,20 +286,26 @@ class Employee(NestedSet):
def reset_employee_emails_cache(self):
prev_doc = self.get_doc_before_save() or {}
- cell_number = cstr(self.get('cell_number'))
- prev_number = cstr(prev_doc.get('cell_number'))
- if (cell_number != prev_number or
- self.get('user_id') != prev_doc.get('user_id')):
- frappe.cache().hdel('employees_with_number', cell_number)
- frappe.cache().hdel('employees_with_number', prev_number)
+ cell_number = cstr(self.get("cell_number"))
+ prev_number = cstr(prev_doc.get("cell_number"))
+ if cell_number != prev_number or self.get("user_id") != prev_doc.get("user_id"):
+ frappe.cache().hdel("employees_with_number", cell_number)
+ frappe.cache().hdel("employees_with_number", prev_number)
+
def get_timeline_data(doctype, name):
- '''Return timeline for attendance'''
- return dict(frappe.db.sql('''select unix_timestamp(attendance_date), count(*)
+ """Return timeline for attendance"""
+ return dict(
+ frappe.db.sql(
+ """select unix_timestamp(attendance_date), count(*)
from `tabAttendance` where employee=%s
and attendance_date > date_sub(curdate(), interval 1 year)
and status in ('Present', 'Half Day')
- group by attendance_date''', name))
+ group by attendance_date""",
+ name,
+ )
+ )
+
@frappe.whitelist()
def get_retirement_date(date_of_birth=None):
@@ -266,14 +313,15 @@ def get_retirement_date(date_of_birth=None):
if date_of_birth:
try:
retirement_age = int(frappe.db.get_single_value("HR Settings", "retirement_age") or 60)
- dt = add_years(getdate(date_of_birth),retirement_age)
- ret = {'date_of_retirement': dt.strftime('%Y-%m-%d')}
+ dt = add_years(getdate(date_of_birth), retirement_age)
+ ret = {"date_of_retirement": dt.strftime("%Y-%m-%d")}
except ValueError:
# invalid date
ret = {}
return ret
+
def validate_employee_role(doc, method):
# called via User hook
if "Employee" in [d.role for d in doc.get("roles")]:
@@ -281,39 +329,52 @@ def validate_employee_role(doc, method):
frappe.msgprint(_("Please set User ID field in an Employee record to set Employee Role"))
doc.get("roles").remove(doc.get("roles", {"role": "Employee"})[0])
+
def update_user_permissions(doc, method):
# called via User hook
if "Employee" in [d.role for d in doc.get("roles")]:
- if not has_permission('User Permission', ptype='write', raise_exception=False): return
+ if not has_permission("User Permission", ptype="write", raise_exception=False):
+ return
employee = frappe.get_doc("Employee", {"user_id": doc.name})
employee.update_user_permissions()
+
def get_employee_email(employee_doc):
- return employee_doc.get("user_id") or employee_doc.get("personal_email") or employee_doc.get("company_email")
+ return (
+ employee_doc.get("user_id")
+ or employee_doc.get("personal_email")
+ or employee_doc.get("company_email")
+ )
+
def get_holiday_list_for_employee(employee, raise_exception=True):
if employee:
holiday_list, company = frappe.db.get_value("Employee", employee, ["holiday_list", "company"])
else:
- holiday_list=''
- company=frappe.db.get_value("Global Defaults", None, "default_company")
+ holiday_list = ""
+ company = frappe.db.get_value("Global Defaults", None, "default_company")
if not holiday_list:
- holiday_list = frappe.get_cached_value('Company', company, "default_holiday_list")
+ holiday_list = frappe.get_cached_value("Company", company, "default_holiday_list")
if not holiday_list and raise_exception:
- frappe.throw(_('Please set a default Holiday List for Employee {0} or Company {1}').format(employee, company))
+ frappe.throw(
+ _("Please set a default Holiday List for Employee {0} or Company {1}").format(employee, company)
+ )
return holiday_list
-def is_holiday(employee, date=None, raise_exception=True, only_non_weekly=False, with_description=False):
- '''
+
+def is_holiday(
+ employee, date=None, raise_exception=True, only_non_weekly=False, with_description=False
+):
+ """
Returns True if given Employee has an holiday on the given date
- :param employee: Employee `name`
- :param date: Date to check. Will check for today if None
- :param raise_exception: Raise an exception if no holiday list found, default is True
- :param only_non_weekly: Check only non-weekly holidays, default is False
- '''
+ :param employee: Employee `name`
+ :param date: Date to check. Will check for today if None
+ :param raise_exception: Raise an exception if no holiday list found, default is True
+ :param only_non_weekly: Check only non-weekly holidays, default is False
+ """
holiday_list = get_holiday_list_for_employee(employee, raise_exception)
if not date:
@@ -322,34 +383,28 @@ def is_holiday(employee, date=None, raise_exception=True, only_non_weekly=False,
if not holiday_list:
return False
- filters = {
- 'parent': holiday_list,
- 'holiday_date': date
- }
+ filters = {"parent": holiday_list, "holiday_date": date}
if only_non_weekly:
- filters['weekly_off'] = False
+ filters["weekly_off"] = False
- holidays = frappe.get_all(
- 'Holiday',
- fields=['description'],
- filters=filters,
- pluck='description'
- )
+ holidays = frappe.get_all("Holiday", fields=["description"], filters=filters, pluck="description")
if with_description:
return len(holidays) > 0, holidays
return len(holidays) > 0
+
@frappe.whitelist()
-def deactivate_sales_person(status = None, employee = None):
+def deactivate_sales_person(status=None, employee=None):
if status == "Left":
sales_person = frappe.db.get_value("Sales Person", {"Employee": employee})
if sales_person:
frappe.db.set_value("Sales Person", sales_person, "enabled", 0)
+
@frappe.whitelist()
-def create_user(employee, user = None, email=None):
+def create_user(employee, user=None, email=None):
emp = frappe.get_doc("Employee", employee)
employee_name = emp.employee_name.split(" ")
@@ -367,93 +422,98 @@ def create_user(employee, user = None, email=None):
emp.prefered_email = email
user = frappe.new_doc("User")
- user.update({
- "name": emp.employee_name,
- "email": emp.prefered_email,
- "enabled": 1,
- "first_name": first_name,
- "middle_name": middle_name,
- "last_name": last_name,
- "gender": emp.gender,
- "birth_date": emp.date_of_birth,
- "phone": emp.cell_number,
- "bio": emp.bio
- })
+ user.update(
+ {
+ "name": emp.employee_name,
+ "email": emp.prefered_email,
+ "enabled": 1,
+ "first_name": first_name,
+ "middle_name": middle_name,
+ "last_name": last_name,
+ "gender": emp.gender,
+ "birth_date": emp.date_of_birth,
+ "phone": emp.cell_number,
+ "bio": emp.bio,
+ }
+ )
user.insert()
return user.name
+
def get_all_employee_emails(company):
- '''Returns list of employee emails either based on user_id or company_email'''
- employee_list = frappe.get_all('Employee',
- fields=['name','employee_name'],
- filters={
- 'status': 'Active',
- 'company': company
- }
+ """Returns list of employee emails either based on user_id or company_email"""
+ employee_list = frappe.get_all(
+ "Employee", fields=["name", "employee_name"], filters={"status": "Active", "company": company}
)
employee_emails = []
for employee in employee_list:
if not employee:
continue
- user, company_email, personal_email = frappe.db.get_value('Employee',
- employee, ['user_id', 'company_email', 'personal_email'])
+ user, company_email, personal_email = frappe.db.get_value(
+ "Employee", employee, ["user_id", "company_email", "personal_email"]
+ )
email = user or company_email or personal_email
if email:
employee_emails.append(email)
return employee_emails
+
def get_employee_emails(employee_list):
- '''Returns list of employee emails either based on user_id or company_email'''
+ """Returns list of employee emails either based on user_id or company_email"""
employee_emails = []
for employee in employee_list:
if not employee:
continue
- user, company_email, personal_email = frappe.db.get_value('Employee', employee,
- ['user_id', 'company_email', 'personal_email'])
+ user, company_email, personal_email = frappe.db.get_value(
+ "Employee", employee, ["user_id", "company_email", "personal_email"]
+ )
email = user or company_email or personal_email
if email:
employee_emails.append(email)
return employee_emails
+
@frappe.whitelist()
def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False):
- filters = [['status', '=', 'Active']]
- if company and company != 'All Companies':
- filters.append(['company', '=', company])
+ filters = [["status", "=", "Active"]]
+ if company and company != "All Companies":
+ filters.append(["company", "=", company])
- fields = ['name as value', 'employee_name as title']
+ fields = ["name as value", "employee_name as title"]
if is_root:
- parent = ''
- if parent and company and parent!=company:
- filters.append(['reports_to', '=', parent])
+ parent = ""
+ if parent and company and parent != company:
+ filters.append(["reports_to", "=", parent])
else:
- filters.append(['reports_to', '=', ''])
+ filters.append(["reports_to", "=", ""])
- employees = frappe.get_list(doctype, fields=fields,
- filters=filters, order_by='name')
+ employees = frappe.get_list(doctype, fields=fields, filters=filters, order_by="name")
for employee in employees:
- is_expandable = frappe.get_all(doctype, filters=[
- ['reports_to', '=', employee.get('value')]
- ])
+ is_expandable = frappe.get_all(doctype, filters=[["reports_to", "=", employee.get("value")]])
employee.expandable = 1 if is_expandable else 0
return employees
+
def on_doctype_update():
frappe.db.add_index("Employee", ["lft", "rgt"])
-def has_user_permission_for_employee(user_name, employee_name):
- return frappe.db.exists({
- 'doctype': 'User Permission',
- 'user': user_name,
- 'allow': 'Employee',
- 'for_value': employee_name
- })
-def has_upload_permission(doc, ptype='read', user=None):
+def has_user_permission_for_employee(user_name, employee_name):
+ return frappe.db.exists(
+ {
+ "doctype": "User Permission",
+ "user": user_name,
+ "allow": "Employee",
+ "for_value": employee_name,
+ }
+ )
+
+
+def has_upload_permission(doc, ptype="read", user=None):
if not user:
user = frappe.session.user
if get_doc_permissions(doc, user=user, ptype=ptype).get(ptype):
diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py
index 0aaff52ee28..d7235418a23 100644
--- a/erpnext/hr/doctype/employee/employee_dashboard.py
+++ b/erpnext/hr/doctype/employee/employee_dashboard.py
@@ -1,52 +1,46 @@
-
from frappe import _
def get_data():
return {
- 'heatmap': True,
- 'heatmap_message': _('This is based on the attendance of this Employee'),
- 'fieldname': 'employee',
- 'non_standard_fieldnames': {
- 'Bank Account': 'party',
- 'Employee Grievance': 'raised_by'
- },
- 'transactions': [
+ "heatmap": True,
+ "heatmap_message": _("This is based on the attendance of this Employee"),
+ "fieldname": "employee",
+ "non_standard_fieldnames": {"Bank Account": "party", "Employee Grievance": "raised_by"},
+ "transactions": [
+ {"label": _("Attendance"), "items": ["Attendance", "Attendance Request", "Employee Checkin"]},
{
- 'label': _('Attendance'),
- 'items': ['Attendance', 'Attendance Request', 'Employee Checkin']
+ "label": _("Leave"),
+ "items": ["Leave Application", "Leave Allocation", "Leave Policy Assignment"],
},
{
- 'label': _('Leave'),
- 'items': ['Leave Application', 'Leave Allocation', 'Leave Policy Assignment']
+ "label": _("Lifecycle"),
+ "items": [
+ "Employee Transfer",
+ "Employee Promotion",
+ "Employee Separation",
+ "Employee Grievance",
+ ],
+ },
+ {"label": _("Shift"), "items": ["Shift Request", "Shift Assignment"]},
+ {"label": _("Expense"), "items": ["Expense Claim", "Travel Request", "Employee Advance"]},
+ {"label": _("Benefit"), "items": ["Employee Benefit Application", "Employee Benefit Claim"]},
+ {
+ "label": _("Payroll"),
+ "items": [
+ "Salary Structure Assignment",
+ "Salary Slip",
+ "Additional Salary",
+ "Timesheet",
+ "Employee Incentive",
+ "Retention Bonus",
+ "Bank Account",
+ ],
},
{
- 'label': _('Lifecycle'),
- 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
+ "label": _("Training"),
+ "items": ["Training Event", "Training Result", "Training Feedback", "Employee Skill Map"],
},
- {
- 'label': _('Shift'),
- 'items': ['Shift Request', 'Shift Assignment']
- },
- {
- 'label': _('Expense'),
- 'items': ['Expense Claim', 'Travel Request', 'Employee Advance']
- },
- {
- 'label': _('Benefit'),
- 'items': ['Employee Benefit Application', 'Employee Benefit Claim']
- },
- {
- 'label': _('Payroll'),
- 'items': ['Salary Structure Assignment', 'Salary Slip', 'Additional Salary', 'Timesheet','Employee Incentive', 'Retention Bonus', 'Bank Account']
- },
- {
- 'label': _('Training'),
- 'items': ['Training Event', 'Training Result', 'Training Feedback', 'Employee Skill Map']
- },
- {
- 'label': _('Evaluation'),
- 'items': ['Appraisal']
- },
- ]
+ {"label": _("Evaluation"), "items": ["Appraisal"]},
+ ],
}
diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py
index 0bb66374d1e..1829bc4f2fc 100644
--- a/erpnext/hr/doctype/employee/employee_reminders.py
+++ b/erpnext/hr/doctype/employee/employee_reminders.py
@@ -44,13 +44,10 @@ def send_advance_holiday_reminders(frequency):
else:
return
- employees = frappe.db.get_all('Employee', filters={'status': 'Active'}, pluck='name')
+ employees = frappe.db.get_all("Employee", filters={"status": "Active"}, pluck="name")
for employee in employees:
holidays = get_holidays_for_employee(
- employee,
- start_date, end_date,
- only_non_weekly=True,
- raise_exception=False
+ employee, start_date, end_date, only_non_weekly=True, raise_exception=False
)
send_holidays_reminder_in_advance(employee, holidays)
@@ -60,7 +57,7 @@ def send_holidays_reminder_in_advance(employee, holidays):
if not holidays:
return
- employee_doc = frappe.get_doc('Employee', employee)
+ employee_doc = frappe.get_doc("Employee", employee)
employee_email = get_employee_email(employee_doc)
frequency = frappe.db.get_single_value("HR Settings", "frequency")
@@ -70,15 +67,18 @@ def send_holidays_reminder_in_advance(employee, holidays):
subject=_("Upcoming Holidays Reminder"),
template="holiday_reminder",
args=dict(
- reminder_text=_("Hey {}! This email is to remind you about the upcoming holidays.").format(employee_doc.get('first_name')),
+ reminder_text=_("Hey {}! This email is to remind you about the upcoming holidays.").format(
+ employee_doc.get("first_name")
+ ),
message=_("Below is the list of upcoming holidays for you:"),
advance_holiday_reminder=True,
holidays=holidays,
- frequency=frequency[:-2]
+ frequency=frequency[:-2],
),
- header=email_header
+ header=email_header,
)
+
# ------------------
# BIRTHDAY REMINDERS
# ------------------
@@ -109,10 +109,10 @@ def send_birthday_reminders():
def get_birthday_reminder_text_and_message(birthday_persons):
if len(birthday_persons) == 1:
- birthday_person_text = birthday_persons[0]['name']
+ birthday_person_text = birthday_persons[0]["name"]
else:
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
- person_names = [d['name'] for d in birthday_persons]
+ person_names = [d["name"] for d in birthday_persons]
birthday_person_text = comma_sep(person_names, frappe._("{0} & {1}"), False)
reminder_text = _("Today is {0}'s birthday 🎉").format(birthday_person_text)
@@ -133,7 +133,7 @@ def send_birthday_reminder(recipients, reminder_text, birthday_persons, message)
birthday_persons=birthday_persons,
message=message,
),
- header=_("Birthday Reminder 🎂")
+ header=_("Birthday Reminder 🎂"),
)
@@ -150,15 +150,16 @@ def get_employees_having_an_event_today(event_type):
from collections import defaultdict
# Set column based on event type
- if event_type == 'birthday':
- condition_column = 'date_of_birth'
- elif event_type == 'work_anniversary':
- condition_column = 'date_of_joining'
+ if event_type == "birthday":
+ condition_column = "date_of_birth"
+ elif event_type == "work_anniversary":
+ condition_column = "date_of_joining"
else:
return
- employees_born_today = frappe.db.multisql({
- "mariadb": f"""
+ employees_born_today = frappe.db.multisql(
+ {
+ "mariadb": f"""
SELECT `personal_email`, `company`, `company_email`, `user_id`, `employee_name` AS 'name', `image`, `date_of_joining`
FROM `tabEmployee`
WHERE
@@ -170,7 +171,7 @@ def get_employees_having_an_event_today(event_type):
AND
`status` = 'Active'
""",
- "postgres": f"""
+ "postgres": f"""
SELECT "personal_email", "company", "company_email", "user_id", "employee_name" AS 'name', "image"
FROM "tabEmployee"
WHERE
@@ -182,12 +183,15 @@ def get_employees_having_an_event_today(event_type):
AND
"status" = 'Active'
""",
- }, dict(today=today(), condition_column=condition_column), as_dict=1)
+ },
+ dict(today=today(), condition_column=condition_column),
+ as_dict=1,
+ )
grouped_employees = defaultdict(lambda: [])
for employee_doc in employees_born_today:
- grouped_employees[employee_doc.get('company')].append(employee_doc)
+ grouped_employees[employee_doc.get("company")].append(employee_doc)
return grouped_employees
@@ -222,19 +226,19 @@ def send_work_anniversary_reminders():
def get_work_anniversary_reminder_text_and_message(anniversary_persons):
if len(anniversary_persons) == 1:
- anniversary_person = anniversary_persons[0]['name']
+ anniversary_person = anniversary_persons[0]["name"]
persons_name = anniversary_person
# Number of years completed at the company
- completed_years = getdate().year - anniversary_persons[0]['date_of_joining'].year
+ completed_years = getdate().year - anniversary_persons[0]["date_of_joining"].year
anniversary_person += f" completed {completed_years} year(s)"
else:
person_names_with_years = []
names = []
for person in anniversary_persons:
- person_text = person['name']
+ person_text = person["name"]
names.append(person_text)
# Number of years completed at the company
- completed_years = getdate().year - person['date_of_joining'].year
+ completed_years = getdate().year - person["date_of_joining"].year
person_text += f" completed {completed_years} year(s)"
person_names_with_years.append(person_text)
@@ -260,5 +264,5 @@ def send_work_anniversary_reminder(recipients, reminder_text, anniversary_person
anniversary_persons=anniversary_persons,
message=message,
),
- header=_("Work Anniversary Reminder")
+ header=_("Work Anniversary Reminder"),
)
diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py
index 67cbea67e1f..50894b61716 100644
--- a/erpnext/hr/doctype/employee/test_employee.py
+++ b/erpnext/hr/doctype/employee/test_employee.py
@@ -9,7 +9,8 @@ import frappe.utils
import erpnext
from erpnext.hr.doctype.employee.employee import InactiveEmployeeStatusError
-test_records = frappe.get_test_records('Employee')
+test_records = frappe.get_test_records("Employee")
+
class TestEmployee(unittest.TestCase):
def test_employee_status_left(self):
@@ -21,7 +22,7 @@ class TestEmployee(unittest.TestCase):
employee2_doc.reports_to = employee1_doc.name
employee2_doc.save()
employee1_doc.reload()
- employee1_doc.status = 'Left'
+ employee1_doc.status = "Left"
self.assertRaises(InactiveEmployeeStatusError, employee1_doc.save)
def test_employee_status_inactive(self):
@@ -36,11 +37,19 @@ class TestEmployee(unittest.TestCase):
employee_doc.reload()
make_holiday_list()
- frappe.db.set_value("Company", employee_doc.company, "default_holiday_list", "Salary Slip Test Holiday List")
+ frappe.db.set_value(
+ "Company", employee_doc.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)
+ 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)
@@ -48,38 +57,43 @@ class TestEmployee(unittest.TestCase):
def tearDown(self):
frappe.db.rollback()
+
def make_employee(user, company=None, **kwargs):
if not frappe.db.get_value("User", user):
- frappe.get_doc({
- "doctype": "User",
- "email": user,
- "first_name": user,
- "new_password": "password",
- "send_welcome_email": 0,
- "roles": [{"doctype": "Has Role", "role": "Employee"}]
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "User",
+ "email": user,
+ "first_name": user,
+ "new_password": "password",
+ "send_welcome_email": 0,
+ "roles": [{"doctype": "Has Role", "role": "Employee"}],
+ }
+ ).insert()
if not frappe.db.get_value("Employee", {"user_id": user}):
- employee = frappe.get_doc({
- "doctype": "Employee",
- "naming_series": "EMP-",
- "first_name": user,
- "company": company or erpnext.get_default_company(),
- "user_id": user,
- "date_of_birth": "1990-05-08",
- "date_of_joining": "2013-01-01",
- "department": frappe.get_all("Department", fields="name")[0].name,
- "gender": "Female",
- "company_email": user,
- "prefered_contact_email": "Company Email",
- "prefered_email": user,
- "status": "Active",
- "employment_type": "Intern"
- })
+ employee = frappe.get_doc(
+ {
+ "doctype": "Employee",
+ "naming_series": "EMP-",
+ "first_name": user,
+ "company": company or erpnext.get_default_company(),
+ "user_id": user,
+ "date_of_birth": "1990-05-08",
+ "date_of_joining": "2013-01-01",
+ "department": frappe.get_all("Department", fields="name")[0].name,
+ "gender": "Female",
+ "company_email": user,
+ "prefered_contact_email": "Company Email",
+ "prefered_email": user,
+ "status": "Active",
+ "employment_type": "Intern",
+ }
+ )
if kwargs:
employee.update(kwargs)
employee.insert()
return employee.name
else:
- frappe.db.set_value("Employee", {"employee_name":user}, "status", "Active")
- return frappe.get_value("Employee", {"employee_name":user}, "name")
+ frappe.db.set_value("Employee", {"employee_name": user}, "status", "Active")
+ return frappe.get_value("Employee", {"employee_name": user}, "name")
diff --git a/erpnext/hr/doctype/employee/test_employee_reminders.py b/erpnext/hr/doctype/employee/test_employee_reminders.py
index a4097ab9d19..9bde77c9569 100644
--- a/erpnext/hr/doctype/employee/test_employee_reminders.py
+++ b/erpnext/hr/doctype/employee/test_employee_reminders.py
@@ -21,23 +21,22 @@ class TestEmployeeReminders(unittest.TestCase):
# Create a test holiday list
test_holiday_dates = cls.get_test_holiday_dates()
test_holiday_list = make_holiday_list(
- 'TestHolidayRemindersList',
+ "TestHolidayRemindersList",
holiday_dates=[
- {'holiday_date': test_holiday_dates[0], 'description': 'test holiday1'},
- {'holiday_date': test_holiday_dates[1], 'description': 'test holiday2'},
- {'holiday_date': test_holiday_dates[2], 'description': 'test holiday3', 'weekly_off': 1},
- {'holiday_date': test_holiday_dates[3], 'description': 'test holiday4'},
- {'holiday_date': test_holiday_dates[4], 'description': 'test holiday5'},
- {'holiday_date': test_holiday_dates[5], 'description': 'test holiday6'},
+ {"holiday_date": test_holiday_dates[0], "description": "test holiday1"},
+ {"holiday_date": test_holiday_dates[1], "description": "test holiday2"},
+ {"holiday_date": test_holiday_dates[2], "description": "test holiday3", "weekly_off": 1},
+ {"holiday_date": test_holiday_dates[3], "description": "test holiday4"},
+ {"holiday_date": test_holiday_dates[4], "description": "test holiday5"},
+ {"holiday_date": test_holiday_dates[5], "description": "test holiday6"},
],
- from_date=getdate()-timedelta(days=10),
- to_date=getdate()+timedelta(weeks=5)
+ from_date=getdate() - timedelta(days=10),
+ to_date=getdate() + timedelta(weeks=5),
)
# Create a test employee
test_employee = frappe.get_doc(
- 'Employee',
- make_employee('test@gopher.io', company="_Test Company")
+ "Employee", make_employee("test@gopher.io", company="_Test Company")
)
# Attach the holiday list to employee
@@ -49,16 +48,16 @@ class TestEmployeeReminders(unittest.TestCase):
cls.test_holiday_dates = test_holiday_dates
# Employee without holidays in this month/week
- test_employee_2 = make_employee('test@empwithoutholiday.io', company="_Test Company")
- test_employee_2 = frappe.get_doc('Employee', test_employee_2)
+ test_employee_2 = make_employee("test@empwithoutholiday.io", company="_Test Company")
+ test_employee_2 = frappe.get_doc("Employee", test_employee_2)
test_holiday_list = make_holiday_list(
- 'TestHolidayRemindersList2',
+ "TestHolidayRemindersList2",
holiday_dates=[
- {'holiday_date': add_months(getdate(), 1), 'description': 'test holiday1'},
+ {"holiday_date": add_months(getdate(), 1), "description": "test holiday1"},
],
from_date=add_months(getdate(), -2),
- to_date=add_months(getdate(), 2)
+ to_date=add_months(getdate(), 2),
)
test_employee_2.holiday_list = test_holiday_list.name
test_employee_2.save()
@@ -71,11 +70,11 @@ class TestEmployeeReminders(unittest.TestCase):
today_date = getdate()
return [
today_date,
- today_date-timedelta(days=4),
- today_date-timedelta(days=3),
- today_date+timedelta(days=1),
- today_date+timedelta(days=3),
- today_date+timedelta(weeks=3)
+ today_date - timedelta(days=4),
+ today_date - timedelta(days=3),
+ today_date + timedelta(days=1),
+ today_date + timedelta(days=3),
+ today_date + timedelta(weeks=3),
]
def setUp(self):
@@ -88,19 +87,23 @@ class TestEmployeeReminders(unittest.TestCase):
self.assertTrue(is_holiday(self.test_employee.name))
self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[1]))
- self.assertFalse(is_holiday(self.test_employee.name, date=getdate()-timedelta(days=1)))
+ self.assertFalse(is_holiday(self.test_employee.name, date=getdate() - timedelta(days=1)))
# Test weekly_off holidays
self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2]))
- self.assertFalse(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2], only_non_weekly=True))
+ self.assertFalse(
+ is_holiday(self.test_employee.name, date=self.test_holiday_dates[2], only_non_weekly=True)
+ )
# Test with descriptions
has_holiday, descriptions = is_holiday(self.test_employee.name, with_description=True)
self.assertTrue(has_holiday)
- self.assertTrue('test holiday1' in descriptions)
+ self.assertTrue("test holiday1" in descriptions)
def test_birthday_reminders(self):
- employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
+ employee = frappe.get_doc(
+ "Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]
+ )
employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:]
employee.company_email = "test@example.com"
employee.company = "_Test Company"
@@ -124,7 +127,8 @@ class TestEmployeeReminders(unittest.TestCase):
self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message)
def test_work_anniversary_reminders(self):
- make_employee("test_work_anniversary@gmail.com",
+ make_employee(
+ "test_work_anniversary@gmail.com",
date_of_joining="1998" + frappe.utils.nowdate()[4:],
company="_Test Company",
)
@@ -134,7 +138,7 @@ class TestEmployeeReminders(unittest.TestCase):
send_work_anniversary_reminders,
)
- employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary')
+ employees_having_work_anniversary = get_employees_having_an_event_today("work_anniversary")
employees = employees_having_work_anniversary.get("_Test Company") or []
user_ids = []
for entry in employees:
@@ -152,14 +156,15 @@ class TestEmployeeReminders(unittest.TestCase):
self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message)
def test_work_anniversary_reminder_not_sent_for_0_years(self):
- make_employee("test_work_anniversary_2@gmail.com",
+ make_employee(
+ "test_work_anniversary_2@gmail.com",
date_of_joining=getdate(),
company="_Test Company",
)
from erpnext.hr.doctype.employee.employee_reminders import get_employees_having_an_event_today
- employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary')
+ employees_having_work_anniversary = get_employees_having_an_event_today("work_anniversary")
employees = employees_having_work_anniversary.get("_Test Company") or []
user_ids = []
for entry in employees:
@@ -168,20 +173,18 @@ class TestEmployeeReminders(unittest.TestCase):
self.assertTrue("test_work_anniversary_2@gmail.com" not in user_ids)
def test_send_holidays_reminder_in_advance(self):
- setup_hr_settings('Weekly')
+ setup_hr_settings("Weekly")
holidays = get_holidays_for_employee(
- self.test_employee.get('name'),
- getdate(), getdate() + timedelta(days=3),
- only_non_weekly=True,
- raise_exception=False
- )
-
- send_holidays_reminder_in_advance(
- self.test_employee.get('name'),
- holidays
+ self.test_employee.get("name"),
+ getdate(),
+ getdate() + timedelta(days=3),
+ only_non_weekly=True,
+ raise_exception=False,
)
+ send_holidays_reminder_in_advance(self.test_employee.get("name"), holidays)
+
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertEqual(len(email_queue), 1)
self.assertTrue("Holidays this Week." in email_queue[0].message)
@@ -189,67 +192,69 @@ class TestEmployeeReminders(unittest.TestCase):
def test_advance_holiday_reminders_monthly(self):
from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_monthly
- setup_hr_settings('Monthly')
+ setup_hr_settings("Monthly")
# disable emp 2, set same holiday list
- frappe.db.set_value('Employee', self.test_employee_2.name, {
- 'status': 'Left',
- 'holiday_list': self.test_employee.holiday_list
- })
+ frappe.db.set_value(
+ "Employee",
+ self.test_employee_2.name,
+ {"status": "Left", "holiday_list": self.test_employee.holiday_list},
+ )
send_reminders_in_advance_monthly()
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue(len(email_queue) > 0)
# even though emp 2 has holiday, non-active employees should not be recipients
- recipients = frappe.db.get_all('Email Queue Recipient', pluck='recipient')
+ recipients = frappe.db.get_all("Email Queue Recipient", pluck="recipient")
self.assertTrue(self.test_employee_2.user_id not in recipients)
# teardown: enable emp 2
- frappe.db.set_value('Employee', self.test_employee_2.name, {
- 'status': 'Active',
- 'holiday_list': self.holiday_list_2.name
- })
+ frappe.db.set_value(
+ "Employee",
+ self.test_employee_2.name,
+ {"status": "Active", "holiday_list": self.holiday_list_2.name},
+ )
def test_advance_holiday_reminders_weekly(self):
from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_weekly
- setup_hr_settings('Weekly')
+ setup_hr_settings("Weekly")
# disable emp 2, set same holiday list
- frappe.db.set_value('Employee', self.test_employee_2.name, {
- 'status': 'Left',
- 'holiday_list': self.test_employee.holiday_list
- })
+ frappe.db.set_value(
+ "Employee",
+ self.test_employee_2.name,
+ {"status": "Left", "holiday_list": self.test_employee.holiday_list},
+ )
send_reminders_in_advance_weekly()
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue(len(email_queue) > 0)
# even though emp 2 has holiday, non-active employees should not be recipients
- recipients = frappe.db.get_all('Email Queue Recipient', pluck='recipient')
+ recipients = frappe.db.get_all("Email Queue Recipient", pluck="recipient")
self.assertTrue(self.test_employee_2.user_id not in recipients)
# teardown: enable emp 2
- frappe.db.set_value('Employee', self.test_employee_2.name, {
- 'status': 'Active',
- 'holiday_list': self.holiday_list_2.name
- })
+ frappe.db.set_value(
+ "Employee",
+ self.test_employee_2.name,
+ {"status": "Active", "holiday_list": self.holiday_list_2.name},
+ )
def test_reminder_not_sent_if_no_holdays(self):
- setup_hr_settings('Monthly')
+ setup_hr_settings("Monthly")
# reminder not sent if there are no holidays
holidays = get_holidays_for_employee(
- self.test_employee_2.get('name'),
- getdate(), getdate() + timedelta(days=3),
+ self.test_employee_2.get("name"),
+ getdate(),
+ getdate() + timedelta(days=3),
only_non_weekly=True,
- raise_exception=False
- )
- send_holidays_reminder_in_advance(
- self.test_employee_2.get('name'),
- holidays
+ raise_exception=False,
)
+ send_holidays_reminder_in_advance(self.test_employee_2.get("name"), holidays)
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertEqual(len(email_queue), 0)
@@ -259,5 +264,5 @@ def setup_hr_settings(frequency=None):
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
hr_settings.send_holiday_reminders = 1
set_proceed_with_frequency_change()
- hr_settings.frequency = frequency or 'Weekly'
- hr_settings.save()
\ No newline at end of file
+ hr_settings.frequency = frequency or "Weekly"
+ hr_settings.save()
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py
index 7aac2b63ed3..3d4023d3195 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.py
@@ -16,17 +16,19 @@ from erpnext.hr.utils import validate_active_employee
class EmployeeAdvanceOverPayment(frappe.ValidationError):
pass
+
class EmployeeAdvance(Document):
def onload(self):
- self.get("__onload").make_payment_via_journal_entry = frappe.db.get_single_value('Accounts Settings',
- 'make_payment_via_journal_entry')
+ self.get("__onload").make_payment_via_journal_entry = frappe.db.get_single_value(
+ "Accounts Settings", "make_payment_via_journal_entry"
+ )
def validate(self):
validate_active_employee(self.employee)
self.set_status()
def on_cancel(self):
- self.ignore_linked_doctypes = ('GL Entry')
+ self.ignore_linked_doctypes = "GL Entry"
def set_status(self):
if self.docstatus == 0:
@@ -46,30 +48,30 @@ class EmployeeAdvance(Document):
paid_amount = (
frappe.qb.from_(gle)
- .select(Sum(gle.debit).as_("paid_amount"))
- .where(
- (gle.against_voucher_type == 'Employee Advance')
- & (gle.against_voucher == self.name)
- & (gle.party_type == 'Employee')
- & (gle.party == self.employee)
- & (gle.docstatus == 1)
- & (gle.is_cancelled == 0)
- )
- ).run(as_dict=True)[0].paid_amount or 0
+ .select(Sum(gle.debit).as_("paid_amount"))
+ .where(
+ (gle.against_voucher_type == "Employee Advance")
+ & (gle.against_voucher == self.name)
+ & (gle.party_type == "Employee")
+ & (gle.party == self.employee)
+ & (gle.docstatus == 1)
+ & (gle.is_cancelled == 0)
+ )
+ ).run(as_dict=True)[0].paid_amount or 0
return_amount = (
frappe.qb.from_(gle)
- .select(Sum(gle.credit).as_("return_amount"))
- .where(
- (gle.against_voucher_type == 'Employee Advance')
- & (gle.voucher_type != 'Expense Claim')
- & (gle.against_voucher == self.name)
- & (gle.party_type == 'Employee')
- & (gle.party == self.employee)
- & (gle.docstatus == 1)
- & (gle.is_cancelled == 0)
- )
- ).run(as_dict=True)[0].return_amount or 0
+ .select(Sum(gle.credit).as_("return_amount"))
+ .where(
+ (gle.against_voucher_type == "Employee Advance")
+ & (gle.voucher_type != "Expense Claim")
+ & (gle.against_voucher == self.name)
+ & (gle.party_type == "Employee")
+ & (gle.party == self.employee)
+ & (gle.docstatus == 1)
+ & (gle.is_cancelled == 0)
+ )
+ ).run(as_dict=True)[0].return_amount or 0
if paid_amount != 0:
paid_amount = flt(paid_amount) / flt(self.exchange_rate)
@@ -77,8 +79,10 @@ class EmployeeAdvance(Document):
return_amount = flt(return_amount) / flt(self.exchange_rate)
if flt(paid_amount) > self.advance_amount:
- frappe.throw(_("Row {0}# Paid Amount cannot be greater than requested advance amount"),
- EmployeeAdvanceOverPayment)
+ frappe.throw(
+ _("Row {0}# Paid Amount cannot be greater than requested advance amount"),
+ EmployeeAdvanceOverPayment,
+ )
if flt(return_amount) > self.paid_amount - self.claimed_amount:
frappe.throw(_("Return amount cannot be greater unclaimed amount"))
@@ -86,11 +90,12 @@ class EmployeeAdvance(Document):
self.db_set("paid_amount", paid_amount)
self.db_set("return_amount", return_amount)
self.set_status()
- frappe.db.set_value("Employee Advance", self.name , "status", self.status)
-
+ frappe.db.set_value("Employee Advance", self.name, "status", self.status)
def update_claimed_amount(self):
- claimed_amount = frappe.db.sql("""
+ claimed_amount = (
+ frappe.db.sql(
+ """
SELECT sum(ifnull(allocated_amount, 0))
FROM `tabExpense Claim Advance` eca, `tabExpense Claim` ec
WHERE
@@ -99,65 +104,83 @@ class EmployeeAdvance(Document):
AND ec.name = eca.parent
AND ec.docstatus=1
AND eca.allocated_amount > 0
- """, self.name)[0][0] or 0
+ """,
+ self.name,
+ )[0][0]
+ or 0
+ )
frappe.db.set_value("Employee Advance", self.name, "claimed_amount", flt(claimed_amount))
self.reload()
self.set_status()
frappe.db.set_value("Employee Advance", self.name, "status", self.status)
+
@frappe.whitelist()
def get_pending_amount(employee, posting_date):
- employee_due_amount = frappe.get_all("Employee Advance", \
- filters = {"employee":employee, "docstatus":1, "posting_date":("<=", posting_date)}, \
- fields = ["advance_amount", "paid_amount"])
+ employee_due_amount = frappe.get_all(
+ "Employee Advance",
+ filters={"employee": employee, "docstatus": 1, "posting_date": ("<=", posting_date)},
+ fields=["advance_amount", "paid_amount"],
+ )
return sum([(emp.advance_amount - emp.paid_amount) for emp in employee_due_amount])
+
@frappe.whitelist()
def make_bank_entry(dt, dn):
doc = frappe.get_doc(dt, dn)
- payment_account = get_default_bank_cash_account(doc.company, account_type="Cash",
- mode_of_payment=doc.mode_of_payment)
+ payment_account = get_default_bank_cash_account(
+ doc.company, account_type="Cash", mode_of_payment=doc.mode_of_payment
+ )
if not payment_account:
frappe.throw(_("Please set a Default Cash Account in Company defaults"))
- advance_account_currency = frappe.db.get_value('Account', doc.advance_account, 'account_currency')
+ advance_account_currency = frappe.db.get_value("Account", doc.advance_account, "account_currency")
- advance_amount, advance_exchange_rate = get_advance_amount_advance_exchange_rate(advance_account_currency,doc )
+ advance_amount, advance_exchange_rate = get_advance_amount_advance_exchange_rate(
+ advance_account_currency, doc
+ )
paying_amount, paying_exchange_rate = get_paying_amount_paying_exchange_rate(payment_account, doc)
je = frappe.new_doc("Journal Entry")
je.posting_date = nowdate()
- je.voucher_type = 'Bank Entry'
+ je.voucher_type = "Bank Entry"
je.company = doc.company
- je.remark = 'Payment against Employee Advance: ' + dn + '\n' + doc.purpose
+ je.remark = "Payment against Employee Advance: " + dn + "\n" + doc.purpose
je.multi_currency = 1 if advance_account_currency != payment_account.account_currency else 0
- je.append("accounts", {
- "account": doc.advance_account,
- "account_currency": advance_account_currency,
- "exchange_rate": flt(advance_exchange_rate),
- "debit_in_account_currency": flt(advance_amount),
- "reference_type": "Employee Advance",
- "reference_name": doc.name,
- "party_type": "Employee",
- "cost_center": erpnext.get_default_cost_center(doc.company),
- "party": doc.employee,
- "is_advance": "Yes"
- })
+ je.append(
+ "accounts",
+ {
+ "account": doc.advance_account,
+ "account_currency": advance_account_currency,
+ "exchange_rate": flt(advance_exchange_rate),
+ "debit_in_account_currency": flt(advance_amount),
+ "reference_type": "Employee Advance",
+ "reference_name": doc.name,
+ "party_type": "Employee",
+ "cost_center": erpnext.get_default_cost_center(doc.company),
+ "party": doc.employee,
+ "is_advance": "Yes",
+ },
+ )
- je.append("accounts", {
- "account": payment_account.account,
- "cost_center": erpnext.get_default_cost_center(doc.company),
- "credit_in_account_currency": flt(paying_amount),
- "account_currency": payment_account.account_currency,
- "account_type": payment_account.account_type,
- "exchange_rate": flt(paying_exchange_rate)
- })
+ je.append(
+ "accounts",
+ {
+ "account": payment_account.account,
+ "cost_center": erpnext.get_default_cost_center(doc.company),
+ "credit_in_account_currency": flt(paying_amount),
+ "account_currency": payment_account.account_currency,
+ "account_type": payment_account.account_type,
+ "exchange_rate": flt(paying_exchange_rate),
+ },
+ )
return je.as_dict()
+
def get_advance_amount_advance_exchange_rate(advance_account_currency, doc):
if advance_account_currency != doc.currency:
advance_amount = flt(doc.advance_amount) * flt(doc.exchange_rate)
@@ -168,6 +191,7 @@ def get_advance_amount_advance_exchange_rate(advance_account_currency, doc):
return advance_amount, advance_exchange_rate
+
def get_paying_amount_paying_exchange_rate(payment_account, doc):
if payment_account.account_currency != doc.currency:
paying_amount = flt(doc.advance_amount) * flt(doc.exchange_rate)
@@ -178,6 +202,7 @@ def get_paying_amount_paying_exchange_rate(payment_account, doc):
return paying_amount, paying_exchange_rate
+
@frappe.whitelist()
def create_return_through_additional_salary(doc):
import json
@@ -185,7 +210,7 @@ def create_return_through_additional_salary(doc):
if isinstance(doc, str):
doc = frappe._dict(json.loads(doc))
- additional_salary = frappe.new_doc('Additional Salary')
+ additional_salary = frappe.new_doc("Additional Salary")
additional_salary.employee = doc.employee
additional_salary.currency = doc.currency
additional_salary.amount = doc.paid_amount - doc.claimed_amount
@@ -195,54 +220,79 @@ def create_return_through_additional_salary(doc):
return additional_salary
+
@frappe.whitelist()
-def make_return_entry(employee, company, employee_advance_name, return_amount, advance_account, currency, exchange_rate, mode_of_payment=None):
- bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment)
+def make_return_entry(
+ employee,
+ company,
+ employee_advance_name,
+ return_amount,
+ advance_account,
+ currency,
+ exchange_rate,
+ mode_of_payment=None,
+):
+ bank_cash_account = get_default_bank_cash_account(
+ company, account_type="Cash", mode_of_payment=mode_of_payment
+ )
if not bank_cash_account:
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.voucher_type = get_voucher_type(mode_of_payment)
je.company = company
- je.remark = 'Return against Employee Advance: ' + employee_advance_name
+ je.remark = "Return against Employee Advance: " + employee_advance_name
je.multi_currency = 1 if advance_account_currency != bank_cash_account.account_currency else 0
- advance_account_amount = flt(return_amount) if advance_account_currency==currency \
+ advance_account_amount = (
+ flt(return_amount)
+ if advance_account_currency == currency
else flt(return_amount) * flt(exchange_rate)
+ )
- je.append('accounts', {
- 'account': advance_account,
- 'credit_in_account_currency': advance_account_amount,
- 'account_currency': advance_account_currency,
- 'exchange_rate': flt(exchange_rate) if advance_account_currency == currency else 1,
- 'reference_type': 'Employee Advance',
- 'reference_name': employee_advance_name,
- 'party_type': 'Employee',
- 'party': employee,
- 'is_advance': 'Yes'
- })
+ je.append(
+ "accounts",
+ {
+ "account": advance_account,
+ "credit_in_account_currency": advance_account_amount,
+ "account_currency": advance_account_currency,
+ "exchange_rate": flt(exchange_rate) if advance_account_currency == currency else 1,
+ "reference_type": "Employee Advance",
+ "reference_name": employee_advance_name,
+ "party_type": "Employee",
+ "party": employee,
+ "is_advance": "Yes",
+ },
+ )
- bank_amount = flt(return_amount) if bank_cash_account.account_currency==currency \
+ bank_amount = (
+ flt(return_amount)
+ if bank_cash_account.account_currency == currency
else flt(return_amount) * flt(exchange_rate)
+ )
- je.append("accounts", {
- "account": bank_cash_account.account,
- "debit_in_account_currency": bank_amount,
- "account_currency": bank_cash_account.account_currency,
- "account_type": bank_cash_account.account_type,
- "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1
- })
+ je.append(
+ "accounts",
+ {
+ "account": bank_cash_account.account,
+ "debit_in_account_currency": bank_amount,
+ "account_currency": bank_cash_account.account_currency,
+ "account_type": bank_cash_account.account_type,
+ "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1,
+ },
+ )
return je.as_dict()
+
def get_voucher_type(mode_of_payment=None):
voucher_type = "Cash Entry"
if mode_of_payment:
- mode_of_payment_type = frappe.get_cached_value('Mode of Payment', mode_of_payment, 'type')
+ mode_of_payment_type = frappe.get_cached_value("Mode of Payment", mode_of_payment, "type")
if mode_of_payment_type == "Bank":
voucher_type = "Bank Entry"
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py b/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py
index 089bd2c1b5c..73fac5131ea 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py
+++ b/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py
@@ -1,18 +1,9 @@
-
-
def get_data():
return {
- 'fieldname': 'employee_advance',
- 'non_standard_fieldnames': {
- 'Payment Entry': 'reference_name',
- 'Journal Entry': 'reference_name'
+ "fieldname": "employee_advance",
+ "non_standard_fieldnames": {
+ "Payment Entry": "reference_name",
+ "Journal Entry": "reference_name",
},
- 'transactions': [
- {
- 'items': ['Expense Claim']
- },
- {
- 'items': ['Payment Entry', 'Journal Entry']
- }
- ]
+ "transactions": [{"items": ["Expense Claim"]}, {"items": ["Payment Entry", "Journal Entry"]}],
}
diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py
index 5f2e720eb46..9b006ffcffe 100644
--- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py
@@ -4,7 +4,7 @@
import unittest
import frappe
-from frappe.utils import nowdate
+from frappe.utils import flt, nowdate
import erpnext
from erpnext.hr.doctype.employee.test_employee import make_employee
@@ -13,6 +13,7 @@ from erpnext.hr.doctype.employee_advance.employee_advance import (
create_return_through_additional_salary,
make_bank_entry,
)
+from erpnext.hr.doctype.expense_claim.expense_claim import get_advances
from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
@@ -58,7 +59,9 @@ class TestEmployeeAdvance(unittest.TestCase):
args = {"type": "Deduction"}
create_salary_component("Advance Salary - Deduction", **args)
- make_salary_structure("Test Additional Salary for Advance Return", "Monthly", employee=employee_name)
+ make_salary_structure(
+ "Test Additional Salary for Advance Return", "Monthly", employee=employee_name
+ )
# additional salary for 700 first
advance.reload()
@@ -100,10 +103,11 @@ def make_payment_entry(advance):
return journal_entry
+
def make_employee_advance(employee_name, args=None):
doc = frappe.new_doc("Employee Advance")
doc.employee = employee_name
- doc.company = "_Test company"
+ doc.company = "_Test company"
doc.purpose = "For site visit"
doc.currency = erpnext.get_company_currency("_Test company")
doc.exchange_rate = 1
@@ -118,3 +122,27 @@ def make_employee_advance(employee_name, args=None):
doc.submit()
return doc
+
+
+def get_advances_for_claim(claim, advance_name, amount=None):
+ advances = get_advances(claim.employee, advance_name)
+
+ for entry in advances:
+ if amount:
+ allocated_amount = amount
+ else:
+ allocated_amount = flt(entry.paid_amount) - flt(entry.claimed_amount)
+
+ claim.append(
+ "advances",
+ {
+ "employee_advance": entry.name,
+ "posting_date": entry.posting_date,
+ "advance_account": entry.advance_account,
+ "advance_paid": entry.paid_amount,
+ "unclaimed_amount": allocated_amount,
+ "allocated_amount": allocated_amount,
+ },
+ )
+
+ return claim
diff --git a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py
index af2ca50b78a..43665cc8b22 100644
--- a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py
+++ b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py
@@ -14,32 +14,31 @@ class EmployeeAttendanceTool(Document):
@frappe.whitelist()
-def get_employees(date, department = None, branch = None, company = None):
+def get_employees(date, department=None, branch=None, company=None):
attendance_not_marked = []
attendance_marked = []
filters = {"status": "Active", "date_of_joining": ["<=", date]}
- for field, value in {'department': department,
- 'branch': branch, 'company': company}.items():
+ for field, value in {"department": department, "branch": branch, "company": company}.items():
if value:
filters[field] = value
- employee_list = frappe.get_list("Employee", fields=["employee", "employee_name"], filters=filters, order_by="employee_name")
+ employee_list = frappe.get_list(
+ "Employee", fields=["employee", "employee_name"], filters=filters, order_by="employee_name"
+ )
marked_employee = {}
- for emp in frappe.get_list("Attendance", fields=["employee", "status"],
- filters={"attendance_date": date}):
- marked_employee[emp['employee']] = emp['status']
+ for emp in frappe.get_list(
+ "Attendance", fields=["employee", "status"], filters={"attendance_date": date}
+ ):
+ marked_employee[emp["employee"]] = emp["status"]
for employee in employee_list:
- employee['status'] = marked_employee.get(employee['employee'])
- if employee['employee'] not in marked_employee:
+ employee["status"] = marked_employee.get(employee["employee"])
+ if employee["employee"] not in marked_employee:
attendance_not_marked.append(employee)
else:
attendance_marked.append(employee)
- return {
- "marked": attendance_marked,
- "unmarked": attendance_not_marked
- }
+ return {"marked": attendance_marked, "unmarked": attendance_not_marked}
@frappe.whitelist()
@@ -53,16 +52,18 @@ def mark_employee_attendance(employee_list, status, date, leave_type=None, compa
else:
leave_type = None
- company = frappe.db.get_value("Employee", employee['employee'], "Company", cache=True)
+ company = frappe.db.get_value("Employee", employee["employee"], "Company", cache=True)
- attendance=frappe.get_doc(dict(
- doctype='Attendance',
- employee=employee.get('employee'),
- employee_name=employee.get('employee_name'),
- attendance_date=getdate(date),
- status=status,
- leave_type=leave_type,
- company=company
- ))
+ attendance = frappe.get_doc(
+ dict(
+ doctype="Attendance",
+ employee=employee.get("employee"),
+ employee_name=employee.get("employee_name"),
+ attendance_date=getdate(date),
+ status=status,
+ leave_type=leave_type,
+ company=company,
+ )
+ )
attendance.insert()
- attendance.submit()
\ No newline at end of file
+ attendance.submit()
diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py
index c1d4ac7fded..f3cd864c908 100644
--- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py
+++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py
@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import cint, get_datetime
+from frappe.utils import cint, get_datetime, get_link_to_form
from erpnext.hr.doctype.shift_assignment.shift_assignment import (
get_actual_start_end_datetime_of_shift,
@@ -20,20 +20,31 @@ class EmployeeCheckin(Document):
self.fetch_shift()
def validate_duplicate_log(self):
- doc = frappe.db.exists('Employee Checkin', {
- 'employee': self.employee,
- 'time': self.time,
- 'name': ['!=', self.name]})
+ doc = frappe.db.exists(
+ "Employee Checkin", {"employee": self.employee, "time": self.time, "name": ["!=", self.name]}
+ )
if doc:
- doc_link = frappe.get_desk_link('Employee Checkin', doc)
- frappe.throw(_('This employee already has a log with the same timestamp.{0}')
- .format(" " + doc_link))
+ doc_link = frappe.get_desk_link("Employee Checkin", doc)
+ frappe.throw(
+ _("This employee already has a log with the same timestamp.{0}").format(" " + doc_link)
+ )
def fetch_shift(self):
- shift_actual_timings = get_actual_start_end_datetime_of_shift(self.employee, get_datetime(self.time), True)
+ shift_actual_timings = get_actual_start_end_datetime_of_shift(
+ self.employee, get_datetime(self.time), True
+ )
if shift_actual_timings[0] and shift_actual_timings[1]:
- if shift_actual_timings[2].shift_type.determine_check_in_and_check_out == 'Strictly based on Log Type in Employee Checkin' and not self.log_type and not self.skip_auto_attendance:
- frappe.throw(_('Log Type is required for check-ins falling in the shift: {0}.').format(shift_actual_timings[2].shift_type.name))
+ if (
+ shift_actual_timings[2].shift_type.determine_check_in_and_check_out
+ == "Strictly based on Log Type in Employee Checkin"
+ and not self.log_type
+ and not self.skip_auto_attendance
+ ):
+ frappe.throw(
+ _("Log Type is required for check-ins falling in the shift: {0}.").format(
+ shift_actual_timings[2].shift_type.name
+ )
+ )
if not self.attendance:
self.shift = shift_actual_timings[2].shift_type.name
self.shift_actual_start = shift_actual_timings[0]
@@ -43,8 +54,16 @@ class EmployeeCheckin(Document):
else:
self.shift = None
+
@frappe.whitelist()
-def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=None, log_type=None, skip_auto_attendance=0, employee_fieldname='attendance_device_id'):
+def add_log_based_on_employee_field(
+ employee_field_value,
+ timestamp,
+ device_id=None,
+ log_type=None,
+ skip_auto_attendance=0,
+ employee_fieldname="attendance_device_id",
+):
"""Finds the relevant Employee using the employee field value and creates a Employee Checkin.
:param employee_field_value: The value to look for in employee field.
@@ -58,11 +77,20 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N
if not employee_field_value or not timestamp:
frappe.throw(_("'employee_field_value' and 'timestamp' are required."))
- employee = frappe.db.get_values("Employee", {employee_fieldname: employee_field_value}, ["name", "employee_name", employee_fieldname], as_dict=True)
+ employee = frappe.db.get_values(
+ "Employee",
+ {employee_fieldname: employee_field_value},
+ ["name", "employee_name", employee_fieldname],
+ as_dict=True,
+ )
if employee:
employee = employee[0]
else:
- frappe.throw(_("No Employee found for the given employee field value. '{}': {}").format(employee_fieldname,employee_field_value))
+ frappe.throw(
+ _("No Employee found for the given employee field value. '{}': {}").format(
+ employee_fieldname, employee_field_value
+ )
+ )
doc = frappe.new_doc("Employee Checkin")
doc.employee = employee.name
@@ -70,13 +98,24 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N
doc.time = timestamp
doc.device_id = device_id
doc.log_type = log_type
- if cint(skip_auto_attendance) == 1: doc.skip_auto_attendance = '1'
+ if cint(skip_auto_attendance) == 1:
+ doc.skip_auto_attendance = "1"
doc.insert()
return doc
-def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, late_entry=False, early_exit=False, in_time=None, out_time=None, shift=None):
+def mark_attendance_and_link_log(
+ logs,
+ attendance_status,
+ attendance_date,
+ working_hours=None,
+ late_entry=False,
+ early_exit=False,
+ in_time=None,
+ out_time=None,
+ shift=None,
+):
"""Creates an attendance and links the attendance to the Employee Checkin.
Note: If attendance is already present for the given date, the logs are marked as skipped and no exception is thrown.
@@ -87,40 +126,54 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
"""
log_names = [x.name for x in logs]
employee = logs[0].employee
- if attendance_status == 'Skip':
- frappe.db.sql("""update `tabEmployee Checkin`
- set skip_auto_attendance = %s
- where name in %s""", ('1', log_names))
+ if attendance_status == "Skip":
+ skip_attendance_in_checkins(log_names)
return None
- elif attendance_status in ('Present', 'Absent', 'Half Day'):
- employee_doc = frappe.get_doc('Employee', employee)
- if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}):
+
+ elif attendance_status in ("Present", "Absent", "Half Day"):
+ employee_doc = frappe.get_doc("Employee", employee)
+ duplicate = frappe.db.exists(
+ "Attendance",
+ {"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")},
+ )
+
+ if not duplicate:
doc_dict = {
- 'doctype': 'Attendance',
- 'employee': employee,
- 'attendance_date': attendance_date,
- 'status': attendance_status,
- 'working_hours': working_hours,
- 'company': employee_doc.company,
- 'shift': shift,
- 'late_entry': late_entry,
- 'early_exit': early_exit,
- 'in_time': in_time,
- 'out_time': out_time
+ "doctype": "Attendance",
+ "employee": employee,
+ "attendance_date": attendance_date,
+ "status": attendance_status,
+ "working_hours": working_hours,
+ "company": employee_doc.company,
+ "shift": shift,
+ "late_entry": late_entry,
+ "early_exit": early_exit,
+ "in_time": in_time,
+ "out_time": out_time,
}
attendance = frappe.get_doc(doc_dict).insert()
attendance.submit()
- frappe.db.sql("""update `tabEmployee Checkin`
+
+ if attendance_status == "Absent":
+ attendance.add_comment(
+ text=_("Employee was marked Absent for not meeting the working hours threshold.")
+ )
+
+ frappe.db.sql(
+ """update `tabEmployee Checkin`
set attendance = %s
- where name in %s""", (attendance.name, log_names))
+ where name in %s""",
+ (attendance.name, log_names),
+ )
return attendance
else:
- frappe.db.sql("""update `tabEmployee Checkin`
- set skip_auto_attendance = %s
- where name in %s""", ('1', log_names))
+ skip_attendance_in_checkins(log_names)
+ if duplicate:
+ add_comment_in_checkins(log_names, duplicate)
+
return None
else:
- frappe.throw(_('{} is an invalid Attendance Status.').format(attendance_status))
+ frappe.throw(_("{} is an invalid Attendance Status.").format(attendance_status))
def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
@@ -133,29 +186,35 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
"""
total_hours = 0
in_time = out_time = None
- if check_in_out_type == 'Alternating entries as IN and OUT during the same shift':
+ if check_in_out_type == "Alternating entries as IN and OUT during the same shift":
in_time = logs[0].time
if len(logs) >= 2:
out_time = logs[-1].time
- if working_hours_calc_type == 'First Check-in and Last Check-out':
+ if working_hours_calc_type == "First Check-in and Last Check-out":
# assumption in this case: First log always taken as IN, Last log always taken as OUT
total_hours = time_diff_in_hours(in_time, logs[-1].time)
- elif working_hours_calc_type == 'Every Valid Check-in and Check-out':
+ elif working_hours_calc_type == "Every Valid Check-in and Check-out":
logs = logs[:]
while len(logs) >= 2:
total_hours += time_diff_in_hours(logs[0].time, logs[1].time)
del logs[:2]
- elif check_in_out_type == 'Strictly based on Log Type in Employee Checkin':
- if working_hours_calc_type == 'First Check-in and Last Check-out':
- first_in_log_index = find_index_in_dict(logs, 'log_type', 'IN')
- first_in_log = logs[first_in_log_index] if first_in_log_index or first_in_log_index == 0 else None
- last_out_log_index = find_index_in_dict(reversed(logs), 'log_type', 'OUT')
- last_out_log = logs[len(logs)-1-last_out_log_index] if last_out_log_index or last_out_log_index == 0 else None
+ elif check_in_out_type == "Strictly based on Log Type in Employee Checkin":
+ if working_hours_calc_type == "First Check-in and Last Check-out":
+ first_in_log_index = find_index_in_dict(logs, "log_type", "IN")
+ first_in_log = (
+ logs[first_in_log_index] if first_in_log_index or first_in_log_index == 0 else None
+ )
+ last_out_log_index = find_index_in_dict(reversed(logs), "log_type", "OUT")
+ last_out_log = (
+ logs[len(logs) - 1 - last_out_log_index]
+ if last_out_log_index or last_out_log_index == 0
+ else None
+ )
if first_in_log and last_out_log:
in_time, out_time = first_in_log.time, last_out_log.time
total_hours = time_diff_in_hours(in_time, out_time)
- elif working_hours_calc_type == 'Every Valid Check-in and Check-out':
+ elif working_hours_calc_type == "Every Valid Check-in and Check-out":
in_log = out_log = None
for log in logs:
if in_log and out_log:
@@ -165,16 +224,44 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
total_hours += time_diff_in_hours(in_log.time, out_log.time)
in_log = out_log = None
if not in_log:
- in_log = log if log.log_type == 'IN' else None
+ in_log = log if log.log_type == "IN" else None
elif not out_log:
- out_log = log if log.log_type == 'OUT' else None
+ out_log = log if log.log_type == "OUT" else None
if in_log and out_log:
out_time = out_log.time
total_hours += time_diff_in_hours(in_log.time, out_log.time)
return total_hours, in_time, out_time
+
def time_diff_in_hours(start, end):
- return round((end-start).total_seconds() / 3600, 1)
+ return round((end - start).total_seconds() / 3600, 1)
+
def find_index_in_dict(dict_list, key, value):
return next((index for (index, d) in enumerate(dict_list) if d[key] == value), None)
+
+
+def add_comment_in_checkins(log_names, duplicate):
+ text = _("Auto Attendance skipped due to duplicate attendance record: {}").format(
+ get_link_to_form("Attendance", duplicate)
+ )
+
+ for name in log_names:
+ frappe.get_doc(
+ {
+ "doctype": "Comment",
+ "comment_type": "Comment",
+ "reference_doctype": "Employee Checkin",
+ "reference_name": name,
+ "content": text,
+ }
+ ).insert(ignore_permissions=True)
+
+
+def skip_attendance_in_checkins(log_names):
+ EmployeeCheckin = frappe.qb.DocType("Employee Checkin")
+ (
+ frappe.qb.update(EmployeeCheckin)
+ .set("skip_auto_attendance", 1)
+ .where(EmployeeCheckin.name.isin(log_names))
+ ).run()
diff --git a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py
index 254bf9e2569..97f76b03502 100644
--- a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py
+++ b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py
@@ -19,85 +19,108 @@ class TestEmployeeCheckin(unittest.TestCase):
def test_add_log_based_on_employee_field(self):
employee = make_employee("test_add_log_based_on_employee_field@example.com")
employee = frappe.get_doc("Employee", employee)
- employee.attendance_device_id = '3344'
+ employee.attendance_device_id = "3344"
employee.save()
time_now = now_datetime().__str__()[:-7]
- employee_checkin = add_log_based_on_employee_field('3344', time_now, 'mumbai_first_floor', 'IN')
+ employee_checkin = add_log_based_on_employee_field("3344", time_now, "mumbai_first_floor", "IN")
self.assertEqual(employee_checkin.employee, employee.name)
self.assertEqual(employee_checkin.time, time_now)
- self.assertEqual(employee_checkin.device_id, 'mumbai_first_floor')
- self.assertEqual(employee_checkin.log_type, 'IN')
+ self.assertEqual(employee_checkin.device_id, "mumbai_first_floor")
+ self.assertEqual(employee_checkin.log_type, "IN")
def test_mark_attendance_and_link_log(self):
employee = make_employee("test_mark_attendance_and_link_log@example.com")
logs = make_n_checkins(employee, 3)
- mark_attendance_and_link_log(logs, 'Skip', nowdate())
+ mark_attendance_and_link_log(logs, "Skip", nowdate())
log_names = [log.name for log in logs]
- logs_count = frappe.db.count('Employee Checkin', {'name':['in', log_names], 'skip_auto_attendance':1})
+ logs_count = frappe.db.count(
+ "Employee Checkin", {"name": ["in", log_names], "skip_auto_attendance": 1}
+ )
self.assertEqual(logs_count, 3)
logs = make_n_checkins(employee, 4, 2)
now_date = nowdate()
- frappe.db.delete('Attendance', {'employee':employee})
- attendance = mark_attendance_and_link_log(logs, 'Present', now_date, 8.2)
+ frappe.db.delete("Attendance", {"employee": employee})
+ attendance = mark_attendance_and_link_log(logs, "Present", now_date, 8.2)
log_names = [log.name for log in logs]
- logs_count = frappe.db.count('Employee Checkin', {'name':['in', log_names], 'attendance':attendance.name})
+ logs_count = frappe.db.count(
+ "Employee Checkin", {"name": ["in", log_names], "attendance": attendance.name}
+ )
self.assertEqual(logs_count, 4)
- attendance_count = frappe.db.count('Attendance', {'status':'Present', 'working_hours':8.2,
- 'employee':employee, 'attendance_date':now_date})
+ attendance_count = frappe.db.count(
+ "Attendance",
+ {"status": "Present", "working_hours": 8.2, "employee": employee, "attendance_date": now_date},
+ )
self.assertEqual(attendance_count, 1)
def test_calculate_working_hours(self):
- check_in_out_type = ['Alternating entries as IN and OUT during the same shift',
- 'Strictly based on Log Type in Employee Checkin']
- working_hours_calc_type = ['First Check-in and Last Check-out',
- 'Every Valid Check-in and Check-out']
+ check_in_out_type = [
+ "Alternating entries as IN and OUT during the same shift",
+ "Strictly based on Log Type in Employee Checkin",
+ ]
+ working_hours_calc_type = [
+ "First Check-in and Last Check-out",
+ "Every Valid Check-in and Check-out",
+ ]
logs_type_1 = [
- {'time':now_datetime()-timedelta(minutes=390)},
- {'time':now_datetime()-timedelta(minutes=300)},
- {'time':now_datetime()-timedelta(minutes=270)},
- {'time':now_datetime()-timedelta(minutes=90)},
- {'time':now_datetime()-timedelta(minutes=0)}
- ]
+ {"time": now_datetime() - timedelta(minutes=390)},
+ {"time": now_datetime() - timedelta(minutes=300)},
+ {"time": now_datetime() - timedelta(minutes=270)},
+ {"time": now_datetime() - timedelta(minutes=90)},
+ {"time": now_datetime() - timedelta(minutes=0)},
+ ]
logs_type_2 = [
- {'time':now_datetime()-timedelta(minutes=390),'log_type':'OUT'},
- {'time':now_datetime()-timedelta(minutes=360),'log_type':'IN'},
- {'time':now_datetime()-timedelta(minutes=300),'log_type':'OUT'},
- {'time':now_datetime()-timedelta(minutes=290),'log_type':'IN'},
- {'time':now_datetime()-timedelta(minutes=260),'log_type':'OUT'},
- {'time':now_datetime()-timedelta(minutes=240),'log_type':'IN'},
- {'time':now_datetime()-timedelta(minutes=150),'log_type':'IN'},
- {'time':now_datetime()-timedelta(minutes=60),'log_type':'OUT'}
- ]
+ {"time": now_datetime() - timedelta(minutes=390), "log_type": "OUT"},
+ {"time": now_datetime() - timedelta(minutes=360), "log_type": "IN"},
+ {"time": now_datetime() - timedelta(minutes=300), "log_type": "OUT"},
+ {"time": now_datetime() - timedelta(minutes=290), "log_type": "IN"},
+ {"time": now_datetime() - timedelta(minutes=260), "log_type": "OUT"},
+ {"time": now_datetime() - timedelta(minutes=240), "log_type": "IN"},
+ {"time": now_datetime() - timedelta(minutes=150), "log_type": "IN"},
+ {"time": now_datetime() - timedelta(minutes=60), "log_type": "OUT"},
+ ]
logs_type_1 = [frappe._dict(x) for x in logs_type_1]
logs_type_2 = [frappe._dict(x) for x in logs_type_2]
- working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[0])
+ working_hours = calculate_working_hours(
+ logs_type_1, check_in_out_type[0], working_hours_calc_type[0]
+ )
self.assertEqual(working_hours, (6.5, logs_type_1[0].time, logs_type_1[-1].time))
- working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[1])
+ working_hours = calculate_working_hours(
+ logs_type_1, check_in_out_type[0], working_hours_calc_type[1]
+ )
self.assertEqual(working_hours, (4.5, logs_type_1[0].time, logs_type_1[-1].time))
- working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[0])
+ working_hours = calculate_working_hours(
+ logs_type_2, check_in_out_type[1], working_hours_calc_type[0]
+ )
self.assertEqual(working_hours, (5, logs_type_2[1].time, logs_type_2[-1].time))
- working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[1])
+ working_hours = calculate_working_hours(
+ logs_type_2, check_in_out_type[1], working_hours_calc_type[1]
+ )
self.assertEqual(working_hours, (4.5, logs_type_2[1].time, logs_type_2[-1].time))
+
def make_n_checkins(employee, n, hours_to_reverse=1):
- logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n+1))]
- for i in range(n-1):
- logs.append(make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n-i)))
+ logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n + 1))]
+ for i in range(n - 1):
+ logs.append(
+ make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n - i))
+ )
return logs
def make_checkin(employee, time=now_datetime()):
- log = frappe.get_doc({
- "doctype": "Employee Checkin",
- "employee" : employee,
- "time" : time,
- "device_id" : "device1",
- "log_type" : "IN"
- }).insert()
+ log = frappe.get_doc(
+ {
+ "doctype": "Employee Checkin",
+ "employee": employee,
+ "time": time,
+ "device_id": "device1",
+ "log_type": "IN",
+ }
+ ).insert()
return log
diff --git a/erpnext/hr/doctype/employee_grade/employee_grade_dashboard.py b/erpnext/hr/doctype/employee_grade/employee_grade_dashboard.py
index 1dd6ad3b15a..efc68ce87a3 100644
--- a/erpnext/hr/doctype/employee_grade/employee_grade_dashboard.py
+++ b/erpnext/hr/doctype/employee_grade/employee_grade_dashboard.py
@@ -1,13 +1,9 @@
-
-
def get_data():
return {
- 'transactions': [
+ "transactions": [
{
- 'items': ['Employee', 'Leave Period'],
+ "items": ["Employee", "Leave Period"],
},
- {
- 'items': ['Employee Onboarding Template', 'Employee Separation Template']
- }
+ {"items": ["Employee Onboarding Template", "Employee Separation Template"]},
]
}
diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.py b/erpnext/hr/doctype/employee_grievance/employee_grievance.py
index fd9a33b3771..45de79f4f5e 100644
--- a/erpnext/hr/doctype/employee_grievance/employee_grievance.py
+++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.py
@@ -9,7 +9,8 @@ from frappe.model.document import Document
class EmployeeGrievance(Document):
def on_submit(self):
if self.status not in ["Invalid", "Resolved"]:
- frappe.throw(_("Only Employee Grievance with status {0} or {1} can be submitted").format(
- bold("Invalid"),
- bold("Resolved"))
+ frappe.throw(
+ _("Only Employee Grievance with status {0} or {1} can be submitted").format(
+ bold("Invalid"), bold("Resolved")
+ )
)
diff --git a/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py
index e2d0002aa62..910d8828603 100644
--- a/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py
+++ b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py
@@ -13,6 +13,7 @@ class TestEmployeeGrievance(unittest.TestCase):
def test_create_employee_grievance(self):
create_employee_grievance()
+
def create_employee_grievance():
grievance_type = create_grievance_type()
emp_1 = make_employee("test_emp_grievance_@example.com", company="_Test Company")
@@ -27,10 +28,10 @@ def create_employee_grievance():
grievance.grievance_against = emp_2
grievance.description = "test descrip"
- #set cause
+ # set cause
grievance.cause_of_grievance = "test cause"
- #resolution details
+ # resolution details
grievance.resolution_date = today()
grievance.resolution_detail = "test resolution detail"
grievance.resolved_by = "test_emp_grievance_@example.com"
diff --git a/erpnext/hr/doctype/employee_group/test_employee_group.py b/erpnext/hr/doctype/employee_group/test_employee_group.py
index a87f4007bd8..3922f54f331 100644
--- a/erpnext/hr/doctype/employee_group/test_employee_group.py
+++ b/erpnext/hr/doctype/employee_group/test_employee_group.py
@@ -11,17 +11,16 @@ from erpnext.hr.doctype.employee.test_employee import make_employee
class TestEmployeeGroup(unittest.TestCase):
pass
+
def make_employee_group():
employee = make_employee("testemployee@example.com")
- employee_group = frappe.get_doc({
- "doctype": "Employee Group",
- "employee_group_name": "_Test Employee Group",
- "employee_list": [
- {
- "employee": employee
- }
- ]
- })
+ employee_group = frappe.get_doc(
+ {
+ "doctype": "Employee Group",
+ "employee_group_name": "_Test Employee Group",
+ "employee_list": [{"employee": employee}],
+ }
+ )
employee_group_exist = frappe.db.exists("Employee Group", "_Test Employee Group")
if not employee_group_exist:
employee_group.insert()
@@ -29,6 +28,7 @@ def make_employee_group():
else:
return employee_group_exist
+
def get_employee_group():
employee_group = frappe.db.exists("Employee Group", "_Test Employee Group")
return employee_group
diff --git a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py
index 01a9fe29a98..14b75bef569 100644
--- a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py
@@ -9,7 +9,9 @@ from frappe.model.mapper import get_mapped_doc
from erpnext.hr.utils import EmployeeBoardingController
-class IncompleteTaskError(frappe.ValidationError): pass
+class IncompleteTaskError(frappe.ValidationError):
+ pass
+
class EmployeeOnboarding(EmployeeBoardingController):
def validate(self):
@@ -17,9 +19,13 @@ class EmployeeOnboarding(EmployeeBoardingController):
self.validate_duplicate_employee_onboarding()
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:
- 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} is already for Job Applicant: {1}").format(
+ frappe.bold(emp_onboarding), frappe.bold(self.job_applicant)
+ )
+ )
def validate_employee_creation(self):
if self.docstatus != 1:
@@ -31,7 +37,9 @@ class EmployeeOnboarding(EmployeeBoardingController):
else:
task_status = frappe.db.get_value("Task", activity.task, "status")
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 Task for employee creation hasn't been done yet."), IncompleteTaskError
+ )
def on_submit(self):
super(EmployeeOnboarding, self).on_submit()
@@ -42,19 +50,29 @@ class EmployeeOnboarding(EmployeeBoardingController):
def on_cancel(self):
super(EmployeeOnboarding, self).on_cancel()
+
@frappe.whitelist()
def make_employee(source_name, target_doc=None):
doc = frappe.get_doc("Employee Onboarding", source_name)
doc.validate_employee_creation()
+
def set_missing_values(source, target):
target.personal_email = frappe.db.get_value("Job Applicant", source.job_applicant, "email_id")
target.status = "Active"
- doc = get_mapped_doc("Employee Onboarding", source_name, {
+
+ doc = get_mapped_doc(
+ "Employee Onboarding",
+ source_name,
+ {
"Employee Onboarding": {
"doctype": "Employee",
"field_map": {
"first_name": "employee_name",
"employee_grade": "grade",
- }}
- }, target_doc, set_missing_values)
+ },
+ }
+ },
+ target_doc,
+ set_missing_values,
+ )
return doc
diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
index daa068e6e03..21d00ce2de7 100644
--- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
@@ -15,8 +15,8 @@ from erpnext.hr.doctype.job_offer.test_job_offer import create_job_offer
class TestEmployeeOnboarding(unittest.TestCase):
def test_employee_onboarding_incomplete_task(self):
- if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}):
- frappe.delete_doc('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.db.sql("delete from `tabEmployee Onboarding`")
project = "Employee Onboarding : test@researcher.com"
frappe.db.sql("delete from tabProject where name=%s", project)
@@ -26,35 +26,31 @@ class TestEmployeeOnboarding(unittest.TestCase):
job_offer = create_job_offer(job_applicant=applicant.name)
job_offer.submit()
- onboarding = frappe.new_doc('Employee Onboarding')
+ 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.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")
- self.assertEqual(project_name, 'Employee Onboarding : test@researcher.com')
+ self.assertEqual(project_name, "Employee Onboarding : test@researcher.com")
# don't allow making employee if onboarding is not complete
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
# complete the task
- project = frappe.get_doc('Project', onboarding.project)
- for task in frappe.get_all('Task', dict(project=project.name)):
- task = frappe.get_doc('Task', task.name)
- task.status = 'Completed'
+ project = frappe.get_doc("Project", onboarding.project)
+ for task in frappe.get_all("Task", dict(project=project.name)):
+ task = frappe.get_doc("Task", task.name)
+ task.status = "Completed"
task.save()
# make employee
@@ -62,23 +58,25 @@ class TestEmployeeOnboarding(unittest.TestCase):
employee = make_employee(onboarding.name)
employee.first_name = employee.employee_name
employee.date_of_joining = nowdate()
- employee.date_of_birth = '1990-05-08'
- employee.gender = 'Female'
+ employee.date_of_birth = "1990-05-08"
+ employee.gender = "Female"
employee.insert()
- self.assertEqual(employee.employee_name, 'Test Researcher')
+ self.assertEqual(employee.employee_name, "Test Researcher")
+
def get_job_applicant():
- if frappe.db.exists('Job Applicant', 'test@researcher.com'):
- return frappe.get_doc('Job Applicant', 'test@researcher.com')
- applicant = frappe.new_doc('Job Applicant')
- applicant.applicant_name = 'Test Researcher'
- applicant.email_id = 'test@researcher.com'
- applicant.designation = 'Researcher'
- applicant.status = 'Open'
- applicant.cover_letter = 'I am a great Researcher.'
+ if frappe.db.exists("Job Applicant", "test@researcher.com"):
+ return frappe.get_doc("Job Applicant", "test@researcher.com")
+ applicant = frappe.new_doc("Job Applicant")
+ applicant.applicant_name = "Test Researcher"
+ applicant.email_id = "test@researcher.com"
+ applicant.designation = "Researcher"
+ applicant.status = "Open"
+ applicant.cover_letter = "I am a great Researcher."
applicant.insert()
return applicant
+
def _set_up():
for doctype in ["Employee Onboarding"]:
frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype))
diff --git a/erpnext/hr/doctype/employee_onboarding_template/employee_onboarding_template_dashboard.py b/erpnext/hr/doctype/employee_onboarding_template/employee_onboarding_template_dashboard.py
index 48f2c1d2709..93237ee3797 100644
--- a/erpnext/hr/doctype/employee_onboarding_template/employee_onboarding_template_dashboard.py
+++ b/erpnext/hr/doctype/employee_onboarding_template/employee_onboarding_template_dashboard.py
@@ -1,11 +1,7 @@
-
-
def get_data():
- return {
- 'fieldname': 'employee_onboarding_template',
- 'transactions': [
- {
- 'items': ['Employee Onboarding']
- },
- ],
- }
+ return {
+ "fieldname": "employee_onboarding_template",
+ "transactions": [
+ {"items": ["Employee Onboarding"]},
+ ],
+ }
diff --git a/erpnext/hr/doctype/employee_promotion/employee_promotion.py b/erpnext/hr/doctype/employee_promotion/employee_promotion.py
index cf6156e3264..d77c1dddfd0 100644
--- a/erpnext/hr/doctype/employee_promotion/employee_promotion.py
+++ b/erpnext/hr/doctype/employee_promotion/employee_promotion.py
@@ -16,12 +16,16 @@ class EmployeePromotion(Document):
def before_submit(self):
if getdate(self.promotion_date) > getdate():
- frappe.throw(_("Employee Promotion cannot be submitted before Promotion Date"),
- frappe.DocstatusTransitionError)
+ frappe.throw(
+ _("Employee Promotion cannot be submitted before Promotion Date"),
+ frappe.DocstatusTransitionError,
+ )
def on_submit(self):
employee = frappe.get_doc("Employee", self.employee)
- employee = update_employee_work_history(employee, self.promotion_details, date=self.promotion_date)
+ employee = update_employee_work_history(
+ employee, self.promotion_details, date=self.promotion_date
+ )
employee.save()
def on_cancel(self):
diff --git a/erpnext/hr/doctype/employee_promotion/test_employee_promotion.py b/erpnext/hr/doctype/employee_promotion/test_employee_promotion.py
index fc9d195a3f3..06825ece910 100644
--- a/erpnext/hr/doctype/employee_promotion/test_employee_promotion.py
+++ b/erpnext/hr/doctype/employee_promotion/test_employee_promotion.py
@@ -15,18 +15,20 @@ class TestEmployeePromotion(unittest.TestCase):
frappe.db.sql("""delete from `tabEmployee Promotion`""")
def test_submit_before_promotion_date(self):
- promotion_obj = frappe.get_doc({
- "doctype": "Employee Promotion",
- "employee": self.employee,
- "promotion_details" :[
- {
- "property": "Designation",
- "current": "Software Developer",
- "new": "Project Manager",
- "fieldname": "designation"
- }
- ]
- })
+ promotion_obj = frappe.get_doc(
+ {
+ "doctype": "Employee Promotion",
+ "employee": self.employee,
+ "promotion_details": [
+ {
+ "property": "Designation",
+ "current": "Software Developer",
+ "new": "Project Manager",
+ "fieldname": "designation",
+ }
+ ],
+ }
+ )
promotion_obj.promotion_date = add_days(getdate(), 1)
promotion_obj.save()
self.assertRaises(frappe.DocstatusTransitionError, promotion_obj.submit)
diff --git a/erpnext/hr/doctype/employee_referral/employee_referral.py b/erpnext/hr/doctype/employee_referral/employee_referral.py
index 4e1780b9978..e349c674d8b 100644
--- a/erpnext/hr/doctype/employee_referral/employee_referral.py
+++ b/erpnext/hr/doctype/employee_referral/employee_referral.py
@@ -30,7 +30,7 @@ class EmployeeReferral(Document):
@frappe.whitelist()
def create_job_applicant(source_name, target_doc=None):
emp_ref = frappe.get_doc("Employee Referral", source_name)
- #just for Api call if some set status apart from default Status
+ # just for Api call if some set status apart from default Status
status = emp_ref.status
if emp_ref.status in ["Pending", "In process"]:
status = "Open"
@@ -47,9 +47,13 @@ def create_job_applicant(source_name, target_doc=None):
job_applicant.resume_link = emp_ref.resume_link
job_applicant.save()
- frappe.msgprint(_("Job Applicant {0} created successfully.").format(
- get_link_to_form("Job Applicant", job_applicant.name)),
- title=_("Success"), indicator="green")
+ frappe.msgprint(
+ _("Job Applicant {0} created successfully.").format(
+ get_link_to_form("Job Applicant", job_applicant.name)
+ ),
+ title=_("Success"),
+ indicator="green",
+ )
emp_ref.db_set("status", "In Process")
diff --git a/erpnext/hr/doctype/employee_referral/employee_referral_dashboard.py b/erpnext/hr/doctype/employee_referral/employee_referral_dashboard.py
index 1733ac9726e..4d683fbfcf3 100644
--- a/erpnext/hr/doctype/employee_referral/employee_referral_dashboard.py
+++ b/erpnext/hr/doctype/employee_referral/employee_referral_dashboard.py
@@ -1,15 +1,8 @@
-
-
def get_data():
return {
- 'fieldname': 'employee_referral',
- 'non_standard_fieldnames': {
- 'Additional Salary': 'ref_docname'
- },
- 'transactions': [
- {
- 'items': ['Job Applicant', 'Additional Salary']
- },
-
- ]
+ "fieldname": "employee_referral",
+ "non_standard_fieldnames": {"Additional Salary": "ref_docname"},
+ "transactions": [
+ {"items": ["Job Applicant", "Additional Salary"]},
+ ],
}
diff --git a/erpnext/hr/doctype/employee_referral/test_employee_referral.py b/erpnext/hr/doctype/employee_referral/test_employee_referral.py
index 529e3551454..475a935e864 100644
--- a/erpnext/hr/doctype/employee_referral/test_employee_referral.py
+++ b/erpnext/hr/doctype/employee_referral/test_employee_referral.py
@@ -15,7 +15,6 @@ from erpnext.hr.doctype.employee_referral.employee_referral import (
class TestEmployeeReferral(unittest.TestCase):
-
def setUp(self):
frappe.db.sql("DELETE FROM `tabJob Applicant`")
frappe.db.sql("DELETE FROM `tabEmployee Referral`")
@@ -23,13 +22,12 @@ class TestEmployeeReferral(unittest.TestCase):
def test_workflow_and_status_sync(self):
emp_ref = create_employee_referral()
- #Check Initial status
+ # Check Initial status
self.assertTrue(emp_ref.status, "Pending")
job_applicant = create_job_applicant(emp_ref.name)
-
- #Check status sync
+ # Check status sync
emp_ref.reload()
self.assertTrue(emp_ref.status, "In Process")
@@ -47,7 +45,6 @@ class TestEmployeeReferral(unittest.TestCase):
emp_ref.reload()
self.assertTrue(emp_ref.status, "Accepted")
-
# Check for Referral reference in additional salary
add_sal = create_additional_salary(emp_ref)
diff --git a/erpnext/hr/doctype/employee_separation/test_employee_separation.py b/erpnext/hr/doctype/employee_separation/test_employee_separation.py
index 0007b9e1f38..5ba57bad899 100644
--- a/erpnext/hr/doctype/employee_separation/test_employee_separation.py
+++ b/erpnext/hr/doctype/employee_separation/test_employee_separation.py
@@ -7,17 +7,15 @@ import frappe
test_dependencies = ["Employee Onboarding"]
+
class TestEmployeeSeparation(unittest.TestCase):
def test_employee_separation(self):
employee = frappe.db.get_value("Employee", {"status": "Active"})
- separation = frappe.new_doc('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.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)
diff --git a/erpnext/hr/doctype/employee_separation_template/employee_separation_template_dashboard.py b/erpnext/hr/doctype/employee_separation_template/employee_separation_template_dashboard.py
index f165d0a0eb4..3ffd8dd6e2c 100644
--- a/erpnext/hr/doctype/employee_separation_template/employee_separation_template_dashboard.py
+++ b/erpnext/hr/doctype/employee_separation_template/employee_separation_template_dashboard.py
@@ -1,11 +1,7 @@
-
-
def get_data():
- return {
- 'fieldname': 'employee_separation_template',
- 'transactions': [
- {
- 'items': ['Employee Separation']
- },
- ],
- }
+ return {
+ "fieldname": "employee_separation_template",
+ "transactions": [
+ {"items": ["Employee Separation"]},
+ ],
+ }
diff --git a/erpnext/hr/doctype/employee_transfer/employee_transfer.py b/erpnext/hr/doctype/employee_transfer/employee_transfer.py
index f927d413ae3..6dbefe59da5 100644
--- a/erpnext/hr/doctype/employee_transfer/employee_transfer.py
+++ b/erpnext/hr/doctype/employee_transfer/employee_transfer.py
@@ -13,8 +13,10 @@ from erpnext.hr.utils import update_employee_work_history
class EmployeeTransfer(Document):
def before_submit(self):
if getdate(self.transfer_date) > getdate():
- frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date"),
- frappe.DocstatusTransitionError)
+ frappe.throw(
+ _("Employee Transfer cannot be submitted before Transfer Date"),
+ frappe.DocstatusTransitionError,
+ )
def on_submit(self):
employee = frappe.get_doc("Employee", self.employee)
@@ -22,22 +24,26 @@ class EmployeeTransfer(Document):
new_employee = frappe.copy_doc(employee)
new_employee.name = None
new_employee.employee_number = None
- new_employee = update_employee_work_history(new_employee, self.transfer_details, date=self.transfer_date)
+ new_employee = update_employee_work_history(
+ new_employee, self.transfer_details, date=self.transfer_date
+ )
if self.new_company and self.company != self.new_company:
new_employee.internal_work_history = []
new_employee.date_of_joining = self.transfer_date
new_employee.company = self.new_company
- #move user_id to new employee before insert
+ # move user_id to new employee before insert
if employee.user_id and not self.validate_user_in_details():
new_employee.user_id = employee.user_id
employee.db_set("user_id", "")
new_employee.insert()
self.db_set("new_employee_id", new_employee.name)
- #relieve the old employee
+ # relieve the old employee
employee.db_set("relieving_date", self.transfer_date)
employee.db_set("status", "Left")
else:
- employee = update_employee_work_history(employee, self.transfer_details, date=self.transfer_date)
+ employee = update_employee_work_history(
+ employee, self.transfer_details, date=self.transfer_date
+ )
if self.new_company and self.company != self.new_company:
employee.company = self.new_company
employee.date_of_joining = self.transfer_date
@@ -47,14 +53,18 @@ class EmployeeTransfer(Document):
employee = frappe.get_doc("Employee", self.employee)
if self.create_new_employee_id:
if self.new_employee_id:
- frappe.throw(_("Please delete the Employee {0} to cancel this document").format(
- "{0}".format(self.new_employee_id)
- ))
- #mark the employee as active
+ frappe.throw(
+ _("Please delete the Employee {0} to cancel this document").format(
+ "{0}".format(self.new_employee_id)
+ )
+ )
+ # mark the employee as active
employee.status = "Active"
- employee.relieving_date = ''
+ employee.relieving_date = ""
else:
- employee = update_employee_work_history(employee, self.transfer_details, date=self.transfer_date, cancel=True)
+ employee = update_employee_work_history(
+ employee, self.transfer_details, date=self.transfer_date, cancel=True
+ )
if self.new_company != self.company:
employee.company = self.company
employee.save()
diff --git a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
index 64eee402fec..37a190a1627 100644
--- a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
+++ b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
@@ -19,18 +19,20 @@ class TestEmployeeTransfer(unittest.TestCase):
def test_submit_before_transfer_date(self):
make_employee("employee2@transfers.com")
- transfer_obj = frappe.get_doc({
- "doctype": "Employee Transfer",
- "employee": frappe.get_value("Employee", {"user_id":"employee2@transfers.com"}, "name"),
- "transfer_details" :[
- {
- "property": "Designation",
- "current": "Software Developer",
- "new": "Project Manager",
- "fieldname": "designation"
- }
- ]
- })
+ transfer_obj = frappe.get_doc(
+ {
+ "doctype": "Employee Transfer",
+ "employee": frappe.get_value("Employee", {"user_id": "employee2@transfers.com"}, "name"),
+ "transfer_details": [
+ {
+ "property": "Designation",
+ "current": "Software Developer",
+ "new": "Project Manager",
+ "fieldname": "designation",
+ }
+ ],
+ }
+ )
transfer_obj.transfer_date = add_days(getdate(), 1)
transfer_obj.save()
self.assertRaises(frappe.DocstatusTransitionError, transfer_obj.submit)
@@ -42,32 +44,35 @@ class TestEmployeeTransfer(unittest.TestCase):
def test_new_employee_creation(self):
make_employee("employee3@transfers.com")
- transfer = frappe.get_doc({
- "doctype": "Employee Transfer",
- "employee": frappe.get_value("Employee", {"user_id":"employee3@transfers.com"}, "name"),
- "create_new_employee_id": 1,
- "transfer_date": getdate(),
- "transfer_details" :[
- {
- "property": "Designation",
- "current": "Software Developer",
- "new": "Project Manager",
- "fieldname": "designation"
- }
- ]
- }).insert()
+ transfer = frappe.get_doc(
+ {
+ "doctype": "Employee Transfer",
+ "employee": frappe.get_value("Employee", {"user_id": "employee3@transfers.com"}, "name"),
+ "create_new_employee_id": 1,
+ "transfer_date": getdate(),
+ "transfer_details": [
+ {
+ "property": "Designation",
+ "current": "Software Developer",
+ "new": "Project Manager",
+ "fieldname": "designation",
+ }
+ ],
+ }
+ ).insert()
transfer.submit()
self.assertTrue(transfer.new_employee_id)
self.assertEqual(frappe.get_value("Employee", transfer.new_employee_id, "status"), "Active")
self.assertEqual(frappe.get_value("Employee", transfer.employee, "status"), "Left")
def test_employee_history(self):
- employee = make_employee("employee4@transfers.com",
+ employee = make_employee(
+ "employee4@transfers.com",
company="Test Company",
date_of_birth=getdate("30-09-1980"),
date_of_joining=getdate("01-10-2021"),
department="Accounts - TC",
- designation="Accountant"
+ designation="Accountant",
)
transfer = create_employee_transfer(employee)
@@ -94,36 +99,40 @@ class TestEmployeeTransfer(unittest.TestCase):
def create_company():
if not frappe.db.exists("Company", "Test Company"):
- frappe.get_doc({
- "doctype": "Company",
- "company_name": "Test Company",
- "default_currency": "INR",
- "country": "India"
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Company",
+ "company_name": "Test Company",
+ "default_currency": "INR",
+ "country": "India",
+ }
+ ).insert()
def create_employee_transfer(employee):
- doc = frappe.get_doc({
- "doctype": "Employee Transfer",
- "employee": employee,
- "transfer_date": getdate(),
- "transfer_details": [
- {
- "property": "Designation",
- "current": "Accountant",
- "new": "Manager",
- "fieldname": "designation"
- },
- {
- "property": "Department",
- "current": "Accounts - TC",
- "new": "Management - TC",
- "fieldname": "department"
- }
- ]
- })
+ doc = frappe.get_doc(
+ {
+ "doctype": "Employee Transfer",
+ "employee": employee,
+ "transfer_date": getdate(),
+ "transfer_details": [
+ {
+ "property": "Designation",
+ "current": "Accountant",
+ "new": "Manager",
+ "fieldname": "designation",
+ },
+ {
+ "property": "Department",
+ "current": "Accounts - TC",
+ "new": "Management - TC",
+ "fieldname": "department",
+ },
+ ],
+ }
+ )
doc.save()
doc.submit()
- return doc
\ No newline at end of file
+ return doc
diff --git a/erpnext/hr/doctype/employment_type/test_employment_type.py b/erpnext/hr/doctype/employment_type/test_employment_type.py
index c43f9636c70..fdf6965e48a 100644
--- a/erpnext/hr/doctype/employment_type/test_employment_type.py
+++ b/erpnext/hr/doctype/employment_type/test_employment_type.py
@@ -3,4 +3,4 @@
import frappe
-test_records = frappe.get_test_records('Employment Type')
+test_records = frappe.get_test_records("Employment Type")
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py
index 7e3898b7d51..311a1eb81c8 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.py
@@ -13,20 +13,26 @@ from erpnext.controllers.accounts_controller import AccountsController
from erpnext.hr.utils import set_employee_name, share_doc_with_approver, validate_active_employee
-class InvalidExpenseApproverError(frappe.ValidationError): pass
-class ExpenseApproverIdentityError(frappe.ValidationError): pass
+class InvalidExpenseApproverError(frappe.ValidationError):
+ pass
+
+
+class ExpenseApproverIdentityError(frappe.ValidationError):
+ pass
+
class ExpenseClaim(AccountsController):
def onload(self):
- self.get("__onload").make_payment_via_journal_entry = frappe.db.get_single_value('Accounts Settings',
- 'make_payment_via_journal_entry')
+ self.get("__onload").make_payment_via_journal_entry = frappe.db.get_single_value(
+ "Accounts Settings", "make_payment_via_journal_entry"
+ )
def validate(self):
validate_active_employee(self.employee)
- self.validate_advances()
+ set_employee_name(self)
self.validate_sanctioned_amount()
self.calculate_total_amount()
- set_employee_name(self)
+ self.validate_advances()
self.set_expense_account(validate=True)
self.set_payable_account()
self.set_cost_center()
@@ -36,21 +42,35 @@ class ExpenseClaim(AccountsController):
self.project = frappe.db.get_value("Task", self.task, "project")
def set_status(self, update=False):
- status = {
- "0": "Draft",
- "1": "Submitted",
- "2": "Cancelled"
- }[cstr(self.docstatus or 0)]
+ status = {"0": "Draft", "1": "Submitted", "2": "Cancelled"}[cstr(self.docstatus or 0)]
- paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount)
precision = self.precision("grand_total")
- 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 self.approval_status == 'Approved':
+
+ if (
+ # set as paid
+ self.is_paid
+ or (
+ flt(self.total_sanctioned_amount > 0)
+ and (
+ # grand total is reimbursed
+ (
+ self.docstatus == 1
+ and flt(self.grand_total, precision) == flt(self.total_amount_reimbursed, precision)
+ )
+ # grand total (to be paid) is 0 since linked advances already cover the claimed amount
+ or (flt(self.grand_total, precision) == 0)
+ )
+ )
+ ) and self.approval_status == "Approved":
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"
+ ):
status = "Unpaid"
- elif self.docstatus == 1 and self.approval_status == 'Rejected':
- status = 'Rejected'
+ elif self.docstatus == 1 and self.approval_status == "Rejected":
+ status = "Rejected"
if update:
self.db_set("status", status)
@@ -62,14 +82,16 @@ class ExpenseClaim(AccountsController):
def set_payable_account(self):
if not self.payable_account and not self.is_paid:
- self.payable_account = frappe.get_cached_value('Company', self.company, 'default_expense_claim_payable_account')
+ self.payable_account = frappe.get_cached_value(
+ "Company", self.company, "default_expense_claim_payable_account"
+ )
def set_cost_center(self):
if not self.cost_center:
- self.cost_center = frappe.get_cached_value('Company', self.company, 'cost_center')
+ self.cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
def on_submit(self):
- if self.approval_status=="Draft":
+ if self.approval_status == "Draft":
frappe.throw(_("""Approval Status must be 'Approved' or 'Rejected'"""))
self.update_task_and_project()
@@ -83,7 +105,7 @@ class ExpenseClaim(AccountsController):
def on_cancel(self):
self.update_task_and_project()
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
if self.payable_account:
self.make_gl_entries(cancel=True)
@@ -114,43 +136,51 @@ class ExpenseClaim(AccountsController):
# payable entry
if self.grand_total:
gl_entry.append(
- self.get_gl_dict({
- "account": self.payable_account,
- "credit": self.grand_total,
- "credit_in_account_currency": self.grand_total,
- "against": ",".join([d.default_account for d in self.expenses]),
- "party_type": "Employee",
- "party": self.employee,
- "against_voucher_type": self.doctype,
- "against_voucher": self.name,
- "cost_center": self.cost_center
- }, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.payable_account,
+ "credit": self.grand_total,
+ "credit_in_account_currency": self.grand_total,
+ "against": ",".join([d.default_account for d in self.expenses]),
+ "party_type": "Employee",
+ "party": self.employee,
+ "against_voucher_type": self.doctype,
+ "against_voucher": self.name,
+ "cost_center": self.cost_center,
+ },
+ item=self,
+ )
)
# expense entries
for data in self.expenses:
gl_entry.append(
- self.get_gl_dict({
- "account": data.default_account,
- "debit": data.sanctioned_amount,
- "debit_in_account_currency": data.sanctioned_amount,
- "against": self.employee,
- "cost_center": data.cost_center or self.cost_center
- }, item=data)
+ self.get_gl_dict(
+ {
+ "account": data.default_account,
+ "debit": data.sanctioned_amount,
+ "debit_in_account_currency": data.sanctioned_amount,
+ "against": self.employee,
+ "cost_center": data.cost_center or self.cost_center,
+ },
+ item=data,
+ )
)
for data in self.advances:
gl_entry.append(
- self.get_gl_dict({
- "account": data.advance_account,
- "credit": data.allocated_amount,
- "credit_in_account_currency": data.allocated_amount,
- "against": ",".join([d.default_account for d in self.expenses]),
- "party_type": "Employee",
- "party": self.employee,
- "against_voucher_type": "Employee Advance",
- "against_voucher": data.employee_advance
- })
+ self.get_gl_dict(
+ {
+ "account": data.advance_account,
+ "credit": data.allocated_amount,
+ "credit_in_account_currency": data.allocated_amount,
+ "against": ",".join([d.default_account for d in self.expenses]),
+ "party_type": "Employee",
+ "party": self.employee,
+ "against_voucher_type": "Employee Advance",
+ "against_voucher": data.employee_advance,
+ }
+ )
)
self.add_tax_gl_entries(gl_entry)
@@ -159,25 +189,31 @@ class ExpenseClaim(AccountsController):
# payment entry
payment_account = get_bank_cash_account(self.mode_of_payment, self.company).get("account")
gl_entry.append(
- self.get_gl_dict({
- "account": payment_account,
- "credit": self.grand_total,
- "credit_in_account_currency": self.grand_total,
- "against": self.employee
- }, item=self)
+ self.get_gl_dict(
+ {
+ "account": payment_account,
+ "credit": self.grand_total,
+ "credit_in_account_currency": self.grand_total,
+ "against": self.employee,
+ },
+ item=self,
+ )
)
gl_entry.append(
- self.get_gl_dict({
- "account": self.payable_account,
- "party_type": "Employee",
- "party": self.employee,
- "against": payment_account,
- "debit": self.grand_total,
- "debit_in_account_currency": self.grand_total,
- "against_voucher": self.name,
- "against_voucher_type": self.doctype,
- }, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.payable_account,
+ "party_type": "Employee",
+ "party": self.employee,
+ "against": payment_account,
+ "debit": self.grand_total,
+ "debit_in_account_currency": self.grand_total,
+ "against_voucher": self.name,
+ "against_voucher_type": self.doctype,
+ },
+ item=self,
+ )
)
return gl_entry
@@ -186,22 +222,28 @@ class ExpenseClaim(AccountsController):
# tax table gl entries
for tax in self.get("taxes"):
gl_entries.append(
- self.get_gl_dict({
- "account": tax.account_head,
- "debit": tax.tax_amount,
- "debit_in_account_currency": tax.tax_amount,
- "against": self.employee,
- "cost_center": self.cost_center,
- "against_voucher_type": self.doctype,
- "against_voucher": self.name
- }, item=tax)
+ self.get_gl_dict(
+ {
+ "account": tax.account_head,
+ "debit": tax.tax_amount,
+ "debit_in_account_currency": tax.tax_amount,
+ "against": self.employee,
+ "cost_center": self.cost_center,
+ "against_voucher_type": self.doctype,
+ "against_voucher": self.name,
+ },
+ item=tax,
+ )
)
def validate_account_details(self):
for data in self.expenses:
if not data.cost_center:
- frappe.throw(_("Row {0}: {1} is required in the expenses table to book an expense claim.")
- .format(data.idx, frappe.bold("Cost Center")))
+ frappe.throw(
+ _("Row {0}: {1} is required in the expenses table to book an expense claim.").format(
+ data.idx, frappe.bold("Cost Center")
+ )
+ )
if self.is_paid:
if not self.mode_of_payment:
@@ -210,8 +252,8 @@ class ExpenseClaim(AccountsController):
def calculate_total_amount(self):
self.total_claimed_amount = 0
self.total_sanctioned_amount = 0
- for d in self.get('expenses'):
- if self.approval_status == 'Rejected':
+ for d in self.get("expenses"):
+ if self.approval_status == "Rejected":
d.sanctioned_amount = 0.0
self.total_claimed_amount += flt(d.amount)
@@ -222,12 +264,16 @@ class ExpenseClaim(AccountsController):
self.total_taxes_and_charges = 0
for tax in self.taxes:
if tax.rate:
- tax.tax_amount = flt(self.total_sanctioned_amount) * flt(tax.rate/100)
+ tax.tax_amount = flt(self.total_sanctioned_amount) * flt(tax.rate / 100)
tax.total = flt(tax.tax_amount) + flt(self.total_sanctioned_amount)
self.total_taxes_and_charges += flt(tax.tax_amount)
- self.grand_total = flt(self.total_sanctioned_amount) + flt(self.total_taxes_and_charges) - flt(self.total_advance_amount)
+ self.grand_total = (
+ flt(self.total_sanctioned_amount)
+ + flt(self.total_taxes_and_charges)
+ - flt(self.total_advance_amount)
+ )
def update_task(self):
task = frappe.get_doc("Task", self.task)
@@ -237,16 +283,23 @@ class ExpenseClaim(AccountsController):
def validate_advances(self):
self.total_advance_amount = 0
for d in self.get("advances"):
- ref_doc = frappe.db.get_value("Employee Advance", d.employee_advance,
- ["posting_date", "paid_amount", "claimed_amount", "advance_account"], as_dict=1)
+ ref_doc = frappe.db.get_value(
+ "Employee Advance",
+ d.employee_advance,
+ ["posting_date", "paid_amount", "claimed_amount", "advance_account"],
+ as_dict=1,
+ )
d.posting_date = ref_doc.posting_date
d.advance_account = ref_doc.advance_account
d.advance_paid = ref_doc.paid_amount
d.unclaimed_amount = flt(ref_doc.paid_amount) - flt(ref_doc.claimed_amount)
if d.allocated_amount and flt(d.allocated_amount) > flt(d.unclaimed_amount):
- frappe.throw(_("Row {0}# Allocated amount {1} cannot be greater than unclaimed amount {2}")
- .format(d.idx, d.allocated_amount, d.unclaimed_amount))
+ frappe.throw(
+ _("Row {0}# Allocated amount {1} cannot be greater than unclaimed amount {2}").format(
+ d.idx, d.allocated_amount, d.unclaimed_amount
+ )
+ )
self.total_advance_amount += flt(d.allocated_amount)
@@ -255,27 +308,36 @@ class ExpenseClaim(AccountsController):
if flt(self.total_advance_amount, precision) > flt(self.total_claimed_amount, precision):
frappe.throw(_("Total advance amount cannot be greater than total claimed amount"))
- if self.total_sanctioned_amount \
- and flt(self.total_advance_amount, precision) > flt(self.total_sanctioned_amount, precision):
+ if self.total_sanctioned_amount and flt(self.total_advance_amount, precision) > flt(
+ self.total_sanctioned_amount, precision
+ ):
frappe.throw(_("Total advance amount cannot be greater than total sanctioned amount"))
def validate_sanctioned_amount(self):
- for d in self.get('expenses'):
+ for d in self.get("expenses"):
if flt(d.sanctioned_amount) > flt(d.amount):
- frappe.throw(_("Sanctioned Amount cannot be greater than Claim Amount in Row {0}.").format(d.idx))
+ frappe.throw(
+ _("Sanctioned Amount cannot be greater than Claim Amount in Row {0}.").format(d.idx)
+ )
def set_expense_account(self, validate=False):
for expense in self.expenses:
if not expense.default_account or not validate:
- expense.default_account = get_expense_claim_account(expense.expense_type, self.company)["account"]
+ expense.default_account = get_expense_claim_account(expense.expense_type, self.company)[
+ "account"
+ ]
+
def update_reimbursed_amount(doc, amount):
doc.total_amount_reimbursed += amount
- frappe.db.set_value("Expense Claim", doc.name , "total_amount_reimbursed", doc.total_amount_reimbursed)
+ frappe.db.set_value(
+ "Expense Claim", doc.name, "total_amount_reimbursed", doc.total_amount_reimbursed
+ )
doc.set_status()
- frappe.db.set_value("Expense Claim", doc.name , "status", doc.status)
+ frappe.db.set_value("Expense Claim", doc.name, "status", doc.status)
+
@frappe.whitelist()
def make_bank_entry(dt, dn):
@@ -286,96 +348,115 @@ def make_bank_entry(dt, dn):
if not default_bank_cash_account:
default_bank_cash_account = get_default_bank_cash_account(expense_claim.company, "Cash")
- payable_amount = flt(expense_claim.total_sanctioned_amount) \
- - flt(expense_claim.total_amount_reimbursed) - flt(expense_claim.total_advance_amount)
+ payable_amount = (
+ flt(expense_claim.total_sanctioned_amount)
+ - flt(expense_claim.total_amount_reimbursed)
+ - flt(expense_claim.total_advance_amount)
+ )
je = frappe.new_doc("Journal Entry")
- je.voucher_type = 'Bank Entry'
+ je.voucher_type = "Bank Entry"
je.company = expense_claim.company
- je.remark = 'Payment against Expense Claim: ' + dn
+ je.remark = "Payment against Expense Claim: " + dn
- je.append("accounts", {
- "account": expense_claim.payable_account,
- "debit_in_account_currency": payable_amount,
- "reference_type": "Expense Claim",
- "party_type": "Employee",
- "party": expense_claim.employee,
- "cost_center": erpnext.get_default_cost_center(expense_claim.company),
- "reference_name": expense_claim.name
- })
+ je.append(
+ "accounts",
+ {
+ "account": expense_claim.payable_account,
+ "debit_in_account_currency": payable_amount,
+ "reference_type": "Expense Claim",
+ "party_type": "Employee",
+ "party": expense_claim.employee,
+ "cost_center": erpnext.get_default_cost_center(expense_claim.company),
+ "reference_name": expense_claim.name,
+ },
+ )
- je.append("accounts", {
- "account": default_bank_cash_account.account,
- "credit_in_account_currency": payable_amount,
- "reference_type": "Expense Claim",
- "reference_name": expense_claim.name,
- "balance": default_bank_cash_account.balance,
- "account_currency": default_bank_cash_account.account_currency,
- "cost_center": erpnext.get_default_cost_center(expense_claim.company),
- "account_type": default_bank_cash_account.account_type
- })
+ je.append(
+ "accounts",
+ {
+ "account": default_bank_cash_account.account,
+ "credit_in_account_currency": payable_amount,
+ "reference_type": "Expense Claim",
+ "reference_name": expense_claim.name,
+ "balance": default_bank_cash_account.balance,
+ "account_currency": default_bank_cash_account.account_currency,
+ "cost_center": erpnext.get_default_cost_center(expense_claim.company),
+ "account_type": default_bank_cash_account.account_type,
+ },
+ )
return je.as_dict()
+
@frappe.whitelist()
def get_expense_claim_account_and_cost_center(expense_claim_type, company):
data = get_expense_claim_account(expense_claim_type, company)
cost_center = erpnext.get_default_cost_center(company)
- return {
- "account": data.get("account"),
- "cost_center": cost_center
- }
+ return {"account": data.get("account"), "cost_center": cost_center}
+
@frappe.whitelist()
def get_expense_claim_account(expense_claim_type, company):
- account = frappe.db.get_value("Expense Claim Account",
- {"parent": expense_claim_type, "company": company}, "default_account")
+ account = frappe.db.get_value(
+ "Expense Claim Account", {"parent": expense_claim_type, "company": company}, "default_account"
+ )
if not account:
- frappe.throw(_("Set the default account for the {0} {1}")
- .format(frappe.bold("Expense Claim Type"), get_link_to_form("Expense Claim Type", expense_claim_type)))
+ frappe.throw(
+ _("Set the default account for the {0} {1}").format(
+ frappe.bold("Expense Claim Type"), get_link_to_form("Expense Claim Type", expense_claim_type)
+ )
+ )
+
+ return {"account": account}
- return {
- "account": account
- }
@frappe.whitelist()
def get_advances(employee, advance_id=None):
if not advance_id:
- condition = 'docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount + return_amount'.format(frappe.db.escape(employee))
+ condition = "docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount + return_amount".format(
+ frappe.db.escape(employee)
+ )
else:
- condition = 'name={0}'.format(frappe.db.escape(advance_id))
+ condition = "name={0}".format(frappe.db.escape(advance_id))
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
select
name, posting_date, paid_amount, claimed_amount, advance_account
from
`tabEmployee Advance`
where {0}
- """.format(condition), as_dict=1)
+ """.format(
+ condition
+ ),
+ as_dict=1,
+ )
@frappe.whitelist()
def get_expense_claim(
- employee_name, company, employee_advance_name, posting_date, paid_amount, claimed_amount):
- default_payable_account = frappe.get_cached_value('Company', company, "default_payable_account")
- default_cost_center = frappe.get_cached_value('Company', company, 'cost_center')
+ employee_name, company, employee_advance_name, posting_date, paid_amount, claimed_amount
+):
+ default_payable_account = frappe.get_cached_value("Company", company, "default_payable_account")
+ default_cost_center = frappe.get_cached_value("Company", company, "cost_center")
- expense_claim = frappe.new_doc('Expense Claim')
+ expense_claim = frappe.new_doc("Expense Claim")
expense_claim.company = company
expense_claim.employee = employee_name
expense_claim.payable_account = default_payable_account
expense_claim.cost_center = default_cost_center
expense_claim.is_paid = 1 if flt(paid_amount) else 0
expense_claim.append(
- 'advances',
+ "advances",
{
- 'employee_advance': employee_advance_name,
- 'posting_date': posting_date,
- 'advance_paid': flt(paid_amount),
- 'unclaimed_amount': flt(paid_amount) - flt(claimed_amount),
- 'allocated_amount': flt(paid_amount) - flt(claimed_amount)
- }
+ "employee_advance": employee_advance_name,
+ "posting_date": posting_date,
+ "advance_paid": flt(paid_amount),
+ "unclaimed_amount": flt(paid_amount) - flt(claimed_amount),
+ "allocated_amount": flt(paid_amount) - flt(claimed_amount),
+ },
)
return expense_claim
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim_dashboard.py b/erpnext/hr/doctype/expense_claim/expense_claim_dashboard.py
index 44052cc8e6b..8b1acc619ee 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim_dashboard.py
+++ b/erpnext/hr/doctype/expense_claim/expense_claim_dashboard.py
@@ -1,21 +1,12 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'reference_name',
- 'internal_links': {
- 'Employee Advance': ['advances', 'employee_advance']
- },
- 'transactions': [
- {
- 'label': _('Payment'),
- 'items': ['Payment Entry', 'Journal Entry']
- },
- {
- 'label': _('Reference'),
- 'items': ['Employee Advance']
- },
- ]
+ "fieldname": "reference_name",
+ "internal_links": {"Employee Advance": ["advances", "employee_advance"]},
+ "transactions": [
+ {"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]},
+ {"label": _("Reference"), "items": ["Employee Advance"]},
+ ],
}
diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
index 46958b1ec4c..9b3d53a2105 100644
--- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
@@ -10,8 +10,8 @@ from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry
-test_dependencies = ['Employee']
-company_name = '_Test Company 3'
+test_dependencies = ["Employee"]
+company_name = "_Test Company 3"
class TestExpenseClaim(unittest.TestCase):
@@ -23,28 +23,26 @@ class TestExpenseClaim(unittest.TestCase):
frappe.db.sql("""delete from `tabProject`""")
frappe.db.sql("update `tabExpense Claim` set project = '', task = ''")
- project = frappe.get_doc({
- "project_name": "_Test Project 1",
- "doctype": "Project"
- })
+ project = frappe.get_doc({"project_name": "_Test Project 1", "doctype": "Project"})
project.save()
- task = frappe.get_doc(dict(
- doctype = 'Task',
- subject = '_Test Project Task 1',
- status = 'Open',
- project = project.name
- )).insert()
+ task = frappe.get_doc(
+ dict(doctype="Task", subject="_Test Project Task 1", status="Open", project=project.name)
+ ).insert()
task_name = task.name
payable_account = get_payable_account(company_name)
- make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3", project.name, task_name)
+ make_expense_claim(
+ payable_account, 300, 200, company_name, "Travel Expenses - _TC3", project.name, task_name
+ )
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200)
self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200)
- expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC3", project.name, task_name)
+ expense_claim2 = make_expense_claim(
+ payable_account, 600, 500, company_name, "Travel Expenses - _TC3", project.name, task_name
+ )
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700)
self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 700)
@@ -56,7 +54,9 @@ class TestExpenseClaim(unittest.TestCase):
def test_expense_claim_status(self):
payable_account = get_payable_account(company_name)
- expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3")
+ expense_claim = make_expense_claim(
+ payable_account, 300, 200, company_name, "Travel Expenses - _TC3"
+ )
je_dict = make_bank_entry("Expense Claim", expense_claim.name)
je = frappe.get_doc(je_dict)
@@ -72,24 +72,110 @@ class TestExpenseClaim(unittest.TestCase):
expense_claim = frappe.get_doc("Expense Claim", expense_claim.name)
self.assertEqual(expense_claim.status, "Unpaid")
+ # expense claim without any sanctioned amount should not have status as Paid
+ claim = make_expense_claim(payable_account, 1000, 0, "_Test Company", "Travel Expenses - _TC")
+ self.assertEqual(claim.total_sanctioned_amount, 0)
+ self.assertEqual(claim.status, "Submitted")
+
+ # no gl entries created
+ gl_entry = frappe.get_all(
+ "GL Entry", {"voucher_type": "Expense Claim", "voucher_no": claim.name}
+ )
+ self.assertEqual(len(gl_entry), 0)
+
+ def test_expense_claim_against_fully_paid_advances(self):
+ from erpnext.hr.doctype.employee_advance.test_employee_advance import (
+ get_advances_for_claim,
+ make_employee_advance,
+ make_payment_entry,
+ )
+
+ frappe.db.delete("Employee Advance")
+
+ payable_account = get_payable_account("_Test Company")
+ claim = make_expense_claim(
+ payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True
+ )
+
+ advance = make_employee_advance(claim.employee)
+ pe = make_payment_entry(advance)
+ pe.submit()
+
+ # claim for already paid out advances
+ claim = get_advances_for_claim(claim, advance.name)
+ claim.save()
+ claim.submit()
+
+ self.assertEqual(claim.grand_total, 0)
+ self.assertEqual(claim.status, "Paid")
+
+ def test_expense_claim_partially_paid_via_advance(self):
+ from erpnext.hr.doctype.employee_advance.test_employee_advance import (
+ get_advances_for_claim,
+ make_employee_advance,
+ )
+ from erpnext.hr.doctype.employee_advance.test_employee_advance import (
+ make_payment_entry as make_advance_payment,
+ )
+
+ frappe.db.delete("Employee Advance")
+
+ payable_account = get_payable_account("_Test Company")
+ claim = make_expense_claim(
+ payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True
+ )
+
+ # link advance for partial amount
+ advance = make_employee_advance(claim.employee, {"advance_amount": 500})
+ pe = make_advance_payment(advance)
+ pe.submit()
+
+ claim = get_advances_for_claim(claim, advance.name)
+ claim.save()
+ claim.submit()
+
+ self.assertEqual(claim.grand_total, 500)
+ self.assertEqual(claim.status, "Unpaid")
+
+ # reimburse remaning amount
+ make_payment_entry(claim, payable_account, 500)
+ claim.reload()
+
+ self.assertEqual(claim.total_amount_reimbursed, 500)
+ self.assertEqual(claim.status, "Paid")
+
def test_expense_claim_gl_entry(self):
payable_account = get_payable_account(company_name)
taxes = generate_taxes()
- expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3",
- do_not_submit=True, taxes=taxes)
+ expense_claim = make_expense_claim(
+ payable_account,
+ 300,
+ 200,
+ company_name,
+ "Travel Expenses - _TC3",
+ do_not_submit=True,
+ taxes=taxes,
+ )
expense_claim.submit()
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit
from `tabGL Entry` where voucher_type='Expense Claim' and voucher_no=%s
- order by account asc""", expense_claim.name, as_dict=1)
+ order by account asc""",
+ expense_claim.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
- expected_values = dict((d[0], d) for d in [
- ['Output Tax CGST - _TC3',18.0, 0.0],
- [payable_account, 0.0, 218.0],
- ["Travel Expenses - _TC3", 200.0, 0.0]
- ])
+ expected_values = dict(
+ (d[0], d)
+ for d in [
+ ["Output Tax CGST - _TC3", 18.0, 0.0],
+ [payable_account, 0.0, 218.0],
+ ["Travel Expenses - _TC3", 200.0, 0.0],
+ ]
+ )
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
@@ -98,20 +184,30 @@ class TestExpenseClaim(unittest.TestCase):
def test_rejected_expense_claim(self):
payable_account = get_payable_account(company_name)
- expense_claim = frappe.get_doc({
- "doctype": "Expense Claim",
- "employee": "_T-Employee-00001",
- "payable_account": payable_account,
- "approval_status": "Rejected",
- "expenses":
- [{"expense_type": "Travel", "default_account": "Travel Expenses - _TC3", "amount": 300, "sanctioned_amount": 200}]
- })
+ expense_claim = frappe.get_doc(
+ {
+ "doctype": "Expense Claim",
+ "employee": "_T-Employee-00001",
+ "payable_account": payable_account,
+ "approval_status": "Rejected",
+ "expenses": [
+ {
+ "expense_type": "Travel",
+ "default_account": "Travel Expenses - _TC3",
+ "amount": 300,
+ "sanctioned_amount": 200,
+ }
+ ],
+ }
+ )
expense_claim.submit()
- self.assertEqual(expense_claim.status, 'Rejected')
+ self.assertEqual(expense_claim.status, "Rejected")
self.assertEqual(expense_claim.total_sanctioned_amount, 0.0)
- gl_entry = frappe.get_all('GL Entry', {'voucher_type': 'Expense Claim', 'voucher_no': expense_claim.name})
+ gl_entry = frappe.get_all(
+ "GL Entry", {"voucher_type": "Expense Claim", "voucher_no": expense_claim.name}
+ )
self.assertEqual(len(gl_entry), 0)
def test_expense_approver_perms(self):
@@ -120,7 +216,9 @@ class TestExpenseClaim(unittest.TestCase):
# check doc shared
payable_account = get_payable_account("_Test Company")
- expense_claim = make_expense_claim(payable_account, 300, 200, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
+ expense_claim = make_expense_claim(
+ payable_account, 300, 200, "_Test Company", "Travel Expenses - _TC", do_not_submit=True
+ )
expense_claim.expense_approver = user
expense_claim.save()
self.assertTrue(expense_claim.name in frappe.share.get_shared("Expense Claim", user))
@@ -144,51 +242,76 @@ class TestExpenseClaim(unittest.TestCase):
def test_multiple_payment_entries_against_expense(self):
# Creating expense claim
payable_account = get_payable_account("_Test Company")
- expense_claim = make_expense_claim(payable_account, 5500, 5500, "_Test Company", "Travel Expenses - _TC")
+ expense_claim = make_expense_claim(
+ payable_account, 5500, 5500, "_Test Company", "Travel Expenses - _TC"
+ )
expense_claim.save()
expense_claim.submit()
# Payment entry 1: paying 500
- make_payment_entry(expense_claim, payable_account,500)
- outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(expense_claim)
+ make_payment_entry(expense_claim, payable_account, 500)
+ outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(
+ expense_claim
+ )
self.assertEqual(outstanding_amount, 5000)
self.assertEqual(total_amount_reimbursed, 500)
# Payment entry 1: paying 2000
- make_payment_entry(expense_claim, payable_account,2000)
- outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(expense_claim)
+ make_payment_entry(expense_claim, payable_account, 2000)
+ outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(
+ expense_claim
+ )
self.assertEqual(outstanding_amount, 3000)
self.assertEqual(total_amount_reimbursed, 2500)
# Payment entry 1: paying 3000
- make_payment_entry(expense_claim, payable_account,3000)
- outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(expense_claim)
+ make_payment_entry(expense_claim, payable_account, 3000)
+ outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(
+ expense_claim
+ )
self.assertEqual(outstanding_amount, 0)
self.assertEqual(total_amount_reimbursed, 5500)
def get_payable_account(company):
- return frappe.get_cached_value('Company', company, 'default_payable_account')
+ return frappe.get_cached_value("Company", company, "default_payable_account")
+
def generate_taxes():
- parent_account = frappe.db.get_value('Account',
- {'company': company_name, 'is_group':1, 'account_type': 'Tax'},
- 'name')
- account = create_account(company=company_name, account_name="Output Tax CGST", account_type="Tax", parent_account=parent_account)
- return {'taxes':[{
- "account_head": account,
- "rate": 9,
- "description": "CGST",
- "tax_amount": 10,
- "total": 210
- }]}
+ parent_account = frappe.db.get_value(
+ "Account", {"company": company_name, "is_group": 1, "account_type": "Tax"}, "name"
+ )
+ account = create_account(
+ company=company_name,
+ account_name="Output Tax CGST",
+ account_type="Tax",
+ parent_account=parent_account,
+ )
+ return {
+ "taxes": [
+ {"account_head": account, "rate": 9, "description": "CGST", "tax_amount": 10, "total": 210}
+ ]
+ }
-def make_expense_claim(payable_account, amount, sanctioned_amount, company, account, project=None, task_name=None, do_not_submit=False, taxes=None):
+
+def make_expense_claim(
+ payable_account,
+ amount,
+ sanctioned_amount,
+ company,
+ account,
+ project=None,
+ task_name=None,
+ do_not_submit=False,
+ taxes=None,
+):
employee = frappe.db.get_value("Employee", {"status": "Active"})
if not employee:
employee = make_employee("test_employee@expense_claim.com", company=company)
- currency, cost_center = frappe.db.get_value('Company', company, ['default_currency', 'cost_center'])
+ currency, cost_center = frappe.db.get_value(
+ "Company", company, ["default_currency", "cost_center"]
+ )
expense_claim = {
"doctype": "Expense Claim",
"employee": employee,
@@ -196,14 +319,16 @@ def make_expense_claim(payable_account, amount, sanctioned_amount, company, acco
"approval_status": "Approved",
"company": company,
"currency": currency,
- "expenses": [{
- "expense_type": "Travel",
- "default_account": account,
- "currency": currency,
- "amount": amount,
- "sanctioned_amount": sanctioned_amount,
- "cost_center": cost_center
- }]
+ "expenses": [
+ {
+ "expense_type": "Travel",
+ "default_account": account,
+ "currency": currency,
+ "amount": amount,
+ "sanctioned_amount": sanctioned_amount,
+ "cost_center": cost_center,
+ }
+ ],
}
if taxes:
expense_claim.update(taxes)
@@ -220,17 +345,24 @@ def make_expense_claim(payable_account, amount, sanctioned_amount, company, acco
expense_claim.submit()
return expense_claim
-def get_outstanding_and_total_reimbursed_amounts(expense_claim):
- outstanding_amount = flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_sanctioned_amount")) - \
- flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_amount_reimbursed"))
- total_amount_reimbursed = flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_amount_reimbursed"))
- return outstanding_amount,total_amount_reimbursed
+def get_outstanding_and_total_reimbursed_amounts(expense_claim):
+ outstanding_amount = flt(
+ frappe.db.get_value("Expense Claim", expense_claim.name, "total_sanctioned_amount")
+ ) - flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_amount_reimbursed"))
+ total_amount_reimbursed = flt(
+ frappe.db.get_value("Expense Claim", expense_claim.name, "total_amount_reimbursed")
+ )
+
+ return outstanding_amount, total_amount_reimbursed
+
def make_payment_entry(expense_claim, payable_account, amt):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
- pe = get_payment_entry("Expense Claim", expense_claim.name, bank_account="_Test Bank USD - _TC", bank_amount=amt)
+ pe = get_payment_entry(
+ "Expense Claim", expense_claim.name, bank_account="_Test Bank USD - _TC", bank_amount=amt
+ )
pe.reference_no = "1"
pe.reference_date = nowdate()
pe.source_exchange_rate = 1
@@ -238,4 +370,3 @@ def make_payment_entry(expense_claim, payable_account, amt):
pe.references[0].allocated_amount = amt
pe.insert()
pe.submit()
-
diff --git a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py
index 570b2c115fa..6d29f7d8e71 100644
--- a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py
+++ b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py
@@ -18,12 +18,13 @@ class ExpenseClaimType(Document):
for entry in self.accounts:
accounts_list.append(entry.company)
- if len(accounts_list)!= len(set(accounts_list)):
+ if len(accounts_list) != len(set(accounts_list)):
frappe.throw(_("Same Company is entered more than once"))
def validate_accounts(self):
for entry in self.accounts:
"""Error when Company of Ledger account doesn't match with Company Selected"""
if frappe.db.get_value("Account", entry.default_account, "company") != entry.company:
- frappe.throw(_("Account {0} does not match with Company {1}"
- ).format(entry.default_account, entry.company))
+ frappe.throw(
+ _("Account {0} does not match with Company {1}").format(entry.default_account, entry.company)
+ )
diff --git a/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.py b/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.py
index a2403b6eb8f..62348e28255 100644
--- a/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.py
+++ b/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Expense Claim Type')
+
class TestExpenseClaimType(unittest.TestCase):
pass
diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.py b/erpnext/hr/doctype/holiday_list/holiday_list.py
index 35775ab816c..ea32ba744d8 100644
--- a/erpnext/hr/doctype/holiday_list/holiday_list.py
+++ b/erpnext/hr/doctype/holiday_list/holiday_list.py
@@ -10,7 +10,9 @@ from frappe.model.document import Document
from frappe.utils import cint, formatdate, getdate, today
-class OverlapError(frappe.ValidationError): pass
+class OverlapError(frappe.ValidationError):
+ pass
+
class HolidayList(Document):
def validate(self):
@@ -21,9 +23,14 @@ class HolidayList(Document):
def get_weekly_off_dates(self):
self.validate_values()
date_list = self.get_weekly_off_date_list(self.from_date, self.to_date)
- last_idx = max([cint(d.idx) for d in self.get("holidays")] or [0,])
+ last_idx = max(
+ [cint(d.idx) for d in self.get("holidays")]
+ or [
+ 0,
+ ]
+ )
for i, d in enumerate(date_list):
- ch = self.append('holidays', {})
+ ch = self.append("holidays", {})
ch.description = self.weekly_off
ch.holiday_date = d
ch.weekly_off = 1
@@ -33,14 +40,17 @@ class HolidayList(Document):
if not self.weekly_off:
throw(_("Please select weekly off day"))
-
def validate_days(self):
if getdate(self.from_date) > getdate(self.to_date):
throw(_("To Date cannot be before From Date"))
for day in self.get("holidays"):
if not (getdate(self.from_date) <= getdate(day.holiday_date) <= getdate(self.to_date)):
- frappe.throw(_("The holiday on {0} is not between From Date and To Date").format(formatdate(day.holiday_date)))
+ frappe.throw(
+ _("The holiday on {0} is not between From Date and To Date").format(
+ formatdate(day.holiday_date)
+ )
+ )
def get_weekly_off_date_list(self, start_date, end_date):
start_date, end_date = getdate(start_date), getdate(end_date)
@@ -66,7 +76,8 @@ class HolidayList(Document):
@frappe.whitelist()
def clear_table(self):
- self.set('holidays', [])
+ self.set("holidays", [])
+
@frappe.whitelist()
def get_events(start, end, filters=None):
@@ -82,23 +93,28 @@ def get_events(start, end, filters=None):
filters = []
if start:
- filters.append(['Holiday', 'holiday_date', '>', getdate(start)])
+ filters.append(["Holiday", "holiday_date", ">", getdate(start)])
if end:
- filters.append(['Holiday', 'holiday_date', '<', getdate(end)])
+ filters.append(["Holiday", "holiday_date", "<", getdate(end)])
- return frappe.get_list('Holiday List',
- fields=['name', '`tabHoliday`.holiday_date', '`tabHoliday`.description', '`tabHoliday List`.color'],
- filters = filters,
- update={"allDay": 1})
+ return frappe.get_list(
+ "Holiday List",
+ fields=[
+ "name",
+ "`tabHoliday`.holiday_date",
+ "`tabHoliday`.description",
+ "`tabHoliday List`.color",
+ ],
+ filters=filters,
+ update={"allDay": 1},
+ )
def is_holiday(holiday_list, date=None):
- """Returns true if the given date is a holiday in the given holiday list
- """
+ """Returns true if the given date is a holiday in the given holiday list"""
if date is None:
date = today()
if holiday_list:
- return bool(frappe.get_all('Holiday List',
- dict(name=holiday_list, holiday_date=date)))
+ return bool(frappe.get_all("Holiday List", dict(name=holiday_list, holiday_date=date)))
else:
return False
diff --git a/erpnext/hr/doctype/holiday_list/holiday_list_dashboard.py b/erpnext/hr/doctype/holiday_list/holiday_list_dashboard.py
index e074e266b87..0cbf09461b5 100644
--- a/erpnext/hr/doctype/holiday_list/holiday_list_dashboard.py
+++ b/erpnext/hr/doctype/holiday_list/holiday_list_dashboard.py
@@ -1,21 +1,15 @@
-
-
def get_data():
return {
- 'fieldname': 'holiday_list',
- 'non_standard_fieldnames': {
- 'Company': 'default_holiday_list',
- 'Leave Period': 'optional_holiday_list'
+ "fieldname": "holiday_list",
+ "non_standard_fieldnames": {
+ "Company": "default_holiday_list",
+ "Leave Period": "optional_holiday_list",
},
- 'transactions': [
+ "transactions": [
{
- 'items': ['Company', 'Employee', 'Workstation'],
+ "items": ["Company", "Employee", "Workstation"],
},
- {
- 'items': ['Leave Period', 'Shift Type']
- },
- {
- 'items': ['Service Level', 'Service Level Agreement']
- }
- ]
+ {"items": ["Leave Period", "Shift Type"]},
+ {"items": ["Service Level", "Service Level Agreement"]},
+ ],
}
diff --git a/erpnext/hr/doctype/holiday_list/test_holiday_list.py b/erpnext/hr/doctype/holiday_list/test_holiday_list.py
index c9239edb720..d32cfe82650 100644
--- a/erpnext/hr/doctype/holiday_list/test_holiday_list.py
+++ b/erpnext/hr/doctype/holiday_list/test_holiday_list.py
@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import unittest
+from contextlib import contextmanager
from datetime import timedelta
import frappe
@@ -11,22 +12,50 @@ from frappe.utils import getdate
class TestHolidayList(unittest.TestCase):
def test_holiday_list(self):
today_date = getdate()
- test_holiday_dates = [today_date-timedelta(days=5), today_date-timedelta(days=4)]
- holiday_list = make_holiday_list("test_holiday_list",
+ test_holiday_dates = [today_date - timedelta(days=5), today_date - timedelta(days=4)]
+ holiday_list = make_holiday_list(
+ "test_holiday_list",
holiday_dates=[
- {'holiday_date': test_holiday_dates[0], 'description': 'test holiday'},
- {'holiday_date': test_holiday_dates[1], 'description': 'test holiday2'}
- ])
- fetched_holiday_list = frappe.get_value('Holiday List', holiday_list.name)
+ {"holiday_date": test_holiday_dates[0], "description": "test holiday"},
+ {"holiday_date": test_holiday_dates[1], "description": "test holiday2"},
+ ],
+ )
+ fetched_holiday_list = frappe.get_value("Holiday List", holiday_list.name)
self.assertEqual(holiday_list.name, fetched_holiday_list)
-def make_holiday_list(name, from_date=getdate()-timedelta(days=10), to_date=getdate(), holiday_dates=None):
+
+def make_holiday_list(
+ name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None
+):
frappe.delete_doc_if_exists("Holiday List", name, force=1)
- doc = frappe.get_doc({
- "doctype": "Holiday List",
- "holiday_list_name": name,
- "from_date" : from_date,
- "to_date" : to_date,
- "holidays" : holiday_dates
- }).insert()
+ doc = frappe.get_doc(
+ {
+ "doctype": "Holiday List",
+ "holiday_list_name": name,
+ "from_date": from_date,
+ "to_date": to_date,
+ "holidays": holiday_dates,
+ }
+ ).insert()
return doc
+
+
+@contextmanager
+def set_holiday_list(holiday_list, company_name):
+ """
+ Context manager for setting holiday list in tests
+ """
+ try:
+ company = frappe.get_doc("Company", company_name)
+ previous_holiday_list = company.default_holiday_list
+
+ company.default_holiday_list = holiday_list
+ company.save()
+
+ yield
+
+ finally:
+ # restore holiday list setup
+ company = frappe.get_doc("Company", company_name)
+ company.default_holiday_list = previous_holiday_list
+ company.save()
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py
index c295bcbc0d9..72a49e285a0 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.py
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.py
@@ -10,6 +10,7 @@ from frappe.utils import format_date
# Wether to proceed with frequency change
PROCEED_WITH_FREQUENCY_CHANGE = False
+
class HRSettings(Document):
def validate(self):
self.set_naming_series()
@@ -22,21 +23,24 @@ class HRSettings(Document):
def set_naming_series(self):
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
- set_by_naming_series("Employee", "employee_number",
- self.get("emp_created_by")=="Naming Series", hide_name_field=True)
+
+ set_by_naming_series(
+ "Employee",
+ "employee_number",
+ self.get("emp_created_by") == "Naming Series",
+ hide_name_field=True,
+ )
def validate_frequency_change(self):
weekly_job, monthly_job = None, None
try:
weekly_job = frappe.get_doc(
- 'Scheduled Job Type',
- 'employee_reminders.send_reminders_in_advance_weekly'
+ "Scheduled Job Type", "employee_reminders.send_reminders_in_advance_weekly"
)
monthly_job = frappe.get_doc(
- 'Scheduled Job Type',
- 'employee_reminders.send_reminders_in_advance_monthly'
+ "Scheduled Job Type", "employee_reminders.send_reminders_in_advance_monthly"
)
except frappe.DoesNotExistError:
return
@@ -62,17 +66,20 @@ class HRSettings(Document):
from_date = frappe.bold(format_date(from_date))
to_date = frappe.bold(format_date(to_date))
frappe.msgprint(
- msg=frappe._('Employees will miss holiday reminders from {} until {}. Do you want to proceed with this change?').format(from_date, to_date),
- title='Confirm change in Frequency',
+ msg=frappe._(
+ "Employees will miss holiday reminders from {} until {}. Do you want to proceed with this change?"
+ ).format(from_date, to_date),
+ title="Confirm change in Frequency",
primary_action={
- 'label': frappe._('Yes, Proceed'),
- 'client_action': 'erpnext.proceed_save_with_reminders_frequency_change'
+ "label": frappe._("Yes, Proceed"),
+ "client_action": "erpnext.proceed_save_with_reminders_frequency_change",
},
- raise_exception=frappe.ValidationError
+ raise_exception=frappe.ValidationError,
)
+
@frappe.whitelist()
def set_proceed_with_frequency_change():
- '''Enables proceed with frequency change'''
+ """Enables proceed with frequency change"""
global PROCEED_WITH_FREQUENCY_CHANGE
PROCEED_WITH_FREQUENCY_CHANGE = True
diff --git a/erpnext/hr/doctype/interest/test_interest.py b/erpnext/hr/doctype/interest/test_interest.py
index d4ecd9b841e..eacb57f7587 100644
--- a/erpnext/hr/doctype/interest/test_interest.py
+++ b/erpnext/hr/doctype/interest/test_interest.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Interest')
+
class TestInterest(unittest.TestCase):
pass
diff --git a/erpnext/hr/doctype/interview/interview.py b/erpnext/hr/doctype/interview/interview.py
index f5312476a28..8f0ff021335 100644
--- a/erpnext/hr/doctype/interview/interview.py
+++ b/erpnext/hr/doctype/interview/interview.py
@@ -13,6 +13,7 @@ from frappe.utils import cstr, get_datetime, get_link_to_form
class DuplicateInterviewRoundError(frappe.ValidationError):
pass
+
class Interview(Document):
def validate(self):
self.validate_duplicate_interview()
@@ -20,37 +21,47 @@ class Interview(Document):
self.validate_overlap()
def on_submit(self):
- if self.status not in ['Cleared', 'Rejected']:
- frappe.throw(_('Only Interviews with Cleared or Rejected status can be submitted.'), title=_('Not Allowed'))
+ if self.status not in ["Cleared", "Rejected"]:
+ frappe.throw(
+ _("Only Interviews with Cleared or Rejected status can be submitted."), title=_("Not Allowed")
+ )
def validate_duplicate_interview(self):
- duplicate_interview = frappe.db.exists('Interview', {
- 'job_applicant': self.job_applicant,
- 'interview_round': self.interview_round,
- 'docstatus': 1
- }
+ duplicate_interview = frappe.db.exists(
+ "Interview",
+ {"job_applicant": self.job_applicant, "interview_round": self.interview_round, "docstatus": 1},
)
if duplicate_interview:
- frappe.throw(_('Job Applicants are not allowed to appear twice for the same Interview round. Interview {0} already scheduled for Job Applicant {1}').format(
- frappe.bold(get_link_to_form('Interview', duplicate_interview)),
- frappe.bold(self.job_applicant)
- ))
+ frappe.throw(
+ _(
+ "Job Applicants are not allowed to appear twice for the same Interview round. Interview {0} already scheduled for Job Applicant {1}"
+ ).format(
+ frappe.bold(get_link_to_form("Interview", duplicate_interview)),
+ frappe.bold(self.job_applicant),
+ )
+ )
def validate_designation(self):
- applicant_designation = frappe.db.get_value('Job Applicant', self.job_applicant, 'designation')
- if self.designation :
+ applicant_designation = frappe.db.get_value("Job Applicant", self.job_applicant, "designation")
+ if self.designation:
if self.designation != applicant_designation:
- frappe.throw(_('Interview Round {0} is only for Designation {1}. Job Applicant has applied for the role {2}').format(
- self.interview_round, frappe.bold(self.designation), applicant_designation),
- exc=DuplicateInterviewRoundError)
+ frappe.throw(
+ _(
+ "Interview Round {0} is only for Designation {1}. Job Applicant has applied for the role {2}"
+ ).format(
+ self.interview_round, frappe.bold(self.designation), applicant_designation
+ ),
+ exc=DuplicateInterviewRoundError,
+ )
else:
self.designation = applicant_designation
def validate_overlap(self):
- interviewers = [entry.interviewer for entry in self.interview_details] or ['']
+ interviewers = [entry.interviewer for entry in self.interview_details] or [""]
- overlaps = frappe.db.sql("""
+ overlaps = frappe.db.sql(
+ """
SELECT interview.name
FROM `tabInterview` as interview
INNER JOIN `tabInterview Detail` as detail
@@ -60,13 +71,25 @@ class Interview(Document):
((from_time < %s and to_time > %s) or
(from_time > %s and to_time < %s) or
(from_time = %s))
- """, (self.scheduled_on, self.name, self.job_applicant, interviewers,
- self.from_time, self.to_time, self.from_time, self.to_time, self.from_time))
+ """,
+ (
+ self.scheduled_on,
+ self.name,
+ self.job_applicant,
+ interviewers,
+ self.from_time,
+ self.to_time,
+ self.from_time,
+ self.to_time,
+ self.from_time,
+ ),
+ )
if overlaps:
- overlapping_details = _('Interview overlaps with {0}').format(get_link_to_form('Interview', overlaps[0][0]))
- frappe.throw(overlapping_details, title=_('Overlap'))
-
+ overlapping_details = _("Interview overlaps with {0}").format(
+ get_link_to_form("Interview", overlaps[0][0])
+ )
+ frappe.throw(overlapping_details, title=_("Overlap"))
@frappe.whitelist()
def reschedule_interview(self, scheduled_on, from_time, to_time):
@@ -74,116 +97,135 @@ class Interview(Document):
from_time = self.from_time
to_time = self.to_time
- self.db_set({
- 'scheduled_on': scheduled_on,
- 'from_time': from_time,
- 'to_time': to_time
- })
+ self.db_set({"scheduled_on": scheduled_on, "from_time": from_time, "to_time": to_time})
self.notify_update()
recipients = get_recipients(self.name)
try:
frappe.sendmail(
- recipients= recipients,
- subject=_('Interview: {0} Rescheduled').format(self.name),
- message=_('Your Interview session is rescheduled from {0} {1} - {2} to {3} {4} - {5}').format(
- original_date, from_time, to_time, self.scheduled_on, self.from_time, self.to_time),
+ recipients=recipients,
+ subject=_("Interview: {0} Rescheduled").format(self.name),
+ message=_("Your Interview session is rescheduled from {0} {1} - {2} to {3} {4} - {5}").format(
+ original_date, from_time, to_time, self.scheduled_on, self.from_time, self.to_time
+ ),
reference_doctype=self.doctype,
- reference_name=self.name
+ reference_name=self.name,
)
except Exception:
- frappe.msgprint(_('Failed to send the Interview Reschedule notification. Please configure your email account.'))
+ frappe.msgprint(
+ _("Failed to send the Interview Reschedule notification. Please configure your email account.")
+ )
- frappe.msgprint(_('Interview Rescheduled successfully'), indicator='green')
+ frappe.msgprint(_("Interview Rescheduled successfully"), indicator="green")
def get_recipients(name, for_feedback=0):
- interview = frappe.get_doc('Interview', name)
+ interview = frappe.get_doc("Interview", name)
if for_feedback:
recipients = [d.interviewer for d in interview.interview_details if not d.interview_feedback]
else:
recipients = [d.interviewer for d in interview.interview_details]
- recipients.append(frappe.db.get_value('Job Applicant', interview.job_applicant, 'email_id'))
+ recipients.append(frappe.db.get_value("Job Applicant", interview.job_applicant, "email_id"))
return recipients
@frappe.whitelist()
def get_interviewers(interview_round):
- return frappe.get_all('Interviewer', filters={'parent': interview_round}, fields=['user as interviewer'])
+ return frappe.get_all(
+ "Interviewer", filters={"parent": interview_round}, fields=["user as interviewer"]
+ )
def send_interview_reminder():
- reminder_settings = frappe.db.get_value('HR Settings', 'HR Settings',
- ['send_interview_reminder', 'interview_reminder_template'], as_dict=True)
+ reminder_settings = frappe.db.get_value(
+ "HR Settings",
+ "HR Settings",
+ ["send_interview_reminder", "interview_reminder_template"],
+ as_dict=True,
+ )
if not reminder_settings.send_interview_reminder:
return
- remind_before = cstr(frappe.db.get_single_value('HR Settings', 'remind_before')) or '01:00:00'
- remind_before = datetime.datetime.strptime(remind_before, '%H:%M:%S')
+ remind_before = cstr(frappe.db.get_single_value("HR Settings", "remind_before")) or "01:00:00"
+ remind_before = datetime.datetime.strptime(remind_before, "%H:%M:%S")
reminder_date_time = datetime.datetime.now() + datetime.timedelta(
- hours=remind_before.hour, minutes=remind_before.minute, seconds=remind_before.second)
+ hours=remind_before.hour, minutes=remind_before.minute, seconds=remind_before.second
+ )
- interviews = frappe.get_all('Interview', filters={
- 'scheduled_on': ['between', (datetime.datetime.now(), reminder_date_time)],
- 'status': 'Pending',
- 'reminded': 0,
- 'docstatus': ['!=', 2]
- })
+ interviews = frappe.get_all(
+ "Interview",
+ filters={
+ "scheduled_on": ["between", (datetime.datetime.now(), reminder_date_time)],
+ "status": "Pending",
+ "reminded": 0,
+ "docstatus": ["!=", 2],
+ },
+ )
- interview_template = frappe.get_doc('Email Template', reminder_settings.interview_reminder_template)
+ interview_template = frappe.get_doc(
+ "Email Template", reminder_settings.interview_reminder_template
+ )
for d in interviews:
- doc = frappe.get_doc('Interview', d.name)
+ doc = frappe.get_doc("Interview", d.name)
context = doc.as_dict()
message = frappe.render_template(interview_template.response, context)
recipients = get_recipients(doc.name)
frappe.sendmail(
- recipients= recipients,
+ recipients=recipients,
subject=interview_template.subject,
message=message,
reference_doctype=doc.doctype,
- reference_name=doc.name
+ reference_name=doc.name,
)
- doc.db_set('reminded', 1)
+ doc.db_set("reminded", 1)
def send_daily_feedback_reminder():
- reminder_settings = frappe.db.get_value('HR Settings', 'HR Settings',
- ['send_interview_feedback_reminder', 'feedback_reminder_notification_template'], as_dict=True)
+ reminder_settings = frappe.db.get_value(
+ "HR Settings",
+ "HR Settings",
+ ["send_interview_feedback_reminder", "feedback_reminder_notification_template"],
+ as_dict=True,
+ )
if not reminder_settings.send_interview_feedback_reminder:
return
- interview_feedback_template = frappe.get_doc('Email Template', reminder_settings.feedback_reminder_notification_template)
- interviews = frappe.get_all('Interview', filters={'status': ['in', ['Under Review', 'Pending']], 'docstatus': ['!=', 2]})
+ interview_feedback_template = frappe.get_doc(
+ "Email Template", reminder_settings.feedback_reminder_notification_template
+ )
+ interviews = frappe.get_all(
+ "Interview", filters={"status": ["in", ["Under Review", "Pending"]], "docstatus": ["!=", 2]}
+ )
for entry in interviews:
recipients = get_recipients(entry.name, for_feedback=1)
- doc = frappe.get_doc('Interview', entry.name)
+ doc = frappe.get_doc("Interview", entry.name)
context = doc.as_dict()
message = frappe.render_template(interview_feedback_template.response, context)
if len(recipients):
frappe.sendmail(
- recipients= recipients,
+ recipients=recipients,
subject=interview_feedback_template.subject,
message=message,
- reference_doctype='Interview',
- reference_name=entry.name
+ reference_doctype="Interview",
+ reference_name=entry.name,
)
@frappe.whitelist()
def get_expected_skill_set(interview_round):
- return frappe.get_all('Expected Skill Set', filters ={'parent': interview_round}, fields=['skill'])
+ return frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields=["skill"])
@frappe.whitelist()
@@ -196,16 +238,16 @@ def create_interview_feedback(data, interview_name, interviewer, job_applicant):
data = frappe._dict(json.loads(data))
if frappe.session.user != interviewer:
- frappe.throw(_('Only Interviewer Are allowed to submit Interview Feedback'))
+ frappe.throw(_("Only Interviewer Are allowed to submit Interview Feedback"))
- interview_feedback = frappe.new_doc('Interview Feedback')
+ interview_feedback = frappe.new_doc("Interview Feedback")
interview_feedback.interview = interview_name
interview_feedback.interviewer = interviewer
interview_feedback.job_applicant = job_applicant
for d in data.skill_set:
d = frappe._dict(d)
- interview_feedback.append('skill_assessment', {'skill': d.skill, 'rating': d.rating})
+ interview_feedback.append("skill_assessment", {"skill": d.skill, "rating": d.rating})
interview_feedback.feedback = data.feedback
interview_feedback.result = data.result
@@ -213,24 +255,33 @@ def create_interview_feedback(data, interview_name, interviewer, job_applicant):
interview_feedback.save()
interview_feedback.submit()
- frappe.msgprint(_('Interview Feedback {0} submitted successfully').format(
- get_link_to_form('Interview Feedback', interview_feedback.name)))
+ frappe.msgprint(
+ _("Interview Feedback {0} submitted successfully").format(
+ get_link_to_form("Interview Feedback", interview_feedback.name)
+ )
+ )
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_interviewer_list(doctype, txt, searchfield, start, page_len, filters):
filters = [
- ['Has Role', 'parent', 'like', '%{}%'.format(txt)],
- ['Has Role', 'role', '=', 'interviewer'],
- ['Has Role', 'parenttype', '=', 'User']
+ ["Has Role", "parent", "like", "%{}%".format(txt)],
+ ["Has Role", "role", "=", "interviewer"],
+ ["Has Role", "parenttype", "=", "User"],
]
if filters and isinstance(filters, list):
filters.extend(filters)
- return frappe.get_all('Has Role', limit_start=start, limit_page_length=page_len,
- filters=filters, fields = ['parent'], as_list=1)
+ return frappe.get_all(
+ "Has Role",
+ limit_start=start,
+ limit_page_length=page_len,
+ filters=filters,
+ fields=["parent"],
+ as_list=1,
+ )
@frappe.whitelist()
@@ -249,12 +300,13 @@ def get_events(start, end, filters=None):
"Pending": "#fff4f0",
"Under Review": "#d3e8fc",
"Cleared": "#eaf5ed",
- "Rejected": "#fce7e7"
+ "Rejected": "#fce7e7",
}
- conditions = get_event_conditions('Interview', filters)
+ conditions = get_event_conditions("Interview", filters)
- interviews = frappe.db.sql("""
+ interviews = frappe.db.sql(
+ """
SELECT DISTINCT
`tabInterview`.name, `tabInterview`.job_applicant, `tabInterview`.interview_round,
`tabInterview`.scheduled_on, `tabInterview`.status, `tabInterview`.from_time as from_time,
@@ -265,10 +317,13 @@ def get_events(start, end, filters=None):
(`tabInterview`.scheduled_on between %(start)s and %(end)s)
and docstatus != 2
{conditions}
- """.format(conditions=conditions), {
- "start": start,
- "end": end
- }, as_dict=True, update={"allDay": 0})
+ """.format(
+ conditions=conditions
+ ),
+ {"start": start, "end": end},
+ as_dict=True,
+ update={"allDay": 0},
+ )
for d in interviews:
subject_data = []
@@ -279,13 +334,13 @@ def get_events(start, end, filters=None):
color = event_color.get(d.status)
interview_data = {
- 'from': get_datetime('%s %s' % (d.scheduled_on, d.from_time or '00:00:00')),
- 'to': get_datetime('%s %s' % (d.scheduled_on, d.to_time or '00:00:00')),
- 'name': d.name,
- 'subject': '\n'.join(subject_data),
- 'color': color if color else "#89bcde"
+ "from": get_datetime("%s %s" % (d.scheduled_on, d.from_time or "00:00:00")),
+ "to": get_datetime("%s %s" % (d.scheduled_on, d.to_time or "00:00:00")),
+ "name": d.name,
+ "subject": "\n".join(subject_data),
+ "color": color if color else "#89bcde",
}
events.append(interview_data)
- return events
\ No newline at end of file
+ return events
diff --git a/erpnext/hr/doctype/interview/test_interview.py b/erpnext/hr/doctype/interview/test_interview.py
index 1a2257a6d90..840a5ad919d 100644
--- a/erpnext/hr/doctype/interview/test_interview.py
+++ b/erpnext/hr/doctype/interview/test_interview.py
@@ -18,23 +18,30 @@ from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_appli
class TestInterview(unittest.TestCase):
def test_validations_for_designation(self):
job_applicant = create_job_applicant()
- interview = create_interview_and_dependencies(job_applicant.name, designation='_Test_Sales_manager', save=0)
+ interview = create_interview_and_dependencies(
+ job_applicant.name, designation="_Test_Sales_manager", save=0
+ )
self.assertRaises(DuplicateInterviewRoundError, interview.save)
def test_notification_on_rescheduling(self):
job_applicant = create_job_applicant()
- interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -4))
+ interview = create_interview_and_dependencies(
+ job_applicant.name, scheduled_on=add_days(getdate(), -4)
+ )
previous_scheduled_date = interview.scheduled_on
frappe.db.sql("DELETE FROM `tabEmail Queue`")
- interview.reschedule_interview(add_days(getdate(previous_scheduled_date), 2),
- from_time=nowtime(), to_time=nowtime())
+ interview.reschedule_interview(
+ add_days(getdate(previous_scheduled_date), 2), from_time=nowtime(), to_time=nowtime()
+ )
interview.reload()
self.assertEqual(interview.scheduled_on, add_days(getdate(previous_scheduled_date), 2))
- notification = frappe.get_all("Email Queue", filters={"message": ("like", "%Your Interview session is rescheduled from%")})
+ notification = frappe.get_all(
+ "Email Queue", filters={"message": ("like", "%Your Interview session is rescheduled from%")}
+ )
self.assertIsNotNone(notification)
def test_notification_for_scheduling(self):
@@ -74,16 +81,17 @@ class TestInterview(unittest.TestCase):
frappe.db.rollback()
-def create_interview_and_dependencies(job_applicant, scheduled_on=None, from_time=None, to_time=None, designation=None, save=1):
+def create_interview_and_dependencies(
+ job_applicant, scheduled_on=None, from_time=None, to_time=None, designation=None, save=1
+):
if designation:
- designation=create_designation(designation_name = "_Test_Sales_manager").name
+ designation = create_designation(designation_name="_Test_Sales_manager").name
interviewer_1 = create_user("test_interviewer1@example.com", "Interviewer")
interviewer_2 = create_user("test_interviewer2@example.com", "Interviewer")
interview_round = create_interview_round(
- "Technical Round", ["Python", "JS"],
- designation=designation, save=True
+ "Technical Round", ["Python", "JS"], designation=designation, save=True
)
interview = frappe.new_doc("Interview")
@@ -101,6 +109,7 @@ def create_interview_and_dependencies(job_applicant, scheduled_on=None, from_tim
return interview
+
def create_interview_round(name, skill_set, interviewers=[], designation=None, save=True):
create_skill_set(skill_set)
interview_round = frappe.new_doc("Interview Round")
@@ -114,15 +123,14 @@ def create_interview_round(name, skill_set, interviewers=[], designation=None, s
interview_round.append("expected_skill_set", {"skill": skill})
for interviewer in interviewers:
- interview_round.append("interviewer", {
- "user": interviewer
- })
+ interview_round.append("interviewer", {"user": interviewer})
if save:
interview_round.save()
return interview_round
+
def create_skill_set(skill_set):
for skill in skill_set:
if not frappe.db.exists("Skill", skill):
@@ -130,6 +138,7 @@ def create_skill_set(skill_set):
doc.skill_name = skill
doc.save()
+
def create_interview_type(name="test_interview_type"):
if frappe.db.exists("Interview Type", name):
return frappe.get_doc("Interview Type", name).name
@@ -141,32 +150,41 @@ def create_interview_type(name="test_interview_type"):
return doc.name
+
def setup_reminder_settings():
- if not frappe.db.exists('Email Template', _('Interview Reminder')):
- base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
- response = frappe.read_file(os.path.join(base_path, 'interview/interview_reminder_notification_template.html'))
+ if not frappe.db.exists("Email Template", _("Interview Reminder")):
+ base_path = frappe.get_app_path("erpnext", "hr", "doctype")
+ response = frappe.read_file(
+ os.path.join(base_path, "interview/interview_reminder_notification_template.html")
+ )
- frappe.get_doc({
- 'doctype': 'Email Template',
- 'name': _('Interview Reminder'),
- 'response': response,
- 'subject': _('Interview Reminder'),
- 'owner': frappe.session.user,
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Email Template",
+ "name": _("Interview Reminder"),
+ "response": response,
+ "subject": _("Interview Reminder"),
+ "owner": frappe.session.user,
+ }
+ ).insert(ignore_permissions=True)
- if not frappe.db.exists('Email Template', _('Interview Feedback Reminder')):
- base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
- response = frappe.read_file(os.path.join(base_path, 'interview/interview_feedback_reminder_template.html'))
+ if not frappe.db.exists("Email Template", _("Interview Feedback Reminder")):
+ base_path = frappe.get_app_path("erpnext", "hr", "doctype")
+ response = frappe.read_file(
+ os.path.join(base_path, "interview/interview_feedback_reminder_template.html")
+ )
- frappe.get_doc({
- 'doctype': 'Email Template',
- 'name': _('Interview Feedback Reminder'),
- 'response': response,
- 'subject': _('Interview Feedback Reminder'),
- 'owner': frappe.session.user,
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Email Template",
+ "name": _("Interview Feedback Reminder"),
+ "response": response,
+ "subject": _("Interview Feedback Reminder"),
+ "owner": frappe.session.user,
+ }
+ ).insert(ignore_permissions=True)
- hr_settings = frappe.get_doc('HR Settings')
- hr_settings.interview_reminder_template = _('Interview Reminder')
- hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder')
+ hr_settings = frappe.get_doc("HR Settings")
+ hr_settings.interview_reminder_template = _("Interview Reminder")
+ hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
hr_settings.save()
diff --git a/erpnext/hr/doctype/interview_feedback/interview_feedback.py b/erpnext/hr/doctype/interview_feedback/interview_feedback.py
index d046458f196..b8f8aee524d 100644
--- a/erpnext/hr/doctype/interview_feedback/interview_feedback.py
+++ b/erpnext/hr/doctype/interview_feedback/interview_feedback.py
@@ -24,28 +24,36 @@ class InterviewFeedback(Document):
def validate_interviewer(self):
applicable_interviewers = get_applicable_interviewers(self.interview)
if self.interviewer not in applicable_interviewers:
- frappe.throw(_('{0} is not allowed to submit Interview Feedback for the Interview: {1}').format(
- frappe.bold(self.interviewer), frappe.bold(self.interview)))
+ frappe.throw(
+ _("{0} is not allowed to submit Interview Feedback for the Interview: {1}").format(
+ frappe.bold(self.interviewer), frappe.bold(self.interview)
+ )
+ )
def validate_interview_date(self):
- scheduled_date = frappe.db.get_value('Interview', self.interview, 'scheduled_on')
+ scheduled_date = frappe.db.get_value("Interview", self.interview, "scheduled_on")
if getdate() < getdate(scheduled_date) and self.docstatus == 1:
- frappe.throw(_('{0} submission before {1} is not allowed').format(
- frappe.bold('Interview Feedback'),
- frappe.bold('Interview Scheduled Date')
- ))
+ frappe.throw(
+ _("{0} submission before {1} is not allowed").format(
+ frappe.bold("Interview Feedback"), frappe.bold("Interview Scheduled Date")
+ )
+ )
def validate_duplicate(self):
- duplicate_feedback = frappe.db.exists('Interview Feedback', {
- 'interviewer': self.interviewer,
- 'interview': self.interview,
- 'docstatus': 1
- })
+ duplicate_feedback = frappe.db.exists(
+ "Interview Feedback",
+ {"interviewer": self.interviewer, "interview": self.interview, "docstatus": 1},
+ )
if duplicate_feedback:
- frappe.throw(_('Feedback already submitted for the Interview {0}. Please cancel the previous Interview Feedback {1} to continue.').format(
- self.interview, get_link_to_form('Interview Feedback', duplicate_feedback)))
+ frappe.throw(
+ _(
+ "Feedback already submitted for the Interview {0}. Please cancel the previous Interview Feedback {1} to continue."
+ ).format(
+ self.interview, get_link_to_form("Interview Feedback", duplicate_feedback)
+ )
+ )
def calculate_average_rating(self):
total_rating = 0
@@ -53,10 +61,12 @@ class InterviewFeedback(Document):
if d.rating:
total_rating += d.rating
- self.average_rating = flt(total_rating / len(self.skill_assessment) if len(self.skill_assessment) else 0)
+ self.average_rating = flt(
+ total_rating / len(self.skill_assessment) if len(self.skill_assessment) else 0
+ )
def update_interview_details(self):
- doc = frappe.get_doc('Interview', self.interview)
+ doc = frappe.get_doc("Interview", self.interview)
total_rating = 0
if self.docstatus == 2:
@@ -75,12 +85,14 @@ class InterviewFeedback(Document):
if entry.average_rating:
total_rating += entry.average_rating
- doc.average_rating = flt(total_rating / len(doc.interview_details) if len(doc.interview_details) else 0)
+ doc.average_rating = flt(
+ total_rating / len(doc.interview_details) if len(doc.interview_details) else 0
+ )
doc.save()
doc.notify_update()
@frappe.whitelist()
def get_applicable_interviewers(interview):
- data = frappe.get_all('Interview Detail', filters={'parent': interview}, fields=['interviewer'])
+ data = frappe.get_all("Interview Detail", filters={"parent": interview}, fields=["interviewer"])
return [d.interviewer for d in data]
diff --git a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
index 4185f2827a5..0c408b4d35d 100644
--- a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
+++ b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
@@ -17,14 +17,16 @@ class TestInterviewFeedback(unittest.TestCase):
def test_validation_for_skill_set(self):
frappe.set_user("Administrator")
job_applicant = create_job_applicant()
- interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -1))
+ interview = create_interview_and_dependencies(
+ job_applicant.name, scheduled_on=add_days(getdate(), -1)
+ )
skill_ratings = get_skills_rating(interview.interview_round)
interviewer = interview.interview_details[0].interviewer
- create_skill_set(['Leadership'])
+ create_skill_set(["Leadership"])
interview_feedback = create_interview_feedback(interview.name, interviewer, skill_ratings)
- interview_feedback.append("skill_assessment", {"skill": 'Leadership', 'rating': 4})
+ interview_feedback.append("skill_assessment", {"skill": "Leadership", "rating": 4})
frappe.set_user(interviewer)
self.assertRaises(frappe.ValidationError, interview_feedback.save)
@@ -33,7 +35,9 @@ class TestInterviewFeedback(unittest.TestCase):
def test_average_ratings_on_feedback_submission_and_cancellation(self):
job_applicant = create_job_applicant()
- interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -1))
+ interview = create_interview_and_dependencies(
+ job_applicant.name, scheduled_on=add_days(getdate(), -1)
+ )
skill_ratings = get_skills_rating(interview.interview_round)
# For First Interviewer Feedback
@@ -48,20 +52,26 @@ class TestInterviewFeedback(unittest.TestCase):
if d.rating:
total_rating += d.rating
- avg_rating = flt(total_rating / len(feedback_1.skill_assessment) if len(feedback_1.skill_assessment) else 0)
+ avg_rating = flt(
+ total_rating / len(feedback_1.skill_assessment) if len(feedback_1.skill_assessment) else 0
+ )
self.assertEqual(flt(avg_rating, 3), feedback_1.average_rating)
- avg_on_interview_detail = frappe.db.get_value('Interview Detail', {
- 'parent': feedback_1.interview,
- 'interviewer': feedback_1.interviewer,
- 'interview_feedback': feedback_1.name
- }, 'average_rating')
+ avg_on_interview_detail = frappe.db.get_value(
+ "Interview Detail",
+ {
+ "parent": feedback_1.interview,
+ "interviewer": feedback_1.interviewer,
+ "interview_feedback": feedback_1.name,
+ },
+ "average_rating",
+ )
# 1. average should be reflected in Interview Detail.
self.assertEqual(avg_on_interview_detail, round(feedback_1.average_rating))
- '''For Second Interviewer Feedback'''
+ """For Second Interviewer Feedback"""
interviewer = interview.interview_details[1].interviewer
frappe.set_user(interviewer)
@@ -95,7 +105,9 @@ def create_interview_feedback(interview, interviewer, skills_ratings):
def get_skills_rating(interview_round):
import random
- skills = frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields = ["skill"])
+ skills = frappe.get_all(
+ "Expected Skill Set", filters={"parent": interview_round}, fields=["skill"]
+ )
for d in skills:
d["rating"] = random.randint(1, 5)
return skills
diff --git a/erpnext/hr/doctype/interview_round/interview_round.py b/erpnext/hr/doctype/interview_round/interview_round.py
index 0f442c320ad..83dbf0ea98e 100644
--- a/erpnext/hr/doctype/interview_round/interview_round.py
+++ b/erpnext/hr/doctype/interview_round/interview_round.py
@@ -11,6 +11,7 @@ from frappe.model.document import Document
class InterviewRound(Document):
pass
+
@frappe.whitelist()
def create_interview(doc):
if isinstance(doc, str):
@@ -24,10 +25,5 @@ def create_interview(doc):
if doc.interviewers:
interview.interview_details = []
for data in doc.interviewers:
- interview.append("interview_details", {
- "interviewer": data.user
- })
+ interview.append("interview_details", {"interviewer": data.user})
return interview
-
-
-
diff --git a/erpnext/hr/doctype/interview_round/test_interview_round.py b/erpnext/hr/doctype/interview_round/test_interview_round.py
index dcec9419c07..95681653743 100644
--- a/erpnext/hr/doctype/interview_round/test_interview_round.py
+++ b/erpnext/hr/doctype/interview_round/test_interview_round.py
@@ -8,4 +8,3 @@ import unittest
class TestInterviewRound(unittest.TestCase):
pass
-
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.py b/erpnext/hr/doctype/job_applicant/job_applicant.py
index 54ccfca38f7..14ebca432e5 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.py
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.py
@@ -13,7 +13,9 @@ from frappe.utils import validate_email_address
from erpnext.hr.doctype.interview.interview import get_interviewers
-class DuplicationError(frappe.ValidationError): pass
+class DuplicationError(frappe.ValidationError):
+ pass
+
class JobApplicant(Document):
def onload(self):
@@ -36,8 +38,8 @@ class JobApplicant(Document):
self.set_status_for_employee_referral()
if not self.applicant_name and self.email_id:
- guess = self.email_id.split('@')[0]
- self.applicant_name = ' '.join([p.capitalize() for p in guess.split('.')])
+ guess = self.email_id.split("@")[0]
+ self.applicant_name = " ".join([p.capitalize() for p in guess.split(".")])
def set_status_for_employee_referral(self):
emp_ref = frappe.get_doc("Employee Referral", self.employee_referral)
@@ -46,6 +48,7 @@ class JobApplicant(Document):
elif self.status in ["Accepted", "Rejected"]:
emp_ref.db_set("status", self.status)
+
@frappe.whitelist()
def create_interview(doc, interview_round):
import json
@@ -59,7 +62,11 @@ def create_interview(doc, interview_round):
round_designation = frappe.db.get_value("Interview Round", interview_round, "designation")
if round_designation and doc.designation and round_designation != doc.designation:
- frappe.throw(_("Interview Round {0} is only applicable for the Designation {1}").format(interview_round, round_designation))
+ frappe.throw(
+ _("Interview Round {0} is only applicable for the Designation {1}").format(
+ interview_round, round_designation
+ )
+ )
interview = frappe.new_doc("Interview")
interview.interview_round = interview_round
@@ -70,16 +77,16 @@ def create_interview(doc, interview_round):
interviewer_detail = get_interviewers(interview_round)
for d in interviewer_detail:
- interview.append("interview_details", {
- "interviewer": d.interviewer
- })
+ interview.append("interview_details", {"interviewer": d.interviewer})
return interview
+
@frappe.whitelist()
def get_interview_details(job_applicant):
- interview_details = frappe.db.get_all("Interview",
- filters={"job_applicant":job_applicant, "docstatus": ["!=", 2]},
- fields=["name", "interview_round", "expected_average_rating", "average_rating", "status"]
+ interview_details = frappe.db.get_all(
+ "Interview",
+ filters={"job_applicant": job_applicant, "docstatus": ["!=", 2]},
+ fields=["name", "interview_round", "expected_average_rating", "average_rating", "status"],
)
interview_detail_map = {}
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py
index 9406fc54855..14b944ac614 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py
+++ b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py
@@ -1,17 +1,9 @@
-
-
def get_data():
return {
- 'fieldname': 'job_applicant',
- 'transactions': [
- {
- 'items': ['Employee', 'Employee Onboarding']
- },
- {
- 'items': ['Job Offer', 'Appointment Letter']
- },
- {
- 'items': ['Interview']
- }
+ "fieldname": "job_applicant",
+ "transactions": [
+ {"items": ["Employee", "Employee Onboarding"]},
+ {"items": ["Job Offer", "Appointment Letter"]},
+ {"items": ["Interview"]},
],
}
diff --git a/erpnext/hr/doctype/job_applicant/test_job_applicant.py b/erpnext/hr/doctype/job_applicant/test_job_applicant.py
index bf1622028d8..99d11619781 100644
--- a/erpnext/hr/doctype/job_applicant/test_job_applicant.py
+++ b/erpnext/hr/doctype/job_applicant/test_job_applicant.py
@@ -10,21 +10,25 @@ from erpnext.hr.doctype.designation.test_designation import create_designation
class TestJobApplicant(unittest.TestCase):
def test_job_applicant_naming(self):
- applicant = frappe.get_doc({
- "doctype": "Job Applicant",
- "status": "Open",
- "applicant_name": "_Test Applicant",
- "email_id": "job_applicant_naming@example.com"
- }).insert()
- self.assertEqual(applicant.name, 'job_applicant_naming@example.com')
+ applicant = frappe.get_doc(
+ {
+ "doctype": "Job Applicant",
+ "status": "Open",
+ "applicant_name": "_Test Applicant",
+ "email_id": "job_applicant_naming@example.com",
+ }
+ ).insert()
+ self.assertEqual(applicant.name, "job_applicant_naming@example.com")
- applicant = frappe.get_doc({
- "doctype": "Job Applicant",
- "status": "Open",
- "applicant_name": "_Test Applicant",
- "email_id": "job_applicant_naming@example.com"
- }).insert()
- self.assertEqual(applicant.name, 'job_applicant_naming@example.com-1')
+ applicant = frappe.get_doc(
+ {
+ "doctype": "Job Applicant",
+ "status": "Open",
+ "applicant_name": "_Test Applicant",
+ "email_id": "job_applicant_naming@example.com",
+ }
+ ).insert()
+ self.assertEqual(applicant.name, "job_applicant_naming@example.com-1")
def tearDown(self):
frappe.db.rollback()
@@ -41,11 +45,13 @@ def create_job_applicant(**args):
if frappe.db.exists("Job Applicant", filters):
return frappe.get_doc("Job Applicant", filters)
- job_applicant = frappe.get_doc({
- "doctype": "Job Applicant",
- "status": args.status or "Open",
- "designation": create_designation().name
- })
+ job_applicant = frappe.get_doc(
+ {
+ "doctype": "Job Applicant",
+ "status": args.status or "Open",
+ "designation": create_designation().name,
+ }
+ )
job_applicant.update(filters)
job_applicant.save()
diff --git a/erpnext/hr/doctype/job_offer/job_offer.py b/erpnext/hr/doctype/job_offer/job_offer.py
index 39f471929b4..dc76a2d0038 100644
--- a/erpnext/hr/doctype/job_offer/job_offer.py
+++ b/erpnext/hr/doctype/job_offer/job_offer.py
@@ -17,9 +17,15 @@ class JobOffer(Document):
def validate(self):
self.validate_vacancies()
- job_offer = frappe.db.exists("Job Offer",{"job_applicant": self.job_applicant, "docstatus": ["!=", 2]})
+ job_offer = frappe.db.exists(
+ "Job Offer", {"job_applicant": self.job_applicant, "docstatus": ["!=", 2]}
+ )
if job_offer and job_offer != self.name:
- frappe.throw(_("Job Offer: {0} is already for Job Applicant: {1}").format(frappe.bold(job_offer), frappe.bold(self.job_applicant)))
+ frappe.throw(
+ _("Job Offer: {0} is already for Job Applicant: {1}").format(
+ frappe.bold(job_offer), frappe.bold(self.job_applicant)
+ )
+ )
def validate_vacancies(self):
staffing_plan = get_staffing_plan_detail(self.designation, self.company, self.offer_date)
@@ -27,7 +33,7 @@ class JobOffer(Document):
if staffing_plan and check_vacancies:
job_offers = self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date)
if not staffing_plan.get("vacancies") or cint(staffing_plan.vacancies) - len(job_offers) <= 0:
- error_variable = 'for ' + frappe.bold(self.designation)
+ error_variable = "for " + frappe.bold(self.designation)
if staffing_plan.get("parent"):
error_variable = frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent))
@@ -37,20 +43,27 @@ class JobOffer(Document):
update_job_applicant(self.status, self.job_applicant)
def get_job_offer(self, from_date, to_date):
- ''' Returns job offer created during a time period '''
- return frappe.get_all("Job Offer", filters={
- "offer_date": ['between', (from_date, to_date)],
+ """Returns job offer created during a time period"""
+ return frappe.get_all(
+ "Job Offer",
+ filters={
+ "offer_date": ["between", (from_date, to_date)],
"designation": self.designation,
"company": self.company,
- "docstatus": 1
- }, fields=['name'])
+ "docstatus": 1,
+ },
+ fields=["name"],
+ )
+
def update_job_applicant(status, job_applicant):
if status in ("Accepted", "Rejected"):
frappe.set_value("Job Applicant", job_applicant, "status", status)
+
def get_staffing_plan_detail(designation, company, offer_date):
- detail = frappe.db.sql("""
+ detail = frappe.db.sql(
+ """
SELECT DISTINCT spd.parent,
sp.from_date as from_date,
sp.to_date as to_date,
@@ -64,20 +77,33 @@ def get_staffing_plan_detail(designation, company, offer_date):
AND sp.company=%s
AND spd.parent = sp.name
AND %s between sp.from_date and sp.to_date
- """, (designation, company, offer_date), as_dict=1)
+ """,
+ (designation, company, offer_date),
+ as_dict=1,
+ )
return frappe._dict(detail[0]) if (detail and detail[0].parent) else None
+
@frappe.whitelist()
def make_employee(source_name, target_doc=None):
def set_missing_values(source, target):
- target.personal_email, target.first_name = frappe.db.get_value("Job Applicant", \
- source.job_applicant, ["email_id", "applicant_name"])
- doc = get_mapped_doc("Job Offer", source_name, {
+ target.personal_email, target.first_name = frappe.db.get_value(
+ "Job Applicant", source.job_applicant, ["email_id", "applicant_name"]
+ )
+
+ doc = get_mapped_doc(
+ "Job Offer",
+ source_name,
+ {
"Job Offer": {
"doctype": "Employee",
"field_map": {
"applicant_name": "employee_name",
- }}
- }, target_doc, set_missing_values)
+ },
+ }
+ },
+ target_doc,
+ set_missing_values,
+ )
return doc
diff --git a/erpnext/hr/doctype/job_offer/test_job_offer.py b/erpnext/hr/doctype/job_offer/test_job_offer.py
index d94e03ca63f..7d8ef115d16 100644
--- a/erpnext/hr/doctype/job_offer/test_job_offer.py
+++ b/erpnext/hr/doctype/job_offer/test_job_offer.py
@@ -12,17 +12,19 @@ from erpnext.hr.doctype.staffing_plan.test_staffing_plan import make_company
# test_records = frappe.get_test_records('Job Offer')
+
class TestJobOffer(unittest.TestCase):
def test_job_offer_creation_against_vacancies(self):
frappe.db.set_value("HR Settings", None, "check_vacancies", 1)
job_applicant = create_job_applicant(email_id="test_job_offer@example.com")
job_offer = create_job_offer(job_applicant=job_applicant.name, designation="UX Designer")
- create_staffing_plan(name='Test No Vacancies', staffing_details=[{
- "designation": "UX Designer",
- "vacancies": 0,
- "estimated_cost_per_position": 5000
- }])
+ create_staffing_plan(
+ name="Test No Vacancies",
+ staffing_details=[
+ {"designation": "UX Designer", "vacancies": 0, "estimated_cost_per_position": 5000}
+ ],
+ )
self.assertRaises(frappe.ValidationError, job_offer.submit)
# test creation of job offer when vacancies are not present
@@ -49,6 +51,7 @@ class TestJobOffer(unittest.TestCase):
def tearDown(self):
frappe.db.sql("DELETE FROM `tabJob Offer` WHERE 1")
+
def create_job_offer(**args):
args = frappe._dict(args)
if not args.job_applicant:
@@ -57,32 +60,34 @@ def create_job_offer(**args):
if not frappe.db.exists("Designation", args.designation):
designation = create_designation(designation_name=args.designation)
- job_offer = frappe.get_doc({
- "doctype": "Job Offer",
- "job_applicant": args.job_applicant or job_applicant.name,
- "offer_date": args.offer_date or nowdate(),
- "designation": args.designation or "Researcher",
- "status": args.status or "Accepted"
- })
+ job_offer = frappe.get_doc(
+ {
+ "doctype": "Job Offer",
+ "job_applicant": args.job_applicant or job_applicant.name,
+ "offer_date": args.offer_date or nowdate(),
+ "designation": args.designation or "Researcher",
+ "status": args.status or "Accepted",
+ }
+ )
return job_offer
+
def create_staffing_plan(**args):
args = frappe._dict(args)
make_company()
frappe.db.set_value("Company", "_Test Company", "is_group", 1)
if frappe.db.exists("Staffing Plan", args.name or "Test"):
return
- staffing_plan = frappe.get_doc({
- "doctype": "Staffing Plan",
- "name": args.name or "Test",
- "from_date": args.from_date or nowdate(),
- "to_date": args.to_date or add_days(nowdate(), 10),
- "staffing_details": args.staffing_details or [{
- "designation": "Researcher",
- "vacancies": 1,
- "estimated_cost_per_position": 50000
- }]
- })
+ staffing_plan = frappe.get_doc(
+ {
+ "doctype": "Staffing Plan",
+ "name": args.name or "Test",
+ "from_date": args.from_date or nowdate(),
+ "to_date": args.to_date or add_days(nowdate(), 10),
+ "staffing_details": args.staffing_details
+ or [{"designation": "Researcher", "vacancies": 1, "estimated_cost_per_position": 50000}],
+ }
+ )
staffing_plan.insert()
staffing_plan.submit()
return staffing_plan
diff --git a/erpnext/hr/doctype/job_opening/job_opening.py b/erpnext/hr/doctype/job_opening/job_opening.py
index d53daf17d87..c71407d71d4 100644
--- a/erpnext/hr/doctype/job_opening/job_opening.py
+++ b/erpnext/hr/doctype/job_opening/job_opening.py
@@ -16,67 +16,78 @@ from erpnext.hr.doctype.staffing_plan.staffing_plan import (
class JobOpening(WebsiteGenerator):
website = frappe._dict(
- template = "templates/generators/job_opening.html",
- condition_field = "publish",
- page_title_field = "job_title",
+ template="templates/generators/job_opening.html",
+ condition_field="publish",
+ page_title_field="job_title",
)
def validate(self):
if not self.route:
- self.route = frappe.scrub(self.job_title).replace('_', '-')
+ self.route = frappe.scrub(self.job_title).replace("_", "-")
self.validate_current_vacancies()
def validate_current_vacancies(self):
if not self.staffing_plan:
- staffing_plan = get_active_staffing_plan_details(self.company,
- self.designation)
+ staffing_plan = get_active_staffing_plan_details(self.company, self.designation)
if staffing_plan:
self.staffing_plan = staffing_plan[0].name
self.planned_vacancies = staffing_plan[0].vacancies
elif not self.planned_vacancies:
- planned_vacancies = frappe.db.sql("""
+ planned_vacancies = frappe.db.sql(
+ """
select vacancies from `tabStaffing Plan Detail`
- where parent=%s and designation=%s""", (self.staffing_plan, self.designation))
+ where parent=%s and designation=%s""",
+ (self.staffing_plan, self.designation),
+ )
self.planned_vacancies = planned_vacancies[0][0] if planned_vacancies else None
if self.staffing_plan and self.planned_vacancies:
staffing_plan_company = frappe.db.get_value("Staffing Plan", self.staffing_plan, "company")
- lft, rgt = frappe.get_cached_value('Company', staffing_plan_company, ["lft", "rgt"])
+ lft, rgt = frappe.get_cached_value("Company", staffing_plan_company, ["lft", "rgt"])
designation_counts = get_designation_counts(self.designation, self.company)
- current_count = designation_counts['employee_count'] + designation_counts['job_openings']
+ current_count = designation_counts["employee_count"] + designation_counts["job_openings"]
if self.planned_vacancies <= current_count:
- frappe.throw(_("Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}").format(
- self.designation, self.staffing_plan))
+ frappe.throw(
+ _(
+ "Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}"
+ ).format(self.designation, self.staffing_plan)
+ )
def get_context(self, context):
- context.parents = [{'route': 'jobs', 'title': _('All Jobs') }]
+ context.parents = [{"route": "jobs", "title": _("All Jobs")}]
+
def get_list_context(context):
context.title = _("Jobs")
- context.introduction = _('Current Job Openings')
+ context.introduction = _("Current Job Openings")
context.get_list = get_job_openings
-def get_job_openings(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by=None):
- fields = ['name', 'status', 'job_title', 'description', 'publish_salary_range',
- 'lower_range', 'upper_range', 'currency', 'job_application_route']
+
+def get_job_openings(
+ doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by=None
+):
+ fields = [
+ "name",
+ "status",
+ "job_title",
+ "description",
+ "publish_salary_range",
+ "lower_range",
+ "upper_range",
+ "currency",
+ "job_application_route",
+ ]
filters = filters or {}
- filters.update({
- 'status': 'Open'
- })
+ filters.update({"status": "Open"})
if txt:
- filters.update({
- 'job_title': ['like', '%{0}%'.format(txt)],
- 'description': ['like', '%{0}%'.format(txt)]
- })
+ filters.update(
+ {"job_title": ["like", "%{0}%".format(txt)], "description": ["like", "%{0}%".format(txt)]}
+ )
- return frappe.get_all(doctype,
- filters,
- fields,
- start=limit_start,
- page_length=limit_page_length,
- order_by=order_by
+ return frappe.get_all(
+ doctype, filters, fields, start=limit_start, page_length=limit_page_length, order_by=order_by
)
diff --git a/erpnext/hr/doctype/job_opening/job_opening_dashboard.py b/erpnext/hr/doctype/job_opening/job_opening_dashboard.py
index 817969004f9..a30932870d0 100644
--- a/erpnext/hr/doctype/job_opening/job_opening_dashboard.py
+++ b/erpnext/hr/doctype/job_opening/job_opening_dashboard.py
@@ -1,11 +1,5 @@
-
-
def get_data():
- return {
- 'fieldname': 'job_title',
- 'transactions': [
- {
- 'items': ['Job Applicant']
- }
- ],
- }
+ return {
+ "fieldname": "job_title",
+ "transactions": [{"items": ["Job Applicant"]}],
+ }
diff --git a/erpnext/hr/doctype/job_opening/test_job_opening.py b/erpnext/hr/doctype/job_opening/test_job_opening.py
index a1c3a1d49e3..a72a6eb3384 100644
--- a/erpnext/hr/doctype/job_opening/test_job_opening.py
+++ b/erpnext/hr/doctype/job_opening/test_job_opening.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Job Opening')
+
class TestJobOpening(unittest.TestCase):
pass
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.js b/erpnext/hr/doctype/leave_allocation/leave_allocation.js
index 9742387c16a..aef44122513 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.js
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.js
@@ -34,6 +34,15 @@ frappe.ui.form.on("Leave Allocation", {
});
}
}
+
+ // make new leaves allocated field read only if allocation is created via leave policy assignment
+ // and leave type is earned leave, since these leaves would be allocated via the scheduler
+ if (frm.doc.leave_policy_assignment) {
+ frappe.db.get_value("Leave Type", frm.doc.leave_type, "is_earned_leave", (r) => {
+ if (r && cint(r.is_earned_leave))
+ frm.set_df_property("new_leaves_allocated", "read_only", 1);
+ });
+ }
},
expire_allocation: function(frm) {
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
index 52ee463db02..4a9b54034af 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
@@ -237,7 +237,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-10-01 15:28:26.335104",
+ "modified": "2022-04-07 09:50:33.145825",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",
@@ -278,5 +278,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
- "timeline_field": "employee"
+ "timeline_field": "employee",
+ "title_field": "employee_name",
+ "track_changes": 1
}
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
index 232118fd67c..27479a5e81f 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
@@ -15,36 +15,65 @@ from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import (
from erpnext.hr.utils import get_leave_period, set_employee_name
-class OverlapError(frappe.ValidationError): pass
-class BackDatedAllocationError(frappe.ValidationError): pass
-class OverAllocationError(frappe.ValidationError): pass
-class LessAllocationError(frappe.ValidationError): pass
-class ValueMultiplierError(frappe.ValidationError): pass
+class OverlapError(frappe.ValidationError):
+ pass
+
+
+class BackDatedAllocationError(frappe.ValidationError):
+ pass
+
+
+class OverAllocationError(frappe.ValidationError):
+ pass
+
+
+class LessAllocationError(frappe.ValidationError):
+ pass
+
+
+class ValueMultiplierError(frappe.ValidationError):
+ pass
+
class LeaveAllocation(Document):
def validate(self):
self.validate_period()
self.validate_allocation_overlap()
- self.validate_back_dated_allocation()
- self.set_total_leaves_allocated()
- self.validate_total_leaves_allocated()
self.validate_lwp()
set_employee_name(self)
+ self.set_total_leaves_allocated()
+ self.validate_leave_days_and_dates()
+
+ def validate_leave_days_and_dates(self):
+ # all validations that should run on save as well as on update after submit
+ self.validate_back_dated_allocation()
+ self.validate_total_leaves_allocated()
self.validate_leave_allocation_days()
def validate_leave_allocation_days(self):
company = frappe.db.get_value("Employee", self.employee, "company")
leave_period = get_leave_period(self.from_date, self.to_date, company)
- max_leaves_allowed = flt(frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed"))
+ max_leaves_allowed = flt(
+ frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed")
+ )
if max_leaves_allowed > 0:
leave_allocated = 0
if leave_period:
- leave_allocated = get_leave_allocation_for_period(self.employee, self.leave_type,
- leave_period[0].from_date, leave_period[0].to_date)
+ leave_allocated = get_leave_allocation_for_period(
+ self.employee,
+ self.leave_type,
+ leave_period[0].from_date,
+ leave_period[0].to_date,
+ exclude_allocation=self.name,
+ )
leave_allocated += flt(self.new_leaves_allocated)
if leave_allocated > max_leaves_allowed:
- frappe.throw(_("Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period")
- .format(self.leave_type, self.employee))
+ frappe.throw(
+ _(
+ "Total allocated leaves are more than maximum allocation allowed for {0} leave type for employee {1} in the period"
+ ).format(self.leave_type, self.employee),
+ OverAllocationError,
+ )
def on_submit(self):
self.create_leave_ledger_entry()
@@ -64,25 +93,34 @@ class LeaveAllocation(Document):
def on_update_after_submit(self):
if self.has_value_changed("new_leaves_allocated"):
self.validate_against_leave_applications()
+
+ # recalculate total leaves allocated
+ self.total_leaves_allocated = flt(self.unused_leaves) + flt(self.new_leaves_allocated)
+ # run required validations again since total leaves are being updated
+ self.validate_leave_days_and_dates()
+
leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count()
args = {
"leaves": leaves_to_be_added,
"from_date": self.from_date,
"to_date": self.to_date,
- "is_carry_forward": 0
+ "is_carry_forward": 0,
}
create_leave_ledger_entry(self, args, True)
+ self.db_update()
def get_existing_leave_count(self):
- ledger_entries = frappe.get_all("Leave Ledger Entry",
- filters={
- "transaction_type": "Leave Allocation",
- "transaction_name": self.name,
- "employee": self.employee,
- "company": self.company,
- "leave_type": self.leave_type
- },
- pluck="leaves")
+ ledger_entries = frappe.get_all(
+ "Leave Ledger Entry",
+ filters={
+ "transaction_type": "Leave Allocation",
+ "transaction_name": self.name,
+ "employee": self.employee,
+ "company": self.company,
+ "leave_type": self.leave_type,
+ },
+ pluck="leaves",
+ )
total_existing_leaves = 0
for entry in ledger_entries:
total_existing_leaves += entry
@@ -90,21 +128,33 @@ class LeaveAllocation(Document):
return total_existing_leaves
def validate_against_leave_applications(self):
- leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type,
- self.from_date, self.to_date)
+ leaves_taken = get_approved_leaves_for_period(
+ self.employee, self.leave_type, self.from_date, self.to_date
+ )
if flt(leaves_taken) > flt(self.total_leaves_allocated):
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
- frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken))
+ frappe.msgprint(
+ _(
+ "Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period"
+ ).format(self.total_leaves_allocated, leaves_taken)
+ )
else:
- frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError)
+ frappe.throw(
+ _(
+ "Total allocated leaves {0} cannot be less than already approved leaves {1} for the period"
+ ).format(self.total_leaves_allocated, leaves_taken),
+ LessAllocationError,
+ )
def update_leave_policy_assignments_when_no_allocations_left(self):
- allocations = frappe.db.get_list("Leave Allocation", filters = {
- "docstatus": 1,
- "leave_policy_assignment": self.leave_policy_assignment
- })
+ allocations = frappe.db.get_list(
+ "Leave Allocation",
+ filters={"docstatus": 1, "leave_policy_assignment": self.leave_policy_assignment},
+ )
if len(allocations) == 0:
- frappe.db.set_value("Leave Policy Assignment", self.leave_policy_assignment ,"leaves_allocated", 0)
+ frappe.db.set_value(
+ "Leave Policy Assignment", self.leave_policy_assignment, "leaves_allocated", 0
+ )
def validate_period(self):
if date_diff(self.to_date, self.from_date) <= 0:
@@ -112,10 +162,13 @@ class LeaveAllocation(Document):
def validate_lwp(self):
if frappe.db.get_value("Leave Type", self.leave_type, "is_lwp"):
- frappe.throw(_("Leave Type {0} cannot be allocated since it is leave without pay").format(self.leave_type))
+ frappe.throw(
+ _("Leave Type {0} cannot be allocated since it is leave without pay").format(self.leave_type)
+ )
def validate_allocation_overlap(self):
- leave_allocation = frappe.db.sql("""
+ leave_allocation = frappe.db.sql(
+ """
SELECT
name
FROM `tabLeave Allocation`
@@ -123,29 +176,44 @@ class LeaveAllocation(Document):
employee=%s AND leave_type=%s
AND name <> %s AND docstatus=1
AND to_date >= %s AND from_date <= %s""",
- (self.employee, self.leave_type, self.name, self.from_date, self.to_date))
+ (self.employee, self.leave_type, self.name, self.from_date, self.to_date),
+ )
if leave_allocation:
- frappe.msgprint(_("{0} already allocated for Employee {1} for period {2} to {3}")
- .format(self.leave_type, self.employee, formatdate(self.from_date), formatdate(self.to_date)))
+ frappe.msgprint(
+ _("{0} already allocated for Employee {1} for period {2} to {3}").format(
+ self.leave_type, self.employee, formatdate(self.from_date), formatdate(self.to_date)
+ )
+ )
- frappe.throw(_('Reference') + ': {0}'
- .format(leave_allocation[0][0]), OverlapError)
+ frappe.throw(
+ _("Reference")
+ + ': {0}'.format(leave_allocation[0][0]),
+ OverlapError,
+ )
def validate_back_dated_allocation(self):
- future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation`
+ future_allocation = frappe.db.sql(
+ """select name, from_date from `tabLeave Allocation`
where employee=%s and leave_type=%s and docstatus=1 and from_date > %s
- and carry_forward=1""", (self.employee, self.leave_type, self.to_date), as_dict=1)
+ and carry_forward=1""",
+ (self.employee, self.leave_type, self.to_date),
+ as_dict=1,
+ )
if future_allocation:
- frappe.throw(_("Leave cannot be allocated before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}")
- .format(formatdate(future_allocation[0].from_date), future_allocation[0].name),
- BackDatedAllocationError)
+ frappe.throw(
+ _(
+ "Leave cannot be allocated before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}"
+ ).format(formatdate(future_allocation[0].from_date), future_allocation[0].name),
+ BackDatedAllocationError,
+ )
@frappe.whitelist()
def set_total_leaves_allocated(self):
- self.unused_leaves = get_carry_forwarded_leaves(self.employee,
- self.leave_type, self.from_date, self.carry_forward)
+ self.unused_leaves = get_carry_forwarded_leaves(
+ self.employee, self.leave_type, self.from_date, self.carry_forward
+ )
self.total_leaves_allocated = flt(self.unused_leaves) + flt(self.new_leaves_allocated)
@@ -154,11 +222,14 @@ class LeaveAllocation(Document):
if self.carry_forward:
self.set_carry_forwarded_leaves_in_previous_allocation()
- if not self.total_leaves_allocated \
- and not frappe.db.get_value("Leave Type", self.leave_type, "is_earned_leave") \
- and not frappe.db.get_value("Leave Type", self.leave_type, "is_compensatory"):
- frappe.throw(_("Total leaves allocated is mandatory for Leave Type {0}")
- .format(self.leave_type))
+ if (
+ not self.total_leaves_allocated
+ and not frappe.db.get_value("Leave Type", self.leave_type, "is_earned_leave")
+ and not frappe.db.get_value("Leave Type", self.leave_type, "is_compensatory")
+ ):
+ frappe.throw(
+ _("Total leaves allocated is mandatory for Leave Type {0}").format(self.leave_type)
+ )
def limit_carry_forward_based_on_max_allowed_leaves(self):
max_leaves_allowed = frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed")
@@ -167,13 +238,17 @@ class LeaveAllocation(Document):
self.unused_leaves = max_leaves_allowed - flt(self.new_leaves_allocated)
def set_carry_forwarded_leaves_in_previous_allocation(self, on_cancel=False):
- ''' Set carry forwarded leaves in previous allocation '''
+ """Set carry forwarded leaves in previous allocation"""
previous_allocation = get_previous_allocation(self.from_date, self.leave_type, self.employee)
if on_cancel:
self.unused_leaves = 0.0
if previous_allocation:
- frappe.db.set_value("Leave Allocation", previous_allocation.name,
- 'carry_forwarded_leaves_count', self.unused_leaves)
+ frappe.db.set_value(
+ "Leave Allocation",
+ previous_allocation.name,
+ "carry_forwarded_leaves_count",
+ self.unused_leaves,
+ )
def validate_total_leaves_allocated(self):
# Adding a day to include To Date in the difference
@@ -183,13 +258,15 @@ class LeaveAllocation(Document):
def create_leave_ledger_entry(self, submit=True):
if self.unused_leaves:
- expiry_days = frappe.db.get_value("Leave Type", self.leave_type, "expire_carry_forwarded_leaves_after_days")
+ expiry_days = frappe.db.get_value(
+ "Leave Type", self.leave_type, "expire_carry_forwarded_leaves_after_days"
+ )
end_date = add_days(self.from_date, expiry_days - 1) if expiry_days else self.to_date
args = dict(
leaves=self.unused_leaves,
from_date=self.from_date,
- to_date= min(getdate(end_date), getdate(self.to_date)),
- is_carry_forward=1
+ to_date=min(getdate(end_date), getdate(self.to_date)),
+ is_carry_forward=1,
)
create_leave_ledger_entry(self, args, submit)
@@ -197,74 +274,85 @@ class LeaveAllocation(Document):
leaves=self.new_leaves_allocated,
from_date=self.from_date,
to_date=self.to_date,
- is_carry_forward=0
+ is_carry_forward=0,
)
create_leave_ledger_entry(self, args, submit)
+
def get_previous_allocation(from_date, leave_type, employee):
- ''' Returns document properties of previous allocation '''
- return frappe.db.get_value("Leave Allocation",
+ """Returns document properties of previous allocation"""
+ return frappe.db.get_value(
+ "Leave Allocation",
filters={
- 'to_date': ("<", from_date),
- 'leave_type': leave_type,
- 'employee': employee,
- 'docstatus': 1
+ "to_date": ("<", from_date),
+ "leave_type": leave_type,
+ "employee": employee,
+ "docstatus": 1,
},
- order_by='to_date DESC',
- fieldname=['name', 'from_date', 'to_date', 'employee', 'leave_type'], as_dict=1)
+ order_by="to_date DESC",
+ fieldname=["name", "from_date", "to_date", "employee", "leave_type"],
+ as_dict=1,
+ )
-def get_leave_allocation_for_period(employee, leave_type, from_date, to_date):
- leave_allocated = 0
- leave_allocations = frappe.db.sql("""
- select employee, leave_type, from_date, to_date, total_leaves_allocated
- from `tabLeave Allocation`
- where employee=%(employee)s and leave_type=%(leave_type)s
- and docstatus=1
- and (from_date between %(from_date)s and %(to_date)s
- or to_date between %(from_date)s and %(to_date)s
- or (from_date < %(from_date)s and to_date > %(to_date)s))
- """, {
- "from_date": from_date,
- "to_date": to_date,
- "employee": employee,
- "leave_type": leave_type
- }, as_dict=1)
- if leave_allocations:
- for leave_alloc in leave_allocations:
- leave_allocated += leave_alloc.total_leaves_allocated
+def get_leave_allocation_for_period(
+ employee, leave_type, from_date, to_date, exclude_allocation=None
+):
+ from frappe.query_builder.functions import Sum
+
+ Allocation = frappe.qb.DocType("Leave Allocation")
+ return (
+ frappe.qb.from_(Allocation)
+ .select(Sum(Allocation.total_leaves_allocated).as_("total_allocated_leaves"))
+ .where(
+ (Allocation.employee == employee)
+ & (Allocation.leave_type == leave_type)
+ & (Allocation.docstatus == 1)
+ & (Allocation.name != exclude_allocation)
+ & (
+ (Allocation.from_date.between(from_date, to_date))
+ | (Allocation.to_date.between(from_date, to_date))
+ | ((Allocation.from_date < from_date) & (Allocation.to_date > to_date))
+ )
+ )
+ ).run()[0][0] or 0.0
- return leave_allocated
@frappe.whitelist()
def get_carry_forwarded_leaves(employee, leave_type, date, carry_forward=None):
- ''' Returns carry forwarded leaves for the given employee '''
+ """Returns carry forwarded leaves for the given employee"""
unused_leaves = 0.0
previous_allocation = get_previous_allocation(date, leave_type, employee)
if carry_forward and previous_allocation:
validate_carry_forward(leave_type)
- unused_leaves = get_unused_leaves(employee, leave_type,
- previous_allocation.from_date, previous_allocation.to_date)
+ unused_leaves = get_unused_leaves(
+ employee, leave_type, previous_allocation.from_date, previous_allocation.to_date
+ )
if unused_leaves:
- max_carry_forwarded_leaves = frappe.db.get_value("Leave Type",
- leave_type, "maximum_carry_forwarded_leaves")
+ max_carry_forwarded_leaves = frappe.db.get_value(
+ "Leave Type", leave_type, "maximum_carry_forwarded_leaves"
+ )
if max_carry_forwarded_leaves and unused_leaves > flt(max_carry_forwarded_leaves):
unused_leaves = flt(max_carry_forwarded_leaves)
return unused_leaves
+
def get_unused_leaves(employee, leave_type, from_date, to_date):
- ''' Returns unused leaves between the given period while skipping leave allocation expiry '''
- leaves = frappe.get_all("Leave Ledger Entry", filters={
- 'employee': employee,
- 'leave_type': leave_type,
- 'from_date': ('>=', from_date),
- 'to_date': ('<=', to_date)
- }, or_filters={
- 'is_expired': 0,
- 'is_carry_forward': 1
- }, fields=['sum(leaves) as leaves'])
- return flt(leaves[0]['leaves'])
+ """Returns unused leaves between the given period while skipping leave allocation expiry"""
+ leaves = frappe.get_all(
+ "Leave Ledger Entry",
+ filters={
+ "employee": employee,
+ "leave_type": leave_type,
+ "from_date": (">=", from_date),
+ "to_date": ("<=", to_date),
+ },
+ or_filters={"is_expired": 0, "is_carry_forward": 1},
+ fields=["sum(leaves) as leaves"],
+ )
+ return flt(leaves[0]["leaves"])
+
def validate_carry_forward(leave_type):
if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"):
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py b/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py
index 08861b8bce3..96e81db617c 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py
@@ -1,19 +1,6 @@
-
-
def get_data():
- return {
- 'fieldname': 'leave_allocation',
- 'transactions': [
- {
- 'items': ['Compensatory Leave Request']
- },
- {
- 'items': ['Leave Encashment']
- }
- ],
- 'reports': [
- {
- 'items': ['Employee Leave Balance']
- }
- ]
- }
+ return {
+ "fieldname": "leave_allocation",
+ "transactions": [{"items": ["Compensatory Leave Request"]}, {"items": ["Leave Encashment"]}],
+ "reports": [{"items": ["Employee Leave Balance"]}],
+ }
diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
index 1310ca65ecf..dde52d7ad8e 100644
--- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
@@ -1,25 +1,26 @@
-
import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, add_months, getdate, nowdate
import erpnext
from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.leave_allocation.leave_allocation import (
+ BackDatedAllocationError,
+ OverAllocationError,
+)
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
-class TestLeaveAllocation(unittest.TestCase):
- @classmethod
- def setUpClass(cls):
- frappe.db.sql("delete from `tabLeave Period`")
+class TestLeaveAllocation(FrappeTestCase):
+ def setUp(self):
+ frappe.db.delete("Leave Period")
+ frappe.db.delete("Leave Allocation")
- emp_id = make_employee("test_emp_leave_allocation@salary.com")
- cls.employee = frappe.get_doc("Employee", emp_id)
-
- def tearDown(self):
- frappe.db.rollback()
+ emp_id = make_employee("test_emp_leave_allocation@salary.com", company="_Test Company")
+ self.employee = frappe.get_doc("Employee", emp_id)
def test_overlapping_allocation(self):
leaves = [
@@ -32,7 +33,7 @@ class TestLeaveAllocation(unittest.TestCase):
"from_date": getdate("2015-10-01"),
"to_date": getdate("2015-10-31"),
"new_leaves_allocated": 5,
- "docstatus": 1
+ "docstatus": 1,
},
{
"doctype": "Leave Allocation",
@@ -42,42 +43,174 @@ class TestLeaveAllocation(unittest.TestCase):
"leave_type": "_Test Leave Type",
"from_date": getdate("2015-09-01"),
"to_date": getdate("2015-11-30"),
- "new_leaves_allocated": 5
- }
+ "new_leaves_allocated": 5,
+ },
]
frappe.get_doc(leaves[0]).save()
self.assertRaises(frappe.ValidationError, frappe.get_doc(leaves[1]).save)
def test_invalid_period(self):
- doc = frappe.get_doc({
- "doctype": "Leave Allocation",
- "__islocal": 1,
- "employee": self.employee.name,
- "employee_name": self.employee.employee_name,
- "leave_type": "_Test Leave Type",
- "from_date": getdate("2015-09-30"),
- "to_date": getdate("2015-09-1"),
- "new_leaves_allocated": 5
- })
+ doc = frappe.get_doc(
+ {
+ "doctype": "Leave Allocation",
+ "__islocal": 1,
+ "employee": self.employee.name,
+ "employee_name": self.employee.employee_name,
+ "leave_type": "_Test Leave Type",
+ "from_date": getdate("2015-09-30"),
+ "to_date": getdate("2015-09-1"),
+ "new_leaves_allocated": 5,
+ }
+ )
# invalid period
self.assertRaises(frappe.ValidationError, doc.save)
- def test_allocated_leave_days_over_period(self):
- doc = frappe.get_doc({
- "doctype": "Leave Allocation",
- "__islocal": 1,
- "employee": self.employee.name,
- "employee_name": self.employee.employee_name,
- "leave_type": "_Test Leave Type",
- "from_date": getdate("2015-09-1"),
- "to_date": getdate("2015-09-30"),
- "new_leaves_allocated": 35
- })
+ def test_validation_for_over_allocation(self):
+ doc = frappe.get_doc(
+ {
+ "doctype": "Leave Allocation",
+ "__islocal": 1,
+ "employee": self.employee.name,
+ "employee_name": self.employee.employee_name,
+ "leave_type": "_Test Leave Type",
+ "from_date": getdate("2015-09-1"),
+ "to_date": getdate("2015-09-30"),
+ "new_leaves_allocated": 35,
+ }
+ )
# allocated leave more than period
- self.assertRaises(frappe.ValidationError, doc.save)
+ self.assertRaises(OverAllocationError, doc.save)
+
+ def test_validation_for_over_allocation_post_submission(self):
+ allocation = frappe.get_doc(
+ {
+ "doctype": "Leave Allocation",
+ "__islocal": 1,
+ "employee": self.employee.name,
+ "employee_name": self.employee.employee_name,
+ "leave_type": "_Test Leave Type",
+ "from_date": getdate("2015-09-1"),
+ "to_date": getdate("2015-09-30"),
+ "new_leaves_allocated": 15,
+ }
+ ).submit()
+ allocation.reload()
+ # allocated leaves more than period after submission
+ allocation.new_leaves_allocated = 35
+ self.assertRaises(OverAllocationError, allocation.save)
+
+ def test_validation_for_over_allocation_based_on_leave_setup(self):
+ frappe.delete_doc_if_exists("Leave Period", "Test Allocation Period")
+ leave_period = frappe.get_doc(
+ dict(
+ name="Test Allocation Period",
+ doctype="Leave Period",
+ from_date=add_months(nowdate(), -6),
+ to_date=add_months(nowdate(), 6),
+ company="_Test Company",
+ is_active=1,
+ )
+ ).insert()
+
+ leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1)
+ leave_type.max_leaves_allowed = 25
+ leave_type.save()
+
+ # 15 leaves allocated in this period
+ allocation = create_leave_allocation(
+ leave_type=leave_type.name,
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name,
+ from_date=leave_period.from_date,
+ to_date=nowdate(),
+ )
+ allocation.submit()
+
+ # trying to allocate additional 15 leaves
+ allocation = create_leave_allocation(
+ leave_type=leave_type.name,
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name,
+ from_date=add_days(nowdate(), 1),
+ to_date=leave_period.to_date,
+ )
+ self.assertRaises(OverAllocationError, allocation.save)
+
+ def test_validation_for_over_allocation_based_on_leave_setup_post_submission(self):
+ frappe.delete_doc_if_exists("Leave Period", "Test Allocation Period")
+ leave_period = frappe.get_doc(
+ dict(
+ name="Test Allocation Period",
+ doctype="Leave Period",
+ from_date=add_months(nowdate(), -6),
+ to_date=add_months(nowdate(), 6),
+ company="_Test Company",
+ is_active=1,
+ )
+ ).insert()
+
+ leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1)
+ leave_type.max_leaves_allowed = 30
+ leave_type.save()
+
+ # 15 leaves allocated
+ allocation = create_leave_allocation(
+ leave_type=leave_type.name,
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name,
+ from_date=leave_period.from_date,
+ to_date=nowdate(),
+ )
+ allocation.submit()
+ allocation.reload()
+
+ # allocate additional 15 leaves
+ allocation = create_leave_allocation(
+ leave_type=leave_type.name,
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name,
+ from_date=add_days(nowdate(), 1),
+ to_date=leave_period.to_date,
+ )
+ allocation.submit()
+ allocation.reload()
+
+ # trying to allocate 25 leaves in 2nd alloc within leave period
+ # total leaves = 40 which is more than `max_leaves_allowed` setting i.e. 30
+ allocation.new_leaves_allocated = 25
+ self.assertRaises(OverAllocationError, allocation.save)
+
+ def test_validate_back_dated_allocation_update(self):
+ leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
+ leave_type.save()
+
+ # initial leave allocation = 15
+ leave_allocation = create_leave_allocation(
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name,
+ leave_type="_Test_CF_leave",
+ from_date=add_months(nowdate(), -12),
+ to_date=add_months(nowdate(), -1),
+ carry_forward=0,
+ )
+ leave_allocation.submit()
+
+ # new_leaves = 15, carry_forwarded = 10
+ leave_allocation_1 = create_leave_allocation(
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name,
+ leave_type="_Test_CF_leave",
+ carry_forward=1,
+ )
+ leave_allocation_1.submit()
+
+ # try updating initial leave allocation
+ leave_allocation.reload()
+ leave_allocation.new_leaves_allocated = 20
+ self.assertRaises(BackDatedAllocationError, leave_allocation.save)
def test_carry_forward_calculation(self):
leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
@@ -92,7 +225,8 @@ class TestLeaveAllocation(unittest.TestCase):
leave_type="_Test_CF_leave",
from_date=add_months(nowdate(), -12),
to_date=add_months(nowdate(), -1),
- carry_forward=0)
+ carry_forward=0,
+ )
leave_allocation.submit()
# carry forwarded leaves considering maximum_carry_forwarded_leaves
@@ -101,10 +235,13 @@ class TestLeaveAllocation(unittest.TestCase):
employee=self.employee.name,
employee_name=self.employee.employee_name,
leave_type="_Test_CF_leave",
- carry_forward=1)
+ carry_forward=1,
+ )
leave_allocation_1.submit()
+ leave_allocation_1.reload()
self.assertEqual(leave_allocation_1.unused_leaves, 10)
+ self.assertEqual(leave_allocation_1.total_leaves_allocated, 25)
leave_allocation_1.cancel()
@@ -115,7 +252,8 @@ class TestLeaveAllocation(unittest.TestCase):
employee_name=self.employee.employee_name,
leave_type="_Test_CF_leave",
carry_forward=1,
- new_leaves_allocated=25)
+ new_leaves_allocated=25,
+ )
leave_allocation_2.submit()
self.assertEqual(leave_allocation_2.unused_leaves, 5)
@@ -124,7 +262,8 @@ class TestLeaveAllocation(unittest.TestCase):
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
- expire_carry_forwarded_leaves_after_days=90)
+ expire_carry_forwarded_leaves_after_days=90,
+ )
leave_type.save()
# initial leave allocation
@@ -134,7 +273,8 @@ class TestLeaveAllocation(unittest.TestCase):
leave_type="_Test_CF_leave_expiry",
from_date=add_months(nowdate(), -24),
to_date=add_months(nowdate(), -12),
- carry_forward=0)
+ carry_forward=0,
+ )
leave_allocation.submit()
leave_allocation = create_leave_allocation(
@@ -143,7 +283,8 @@ class TestLeaveAllocation(unittest.TestCase):
leave_type="_Test_CF_leave_expiry",
from_date=add_days(nowdate(), -90),
to_date=add_days(nowdate(), 100),
- carry_forward=1)
+ carry_forward=1,
+ )
leave_allocation.submit()
# expires all the carry forwarded leaves after 90 days
@@ -156,19 +297,21 @@ class TestLeaveAllocation(unittest.TestCase):
leave_type="_Test_CF_leave_expiry",
carry_forward=1,
from_date=add_months(nowdate(), 6),
- to_date=add_months(nowdate(), 12))
+ to_date=add_months(nowdate(), 12),
+ )
leave_allocation_1.submit()
self.assertEqual(leave_allocation_1.unused_leaves, leave_allocation.new_leaves_allocated)
def test_creation_of_leave_ledger_entry_on_submit(self):
leave_allocation = create_leave_allocation(
- employee=self.employee.name,
- employee_name=self.employee.employee_name
+ employee=self.employee.name, employee_name=self.employee.employee_name
)
leave_allocation.submit()
- leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_allocation.name))
+ leave_ledger_entry = frappe.get_all(
+ "Leave Ledger Entry", fields="*", filters=dict(transaction_name=leave_allocation.name)
+ )
self.assertEqual(len(leave_ledger_entry), 1)
self.assertEqual(leave_ledger_entry[0].employee, leave_allocation.employee)
@@ -177,54 +320,63 @@ class TestLeaveAllocation(unittest.TestCase):
# check if leave ledger entry is deleted on cancellation
leave_allocation.cancel()
- self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
+ self.assertFalse(
+ frappe.db.exists("Leave Ledger Entry", {"transaction_name": leave_allocation.name})
+ )
def test_leave_addition_after_submit(self):
leave_allocation = create_leave_allocation(
- employee=self.employee.name,
- employee_name=self.employee.employee_name
+ employee=self.employee.name, employee_name=self.employee.employee_name
)
leave_allocation.submit()
+ leave_allocation.reload()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
+
leave_allocation.new_leaves_allocated = 40
leave_allocation.submit()
+ leave_allocation.reload()
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
def test_leave_subtraction_after_submit(self):
leave_allocation = create_leave_allocation(
- employee=self.employee.name,
- employee_name=self.employee.employee_name
+ employee=self.employee.name, employee_name=self.employee.employee_name
)
leave_allocation.submit()
+ leave_allocation.reload()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
+
leave_allocation.new_leaves_allocated = 10
leave_allocation.submit()
+ leave_allocation.reload()
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
def test_validation_against_leave_application_after_submit(self):
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
make_holiday_list()
- frappe.db.set_value("Company", self.employee.company, "default_holiday_list", "Salary Slip Test Holiday List")
+ frappe.db.set_value(
+ "Company", self.employee.company, "default_holiday_list", "Salary Slip Test Holiday List"
+ )
leave_allocation = create_leave_allocation(
- employee=self.employee.name,
- employee_name=self.employee.employee_name
+ employee=self.employee.name, employee_name=self.employee.employee_name
)
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
- leave_application = frappe.get_doc({
- "doctype": 'Leave Application',
- "employee": self.employee.name,
- "leave_type": "_Test Leave Type",
- "from_date": add_months(nowdate(), 2),
- "to_date": add_months(add_days(nowdate(), 10), 2),
- "company": self.employee.company,
- "docstatus": 1,
- "status": "Approved",
- "leave_approver": 'test@example.com'
- })
+ leave_application = frappe.get_doc(
+ {
+ "doctype": "Leave Application",
+ "employee": self.employee.name,
+ "leave_type": "_Test Leave Type",
+ "from_date": add_months(nowdate(), 2),
+ "to_date": add_months(add_days(nowdate(), 10), 2),
+ "company": self.employee.company,
+ "docstatus": 1,
+ "status": "Approved",
+ "leave_approver": "test@example.com",
+ }
+ )
leave_application.submit()
leave_application.reload()
@@ -233,22 +385,26 @@ class TestLeaveAllocation(unittest.TestCase):
leave_allocation.total_leaves_allocated = leave_application.total_leave_days - 1
self.assertRaises(frappe.ValidationError, leave_allocation.submit)
+
def create_leave_allocation(**args):
args = frappe._dict(args)
emp_id = make_employee("test_emp_leave_allocation@salary.com")
employee = frappe.get_doc("Employee", emp_id)
- return frappe.get_doc({
- "doctype": "Leave Allocation",
- "__islocal": 1,
- "employee": args.employee or employee.name,
- "employee_name": args.employee_name or employee.employee_name,
- "leave_type": args.leave_type or "_Test Leave Type",
- "from_date": args.from_date or nowdate(),
- "new_leaves_allocated": args.new_leaves_allocated or 15,
- "carry_forward": args.carry_forward or 0,
- "to_date": args.to_date or add_months(nowdate(), 12)
- })
+ return frappe.get_doc(
+ {
+ "doctype": "Leave Allocation",
+ "__islocal": 1,
+ "employee": args.employee or employee.name,
+ "employee_name": args.employee_name or employee.employee_name,
+ "leave_type": args.leave_type or "_Test Leave Type",
+ "from_date": args.from_date or nowdate(),
+ "new_leaves_allocated": args.new_leaves_allocated or 15,
+ "carry_forward": args.carry_forward or 0,
+ "to_date": args.to_date or add_months(nowdate(), 12),
+ }
+ )
+
test_dependencies = ["Employee", "Leave Type"]
diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js
index 9e8cb5516f3..85997a4087f 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.js
+++ b/erpnext/hr/doctype/leave_application/leave_application.js
@@ -52,7 +52,7 @@ frappe.ui.form.on("Leave Application", {
make_dashboard: function(frm) {
var leave_details;
let lwps;
- if (frm.doc.employee) {
+ if (frm.doc.employee && frm.doc.from_date) {
frappe.call({
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details",
async: false,
@@ -146,6 +146,7 @@ frappe.ui.form.on("Leave Application", {
},
to_date: function(frm) {
+ frm.trigger("make_dashboard");
frm.trigger("half_day_datepicker");
frm.trigger("calculate_total_days");
},
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 70250f5bcf8..e6fc2e6fc06 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -1,9 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
+from typing import Dict, Optional, Tuple
import frappe
from frappe import _
+from frappe.query_builder.functions import Max, Min, Sum
from frappe.utils import (
add_days,
cint,
@@ -30,10 +32,29 @@ from erpnext.hr.utils import (
)
-class LeaveDayBlockedError(frappe.ValidationError): pass
-class OverlapError(frappe.ValidationError): pass
-class AttendanceAlreadyMarkedError(frappe.ValidationError): pass
-class NotAnOptionalHoliday(frappe.ValidationError): pass
+class LeaveDayBlockedError(frappe.ValidationError):
+ pass
+
+
+class OverlapError(frappe.ValidationError):
+ pass
+
+
+class AttendanceAlreadyMarkedError(frappe.ValidationError):
+ pass
+
+
+class NotAnOptionalHoliday(frappe.ValidationError):
+ pass
+
+
+class InsufficientLeaveBalanceError(frappe.ValidationError):
+ pass
+
+
+class LeaveAcrossAllocationsError(frappe.ValidationError):
+ pass
+
from frappe.model.document import Document
@@ -54,7 +75,7 @@ class LeaveApplication(Document):
self.validate_salary_processed_days()
self.validate_attendance()
self.set_half_day_date()
- if frappe.db.get_value("Leave Type", self.leave_type, 'is_optional_leave'):
+ if frappe.db.get_value("Leave Type", self.leave_type, "is_optional_leave"):
self.validate_optional_leave()
self.validate_applicable_after()
@@ -68,7 +89,9 @@ class LeaveApplication(Document):
def on_submit(self):
if self.status == "Open":
- frappe.throw(_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted"))
+ frappe.throw(
+ _("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")
+ )
self.validate_back_dated_application()
self.update_attendance()
@@ -95,7 +118,9 @@ class LeaveApplication(Document):
leave_type = frappe.get_doc("Leave Type", self.leave_type)
if leave_type.applicable_after > 0:
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
- leave_days = get_approved_leaves_for_period(self.employee, False, date_of_joining, self.from_date)
+ leave_days = get_approved_leaves_for_period(
+ self.employee, False, date_of_joining, self.from_date
+ )
number_of_days = date_diff(getdate(self.from_date), date_of_joining)
if number_of_days >= 0:
holidays = 0
@@ -103,29 +128,48 @@ class LeaveApplication(Document):
holidays = get_holidays(self.employee, date_of_joining, self.from_date)
number_of_days = number_of_days - leave_days - holidays
if number_of_days < leave_type.applicable_after:
- frappe.throw(_("{0} applicable after {1} working days").format(self.leave_type, leave_type.applicable_after))
+ frappe.throw(
+ _("{0} applicable after {1} working days").format(
+ self.leave_type, leave_type.applicable_after
+ )
+ )
def validate_dates(self):
if frappe.db.get_single_value("HR Settings", "restrict_backdated_leave_application"):
if self.from_date and getdate(self.from_date) < getdate():
- allowed_role = frappe.db.get_single_value("HR Settings", "role_allowed_to_create_backdated_leave_application")
+ allowed_role = frappe.db.get_single_value(
+ "HR Settings", "role_allowed_to_create_backdated_leave_application"
+ )
user = frappe.get_doc("User", frappe.session.user)
user_roles = [d.role for d in user.roles]
if not allowed_role:
- frappe.throw(_("Backdated Leave Application is restricted. Please set the {} in {}").format(
- frappe.bold("Role Allowed to Create Backdated Leave Application"), get_link_to_form("HR Settings", "HR Settings")))
+ frappe.throw(
+ _("Backdated Leave Application is restricted. Please set the {} in {}").format(
+ frappe.bold("Role Allowed to Create Backdated Leave Application"),
+ get_link_to_form("HR Settings", "HR Settings"),
+ )
+ )
- if (allowed_role and allowed_role not in user_roles):
- frappe.throw(_("Only users with the {0} role can create backdated leave applications").format(allowed_role))
+ if allowed_role and allowed_role not in user_roles:
+ frappe.throw(
+ _("Only users with the {0} role can create backdated leave applications").format(
+ allowed_role
+ )
+ )
if self.from_date and self.to_date and (getdate(self.to_date) < getdate(self.from_date)):
frappe.throw(_("To date cannot be before from date"))
- if self.half_day and self.half_day_date \
- and (getdate(self.half_day_date) < getdate(self.from_date)
- or getdate(self.half_day_date) > getdate(self.to_date)):
+ if (
+ self.half_day
+ and self.half_day_date
+ and (
+ getdate(self.half_day_date) < getdate(self.from_date)
+ or getdate(self.half_day_date) > getdate(self.to_date)
+ )
+ ):
- frappe.throw(_("Half Day Date should be between From Date and To Date"))
+ frappe.throw(_("Half Day Date should be between From Date and To Date"))
if not is_lwp(self.leave_type):
self.validate_dates_across_allocation()
@@ -134,30 +178,55 @@ class LeaveApplication(Document):
def validate_dates_across_allocation(self):
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
return
- def _get_leave_allocation_record(date):
- allocation = frappe.db.sql("""select name from `tabLeave Allocation`
- where employee=%s and leave_type=%s and docstatus=1
- and %s between from_date and to_date""", (self.employee, self.leave_type, date))
- return allocation and allocation[0][0]
+ alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates()
+
+ if not (alloc_on_from_date or alloc_on_to_date):
+ frappe.throw(_("Application period cannot be outside leave allocation period"))
+ elif self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date):
+ frappe.throw(
+ _("Application period cannot be across two allocation records"),
+ exc=LeaveAcrossAllocationsError,
+ )
+
+ def get_allocation_based_on_application_dates(self) -> Tuple[Dict, Dict]:
+ """Returns allocation name, from and to dates for application dates"""
+
+ def _get_leave_allocation_record(date):
+ LeaveAllocation = frappe.qb.DocType("Leave Allocation")
+ allocation = (
+ frappe.qb.from_(LeaveAllocation)
+ .select(LeaveAllocation.name, LeaveAllocation.from_date, LeaveAllocation.to_date)
+ .where(
+ (LeaveAllocation.employee == self.employee)
+ & (LeaveAllocation.leave_type == self.leave_type)
+ & (LeaveAllocation.docstatus == 1)
+ & ((date >= LeaveAllocation.from_date) & (date <= LeaveAllocation.to_date))
+ )
+ ).run(as_dict=True)
+
+ return allocation and allocation[0]
allocation_based_on_from_date = _get_leave_allocation_record(self.from_date)
allocation_based_on_to_date = _get_leave_allocation_record(self.to_date)
- if not (allocation_based_on_from_date or allocation_based_on_to_date):
- frappe.throw(_("Application period cannot be outside leave allocation period"))
-
- elif allocation_based_on_from_date != allocation_based_on_to_date:
- frappe.throw(_("Application period cannot be across two allocation records"))
+ return allocation_based_on_from_date, allocation_based_on_to_date
def validate_back_dated_application(self):
- future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation`
+ future_allocation = frappe.db.sql(
+ """select name, from_date from `tabLeave Allocation`
where employee=%s and leave_type=%s and docstatus=1 and from_date > %s
- and carry_forward=1""", (self.employee, self.leave_type, self.to_date), as_dict=1)
+ and carry_forward=1""",
+ (self.employee, self.leave_type, self.to_date),
+ as_dict=1,
+ )
if future_allocation:
- frappe.throw(_("Leave cannot be applied/cancelled before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}")
- .format(formatdate(future_allocation[0].from_date), future_allocation[0].name))
+ frappe.throw(
+ _(
+ "Leave cannot be applied/cancelled before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}"
+ ).format(formatdate(future_allocation[0].from_date), future_allocation[0].name)
+ )
def update_attendance(self):
if self.status != "Approved":
@@ -169,8 +238,9 @@ class LeaveApplication(Document):
for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
date = dt.strftime("%Y-%m-%d")
- attendance_name = frappe.db.exists("Attendance", dict(employee = self.employee,
- attendance_date = date, docstatus = ('!=', 2)))
+ attendance_name = frappe.db.exists(
+ "Attendance", dict(employee=self.employee, attendance_date=date, docstatus=("!=", 2))
+ )
# don't mark attendance for holidays
# if leave type does not include holidays within leaves as leaves
@@ -187,17 +257,17 @@ class LeaveApplication(Document):
self.create_or_update_attendance(attendance_name, date)
def create_or_update_attendance(self, attendance_name, date):
- status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave"
+ status = (
+ "Half Day"
+ if self.half_day_date and getdate(date) == getdate(self.half_day_date)
+ else "On Leave"
+ )
if attendance_name:
# update existing attendance, change absent to on leave
- doc = frappe.get_doc('Attendance', attendance_name)
+ doc = frappe.get_doc("Attendance", attendance_name)
if doc.status != status:
- doc.db_set({
- 'status': status,
- 'leave_type': self.leave_type,
- 'leave_application': self.name
- })
+ doc.db_set({"status": status, "leave_type": self.leave_type, "leave_application": self.name})
else:
# make new attendance and submit it
doc = frappe.new_doc("Attendance")
@@ -214,8 +284,12 @@ class LeaveApplication(Document):
def cancel_attendance(self):
if self.docstatus == 2:
- attendance = frappe.db.sql("""select name from `tabAttendance` where employee = %s\
- and (attendance_date between %s and %s) and docstatus < 2 and status in ('On Leave', 'Half Day')""",(self.employee, self.from_date, self.to_date), as_dict=1)
+ attendance = frappe.db.sql(
+ """select name from `tabAttendance` where employee = %s\
+ and (attendance_date between %s and %s) and docstatus < 2 and status in ('On Leave', 'Half Day')""",
+ (self.employee, self.from_date, self.to_date),
+ as_dict=1,
+ )
for name in attendance:
frappe.db.set_value("Attendance", name, "docstatus", 2)
@@ -223,21 +297,29 @@ class LeaveApplication(Document):
if not frappe.db.get_value("Leave Type", self.leave_type, "is_lwp"):
return
- last_processed_pay_slip = frappe.db.sql("""
+ last_processed_pay_slip = frappe.db.sql(
+ """
select start_date, end_date from `tabSalary Slip`
where docstatus = 1 and employee = %s
and ((%s between start_date and end_date) or (%s between start_date and end_date))
order by modified desc limit 1
- """,(self.employee, self.to_date, self.from_date))
+ """,
+ (self.employee, self.to_date, self.from_date),
+ )
if last_processed_pay_slip:
- frappe.throw(_("Salary already processed for period between {0} and {1}, Leave application period cannot be between this date range.").format(formatdate(last_processed_pay_slip[0][0]),
- formatdate(last_processed_pay_slip[0][1])))
-
+ frappe.throw(
+ _(
+ "Salary already processed for period between {0} and {1}, Leave application period cannot be between this date range."
+ ).format(
+ formatdate(last_processed_pay_slip[0][0]), formatdate(last_processed_pay_slip[0][1])
+ )
+ )
def show_block_day_warning(self):
- block_dates = get_applicable_block_dates(self.from_date, self.to_date,
- self.employee, self.company, all_lists=True)
+ block_dates = get_applicable_block_dates(
+ self.from_date, self.to_date, self.employee, self.company, all_lists=True
+ )
if block_dates:
frappe.msgprint(_("Warning: Leave application contains following block dates") + ":")
@@ -245,53 +327,99 @@ class LeaveApplication(Document):
frappe.msgprint(formatdate(d.block_date) + ": " + d.reason)
def validate_block_days(self):
- block_dates = get_applicable_block_dates(self.from_date, self.to_date,
- self.employee, self.company)
+ block_dates = get_applicable_block_dates(
+ self.from_date, self.to_date, self.employee, self.company
+ )
if block_dates and self.status == "Approved":
frappe.throw(_("You are not authorized to approve leaves on Block Dates"), LeaveDayBlockedError)
def validate_balance_leaves(self):
if self.from_date and self.to_date:
- self.total_leave_days = get_number_of_leave_days(self.employee, self.leave_type,
- self.from_date, self.to_date, self.half_day, self.half_day_date)
+ self.total_leave_days = get_number_of_leave_days(
+ self.employee, self.leave_type, self.from_date, self.to_date, self.half_day, self.half_day_date
+ )
if self.total_leave_days <= 0:
- frappe.throw(_("The day(s) on which you are applying for leave are holidays. You need not apply for leave."))
+ frappe.throw(
+ _(
+ "The day(s) on which you are applying for leave are holidays. You need not apply for leave."
+ )
+ )
if not is_lwp(self.leave_type):
- self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date,
- consider_all_leaves_in_the_allocation_period=True)
- if self.status != "Rejected" and (self.leave_balance < self.total_leave_days or not self.leave_balance):
- if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
- frappe.msgprint(_("Note: There is not enough leave balance for Leave Type {0}")
- .format(self.leave_type))
- else:
- frappe.throw(_("There is not enough leave balance for Leave Type {0}")
- .format(self.leave_type))
+ leave_balance = get_leave_balance_on(
+ self.employee,
+ self.leave_type,
+ self.from_date,
+ self.to_date,
+ consider_all_leaves_in_the_allocation_period=True,
+ for_consumption=True,
+ )
+ self.leave_balance = leave_balance.get("leave_balance")
+ leave_balance_for_consumption = leave_balance.get("leave_balance_for_consumption")
+
+ if self.status != "Rejected" and (
+ leave_balance_for_consumption < self.total_leave_days or not leave_balance_for_consumption
+ ):
+ self.show_insufficient_balance_message(leave_balance_for_consumption)
+
+ def show_insufficient_balance_message(self, leave_balance_for_consumption: float) -> None:
+ alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates()
+
+ if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
+ if leave_balance_for_consumption != self.leave_balance:
+ msg = _("Warning: Insufficient leave balance for Leave Type {0} in this allocation.").format(
+ frappe.bold(self.leave_type)
+ )
+ msg += "
"
+ msg += _(
+ "Actual balances aren't available because the leave application spans over different leave allocations. You can still apply for leaves which would be compensated during the next allocation."
+ )
+ else:
+ msg = _("Warning: Insufficient leave balance for Leave Type {0}.").format(
+ frappe.bold(self.leave_type)
+ )
+
+ frappe.msgprint(msg, title=_("Warning"), indicator="orange")
+ else:
+ frappe.throw(
+ _("Insufficient leave balance for Leave Type {0}").format(frappe.bold(self.leave_type)),
+ exc=InsufficientLeaveBalanceError,
+ title=_("Insufficient Balance"),
+ )
def validate_leave_overlap(self):
if not self.name:
# hack! if name is null, it could cause problems with !=
self.name = "New Leave Application"
- for d in frappe.db.sql("""
+ for d in frappe.db.sql(
+ """
select
name, leave_type, posting_date, from_date, to_date, total_leave_days, half_day_date
from `tabLeave Application`
where employee = %(employee)s and docstatus < 2 and status in ("Open", "Approved")
and to_date >= %(from_date)s and from_date <= %(to_date)s
- and name != %(name)s""", {
+ and name != %(name)s""",
+ {
"employee": self.employee,
"from_date": self.from_date,
"to_date": self.to_date,
- "name": self.name
- }, as_dict = 1):
+ "name": self.name,
+ },
+ as_dict=1,
+ ):
- if cint(self.half_day)==1 and getdate(self.half_day_date) == getdate(d.half_day_date) and (
- flt(self.total_leave_days)==0.5
- or getdate(self.from_date) == getdate(d.to_date)
- or getdate(self.to_date) == getdate(d.from_date)):
+ if (
+ cint(self.half_day) == 1
+ and getdate(self.half_day_date) == getdate(d.half_day_date)
+ and (
+ flt(self.total_leave_days) == 0.5
+ or getdate(self.from_date) == getdate(d.to_date)
+ or getdate(self.to_date) == getdate(d.from_date)
+ )
+ ):
total_leaves_on_half_day = self.get_total_leaves_on_half_day()
if total_leaves_on_half_day >= 1:
@@ -301,22 +429,22 @@ class LeaveApplication(Document):
def throw_overlap_error(self, d):
form_link = get_link_to_form("Leave Application", d.name)
- msg = _("Employee {0} has already applied for {1} between {2} and {3} : {4}").format(self.employee,
- d['leave_type'], formatdate(d['from_date']), formatdate(d['to_date']), form_link)
+ msg = _("Employee {0} has already applied for {1} between {2} and {3} : {4}").format(
+ self.employee, d["leave_type"], formatdate(d["from_date"]), formatdate(d["to_date"]), form_link
+ )
frappe.throw(msg, OverlapError)
def get_total_leaves_on_half_day(self):
- leave_count_on_half_day_date = frappe.db.sql("""select count(name) from `tabLeave Application`
+ leave_count_on_half_day_date = frappe.db.sql(
+ """select count(name) from `tabLeave Application`
where employee = %(employee)s
and docstatus < 2
and status in ("Open", "Approved")
and half_day = 1
and half_day_date = %(half_day_date)s
- and name != %(name)s""", {
- "employee": self.employee,
- "half_day_date": self.half_day_date,
- "name": self.name
- })[0][0]
+ and name != %(name)s""",
+ {"employee": self.employee, "half_day_date": self.half_day_date, "name": self.name},
+ )[0][0]
return leave_count_on_half_day_date * 0.5
@@ -326,24 +454,36 @@ class LeaveApplication(Document):
frappe.throw(_("Leave of type {0} cannot be longer than {1}").format(self.leave_type, max_days))
def validate_attendance(self):
- attendance = frappe.db.sql("""select name from `tabAttendance` where employee = %s and (attendance_date between %s and %s)
+ attendance = frappe.db.sql(
+ """select name from `tabAttendance` where employee = %s and (attendance_date between %s and %s)
and status = "Present" and docstatus = 1""",
- (self.employee, self.from_date, self.to_date))
+ (self.employee, self.from_date, self.to_date),
+ )
if attendance:
- frappe.throw(_("Attendance for employee {0} is already marked for this day").format(self.employee),
- AttendanceAlreadyMarkedError)
+ frappe.throw(
+ _("Attendance for employee {0} is already marked for this day").format(self.employee),
+ AttendanceAlreadyMarkedError,
+ )
def validate_optional_leave(self):
leave_period = get_leave_period(self.from_date, self.to_date, self.company)
if not leave_period:
frappe.throw(_("Cannot find active Leave Period"))
- optional_holiday_list = frappe.db.get_value("Leave Period", leave_period[0]["name"], "optional_holiday_list")
+ optional_holiday_list = frappe.db.get_value(
+ "Leave Period", leave_period[0]["name"], "optional_holiday_list"
+ )
if not optional_holiday_list:
- frappe.throw(_("Optional Holiday List not set for leave period {0}").format(leave_period[0]["name"]))
+ frappe.throw(
+ _("Optional Holiday List not set for leave period {0}").format(leave_period[0]["name"])
+ )
day = getdate(self.from_date)
while day <= getdate(self.to_date):
- if not frappe.db.exists({"doctype": "Holiday", "parent": optional_holiday_list, "holiday_date": day}):
- frappe.throw(_("{0} is not in Optional Holiday List").format(formatdate(day)), NotAnOptionalHoliday)
+ if not frappe.db.exists(
+ {"doctype": "Holiday", "parent": optional_holiday_list, "holiday_date": day}
+ ):
+ frappe.throw(
+ _("{0} is not in Optional Holiday List").format(formatdate(day)), NotAnOptionalHoliday
+ )
day = add_days(day, 1)
def set_half_day_date(self):
@@ -358,44 +498,50 @@ class LeaveApplication(Document):
if not employee.user_id:
return
- parent_doc = frappe.get_doc('Leave Application', self.name)
+ parent_doc = frappe.get_doc("Leave Application", self.name)
args = parent_doc.as_dict()
- template = frappe.db.get_single_value('HR Settings', 'leave_status_notification_template')
+ template = frappe.db.get_single_value("HR Settings", "leave_status_notification_template")
if not template:
frappe.msgprint(_("Please set default template for Leave Status Notification in HR Settings."))
return
email_template = frappe.get_doc("Email Template", template)
message = frappe.render_template(email_template.response, args)
- self.notify({
- # for post in messages
- "message": message,
- "message_to": employee.user_id,
- # for email
- "subject": email_template.subject,
- "notify": "employee"
- })
+ self.notify(
+ {
+ # for post in messages
+ "message": message,
+ "message_to": employee.user_id,
+ # for email
+ "subject": email_template.subject,
+ "notify": "employee",
+ }
+ )
def notify_leave_approver(self):
if self.leave_approver:
- parent_doc = frappe.get_doc('Leave Application', self.name)
+ parent_doc = frappe.get_doc("Leave Application", self.name)
args = parent_doc.as_dict()
- template = frappe.db.get_single_value('HR Settings', 'leave_approval_notification_template')
+ template = frappe.db.get_single_value("HR Settings", "leave_approval_notification_template")
if not template:
- frappe.msgprint(_("Please set default template for Leave Approval Notification in HR Settings."))
+ frappe.msgprint(
+ _("Please set default template for Leave Approval Notification in HR Settings.")
+ )
return
email_template = frappe.get_doc("Email Template", template)
message = frappe.render_template(email_template.response, args)
- self.notify({
- # for post in messages
- "message": message,
- "message_to": self.leave_approver,
- # for email
- "subject": email_template.subject
- })
+ self.notify(
+ {
+ # for post in messages
+ "message": message,
+ "message_to": self.leave_approver,
+ # for email
+ "subject": email_template.subject,
+ }
+ )
def notify(self, args):
args = frappe._dict(args)
@@ -404,94 +550,195 @@ class LeaveApplication(Document):
contact = args.message_to
if not isinstance(contact, list):
if not args.notify == "employee":
- contact = frappe.get_doc('User', contact).email or contact
+ contact = frappe.get_doc("User", contact).email or contact
- sender = dict()
- sender['email'] = frappe.get_doc('User', frappe.session.user).email
- sender['full_name'] = get_fullname(sender['email'])
+ sender = dict()
+ sender["email"] = frappe.get_doc("User", frappe.session.user).email
+ sender["full_name"] = get_fullname(sender["email"])
try:
frappe.sendmail(
- recipients = contact,
- sender = sender['email'],
- subject = args.subject,
- message = args.message,
+ recipients=contact,
+ sender=sender["email"],
+ subject=args.subject,
+ message=args.message,
)
frappe.msgprint(_("Email sent to {0}").format(contact))
except frappe.OutgoingEmailError:
pass
def create_leave_ledger_entry(self, submit=True):
- if self.status != 'Approved' and submit:
+ if self.status != "Approved" and submit:
return
- expiry_date = get_allocation_expiry(self.employee, self.leave_type,
- self.to_date, self.from_date)
-
+ expiry_date = get_allocation_expiry_for_cf_leaves(
+ self.employee, self.leave_type, self.to_date, self.from_date
+ )
lwp = frappe.db.get_value("Leave Type", self.leave_type, "is_lwp")
if expiry_date:
self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp)
else:
- raise_exception = True
- if frappe.flags.in_patch:
- raise_exception=False
+ alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates()
+ if self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date):
+ # required only if negative balance is allowed for leave type
+ # else will be stopped in validation itself
+ self.create_separate_ledger_entries(alloc_on_from_date, alloc_on_to_date, submit, lwp)
+ else:
+ raise_exception = False if frappe.flags.in_patch else True
+ args = dict(
+ leaves=self.total_leave_days * -1,
+ from_date=self.from_date,
+ to_date=self.to_date,
+ is_lwp=lwp,
+ holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception)
+ or "",
+ )
+ create_leave_ledger_entry(self, args, submit)
- args = dict(
- leaves=self.total_leave_days * -1,
- from_date=self.from_date,
- to_date=self.to_date,
- is_lwp=lwp,
- holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
+ def is_separate_ledger_entry_required(
+ self, alloc_on_from_date: Optional[Dict] = None, alloc_on_to_date: Optional[Dict] = None
+ ) -> bool:
+ """Checks if application dates fall in separate allocations"""
+ if (
+ (alloc_on_from_date and not alloc_on_to_date)
+ or (not alloc_on_from_date and alloc_on_to_date)
+ or (
+ alloc_on_from_date and alloc_on_to_date and alloc_on_from_date.name != alloc_on_to_date.name
+ )
+ ):
+ return True
+ return False
+
+ def create_separate_ledger_entries(self, alloc_on_from_date, alloc_on_to_date, submit, lwp):
+ """Creates separate ledger entries for application period falling into separate allocations"""
+ # for creating separate ledger entries existing allocation periods should be consecutive
+ if (
+ submit
+ and alloc_on_from_date
+ and alloc_on_to_date
+ and add_days(alloc_on_from_date.to_date, 1) != alloc_on_to_date.from_date
+ ):
+ frappe.throw(
+ _(
+ "Leave Application period cannot be across two non-consecutive leave allocations {0} and {1}."
+ ).format(
+ get_link_to_form("Leave Allocation", alloc_on_from_date.name),
+ get_link_to_form("Leave Allocation", alloc_on_to_date),
+ )
+ )
+
+ raise_exception = False if frappe.flags.in_patch else True
+
+ if alloc_on_from_date:
+ first_alloc_end = alloc_on_from_date.to_date
+ second_alloc_start = add_days(alloc_on_from_date.to_date, 1)
+ else:
+ first_alloc_end = add_days(alloc_on_to_date.from_date, -1)
+ second_alloc_start = alloc_on_to_date.from_date
+
+ leaves_in_first_alloc = get_number_of_leave_days(
+ self.employee,
+ self.leave_type,
+ self.from_date,
+ first_alloc_end,
+ self.half_day,
+ self.half_day_date,
+ )
+ leaves_in_second_alloc = get_number_of_leave_days(
+ self.employee,
+ self.leave_type,
+ second_alloc_start,
+ self.to_date,
+ self.half_day,
+ self.half_day_date,
+ )
+
+ args = dict(
+ is_lwp=lwp,
+ holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception)
+ or "",
+ )
+
+ if leaves_in_first_alloc:
+ args.update(
+ dict(from_date=self.from_date, to_date=first_alloc_end, leaves=leaves_in_first_alloc * -1)
+ )
+ create_leave_ledger_entry(self, args, submit)
+
+ if leaves_in_second_alloc:
+ args.update(
+ dict(from_date=second_alloc_start, to_date=self.to_date, leaves=leaves_in_second_alloc * -1)
)
create_leave_ledger_entry(self, args, submit)
def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp):
- ''' splits leave application into two ledger entries to consider expiry of allocation '''
+ """Splits leave application into two ledger entries to consider expiry of allocation"""
+ raise_exception = False if frappe.flags.in_patch else True
- raise_exception = True
- if frappe.flags.in_patch:
- raise_exception=False
-
- args = dict(
- from_date=self.from_date,
- to_date=expiry_date,
- leaves=(date_diff(expiry_date, self.from_date) + 1) * -1,
- is_lwp=lwp,
- holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or ''
+ leaves = get_number_of_leave_days(
+ self.employee, self.leave_type, self.from_date, expiry_date, self.half_day, self.half_day_date
)
- create_leave_ledger_entry(self, args, submit)
+
+ if leaves:
+ args = dict(
+ from_date=self.from_date,
+ to_date=expiry_date,
+ leaves=leaves * -1,
+ is_lwp=lwp,
+ holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception)
+ or "",
+ )
+ create_leave_ledger_entry(self, args, submit)
if getdate(expiry_date) != getdate(self.to_date):
start_date = add_days(expiry_date, 1)
- args.update(dict(
- from_date=start_date,
- to_date=self.to_date,
- leaves=date_diff(self.to_date, expiry_date) * -1
- ))
- create_leave_ledger_entry(self, args, submit)
+ leaves = get_number_of_leave_days(
+ self.employee, self.leave_type, start_date, self.to_date, self.half_day, self.half_day_date
+ )
+
+ if leaves:
+ args.update(dict(from_date=start_date, to_date=self.to_date, leaves=leaves * -1))
+ create_leave_ledger_entry(self, args, submit)
-def get_allocation_expiry(employee, leave_type, to_date, from_date):
- ''' Returns expiry of carry forward allocation in leave ledger entry '''
- expiry = frappe.get_all("Leave Ledger Entry",
+def get_allocation_expiry_for_cf_leaves(
+ employee: str, leave_type: str, to_date: str, from_date: str
+) -> str:
+ """Returns expiry of carry forward allocation in leave ledger entry"""
+ expiry = frappe.get_all(
+ "Leave Ledger Entry",
filters={
- 'employee': employee,
- 'leave_type': leave_type,
- 'is_carry_forward': 1,
- 'transaction_type': 'Leave Allocation',
- 'to_date': ['between', (from_date, to_date)]
- },fields=['to_date'])
- return expiry[0]['to_date'] if expiry else None
+ "employee": employee,
+ "leave_type": leave_type,
+ "is_carry_forward": 1,
+ "transaction_type": "Leave Allocation",
+ "to_date": ["between", (from_date, to_date)],
+ "docstatus": 1,
+ },
+ fields=["to_date"],
+ )
+ return expiry[0]["to_date"] if expiry else ""
+
@frappe.whitelist()
-def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None, holiday_list = None):
+def get_number_of_leave_days(
+ employee: str,
+ leave_type: str,
+ from_date: str,
+ to_date: str,
+ half_day: Optional[int] = None,
+ half_day_date: Optional[str] = None,
+ holiday_list: Optional[str] = None,
+) -> float:
+ """Returns number of leave days between 2 dates after considering half day and holidays
+ (Based on the include_holiday setting in Leave Type)"""
number_of_days = 0
if cint(half_day) == 1:
- if from_date == to_date:
+ if getdate(from_date) == getdate(to_date):
number_of_days = 0.5
- elif half_day_date and half_day_date <= to_date:
- number_of_days = date_diff(to_date, from_date) + .5
+ elif half_day_date and getdate(from_date) <= getdate(half_day_date) <= getdate(to_date):
+ number_of_days = date_diff(to_date, from_date) + 0.5
else:
number_of_days = date_diff(to_date, from_date) + 1
@@ -499,9 +746,12 @@ def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day
number_of_days = date_diff(to_date, from_date) + 1
if not frappe.db.get_value("Leave Type", leave_type, "include_holiday"):
- number_of_days = flt(number_of_days) - flt(get_holidays(employee, from_date, to_date, holiday_list=holiday_list))
+ number_of_days = flt(number_of_days) - flt(
+ get_holidays(employee, from_date, to_date, holiday_list=holiday_list)
+ )
return number_of_days
+
@frappe.whitelist()
def get_leave_details(employee, date):
allocation_records = get_leave_allocation_records(employee, date)
@@ -509,49 +759,71 @@ def get_leave_details(employee, date):
for d in allocation_records:
allocation = allocation_records.get(d, frappe._dict())
- total_allocated_leaves = frappe.db.get_value('Leave Allocation', {
- 'from_date': ('<=', date),
- 'to_date': ('>=', date),
- 'employee': employee,
- 'leave_type': allocation.leave_type,
- }, 'SUM(total_leaves_allocated)') or 0
+ total_allocated_leaves = (
+ frappe.db.get_value(
+ "Leave Allocation",
+ {
+ "from_date": ("<=", date),
+ "to_date": (">=", date),
+ "employee": employee,
+ "leave_type": allocation.leave_type,
+ "docstatus": 1,
+ },
+ "SUM(total_leaves_allocated)",
+ )
+ or 0
+ )
- remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date,
- consider_all_leaves_in_the_allocation_period=True)
+ remaining_leaves = get_leave_balance_on(
+ employee, d, date, to_date=allocation.to_date, consider_all_leaves_in_the_allocation_period=True
+ )
end_date = allocation.to_date
leaves_taken = get_leaves_for_period(employee, d, allocation.from_date, end_date) * -1
- leaves_pending = get_pending_leaves_for_period(employee, d, allocation.from_date, end_date)
+ leaves_pending = get_leaves_pending_approval_for_period(
+ employee, d, allocation.from_date, end_date
+ )
leave_allocation[d] = {
"total_leaves": total_allocated_leaves,
"expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken),
"leaves_taken": leaves_taken,
- "pending_leaves": leaves_pending,
- "remaining_leaves": remaining_leaves}
+ "leaves_pending_approval": leaves_pending,
+ "remaining_leaves": remaining_leaves,
+ }
- #is used in set query
- lwps = frappe.get_list("Leave Type", filters = {"is_lwp": 1})
- lwps = [lwp.name for lwp in lwps]
+ # is used in set query
+ lwp = frappe.get_list("Leave Type", filters={"is_lwp": 1}, pluck="name")
- ret = {
- 'leave_allocation': leave_allocation,
- 'leave_approver': get_leave_approver(employee),
- 'lwps': lwps
+ return {
+ "leave_allocation": leave_allocation,
+ "leave_approver": get_leave_approver(employee),
+ "lwps": lwp,
}
- return ret
@frappe.whitelist()
-def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_leaves_in_the_allocation_period=False):
- '''
- Returns leave balance till date
- :param employee: employee name
- :param leave_type: leave type
- :param date: date to check balance on
- :param to_date: future date to check for allocation expiry
- :param consider_all_leaves_in_the_allocation_period: consider all leaves taken till the allocation end date
- '''
+def get_leave_balance_on(
+ employee: str,
+ leave_type: str,
+ date: str,
+ to_date: str = None,
+ consider_all_leaves_in_the_allocation_period: bool = False,
+ for_consumption: bool = False,
+):
+ """
+ Returns leave balance till date
+ :param employee: employee name
+ :param leave_type: leave type
+ :param date: date to check balance on
+ :param to_date: future date to check for allocation expiry
+ :param consider_all_leaves_in_the_allocation_period: consider all leaves taken till the allocation end date
+ :param for_consumption: flag to check if leave balance is required for consumption or display
+ eg: employee has leave balance = 10 but allocation is expiring in 1 day so employee can only consume 1 leave
+ in this case leave_balance = 10 but leave_balance_for_consumption = 1
+ if True, returns a dict eg: {'leave_balance': 10, 'leave_balance_for_consumption': 1}
+ else, returns leave_balance (in this case 10)
+ """
if not to_date:
to_date = nowdate()
@@ -560,97 +832,147 @@ def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_
allocation = allocation_records.get(leave_type, frappe._dict())
end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date
- expiry = get_allocation_expiry(employee, leave_type, to_date, date)
+ cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date)
leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
- return get_remaining_leaves(allocation, leaves_taken, date, expiry)
+ remaining_leaves = get_remaining_leaves(allocation, leaves_taken, date, cf_expiry)
+
+ if for_consumption:
+ return remaining_leaves
+ else:
+ return remaining_leaves.get("leave_balance")
+
def get_leave_allocation_records(employee, date, leave_type=None):
- ''' returns the total allocated leaves and carry forwarded leaves based on ledger entries '''
+ """Returns the total allocated leaves and carry forwarded leaves based on ledger entries"""
+ Ledger = frappe.qb.DocType("Leave Ledger Entry")
- conditions = ("and leave_type='%s'" % leave_type) if leave_type else ""
- allocation_details = frappe.db.sql("""
- SELECT
- SUM(CASE WHEN is_carry_forward = 1 THEN leaves ELSE 0 END) as cf_leaves,
- SUM(CASE WHEN is_carry_forward = 0 THEN leaves ELSE 0 END) as new_leaves,
- MIN(from_date) as from_date,
- MAX(to_date) as to_date,
- leave_type
- FROM `tabLeave Ledger Entry`
- WHERE
- from_date <= %(date)s
- AND to_date >= %(date)s
- AND docstatus=1
- AND transaction_type="Leave Allocation"
- AND employee=%(employee)s
- AND is_expired=0
- AND is_lwp=0
- {0}
- GROUP BY employee, leave_type
- """.format(conditions), dict(date=date, employee=employee), as_dict=1) #nosec
+ cf_leave_case = (
+ frappe.qb.terms.Case().when(Ledger.is_carry_forward == "1", Ledger.leaves).else_(0)
+ )
+ sum_cf_leaves = Sum(cf_leave_case).as_("cf_leaves")
+
+ new_leaves_case = (
+ frappe.qb.terms.Case().when(Ledger.is_carry_forward == "0", Ledger.leaves).else_(0)
+ )
+ sum_new_leaves = Sum(new_leaves_case).as_("new_leaves")
+
+ query = (
+ frappe.qb.from_(Ledger)
+ .select(
+ sum_cf_leaves,
+ sum_new_leaves,
+ Min(Ledger.from_date).as_("from_date"),
+ Max(Ledger.to_date).as_("to_date"),
+ Ledger.leave_type,
+ )
+ .where(
+ (Ledger.from_date <= date)
+ & (Ledger.to_date >= date)
+ & (Ledger.docstatus == 1)
+ & (Ledger.transaction_type == "Leave Allocation")
+ & (Ledger.employee == employee)
+ & (Ledger.is_expired == 0)
+ & (Ledger.is_lwp == 0)
+ )
+ )
+
+ if leave_type:
+ query = query.where((Ledger.leave_type == leave_type))
+ query = query.groupby(Ledger.employee, Ledger.leave_type)
+
+ allocation_details = query.run(as_dict=True)
allocated_leaves = frappe._dict()
for d in allocation_details:
- allocated_leaves.setdefault(d.leave_type, frappe._dict({
- "from_date": d.from_date,
- "to_date": d.to_date,
- "total_leaves_allocated": flt(d.cf_leaves) + flt(d.new_leaves),
- "unused_leaves": d.cf_leaves,
- "new_leaves_allocated": d.new_leaves,
- "leave_type": d.leave_type
- }))
+ allocated_leaves.setdefault(
+ d.leave_type,
+ frappe._dict(
+ {
+ "from_date": d.from_date,
+ "to_date": d.to_date,
+ "total_leaves_allocated": flt(d.cf_leaves) + flt(d.new_leaves),
+ "unused_leaves": d.cf_leaves,
+ "new_leaves_allocated": d.new_leaves,
+ "leave_type": d.leave_type,
+ }
+ ),
+ )
return allocated_leaves
-def get_pending_leaves_for_period(employee, leave_type, from_date, to_date):
- ''' Returns leaves that are pending approval '''
- leaves = frappe.get_all("Leave Application",
- filters={
- "employee": employee,
- "leave_type": leave_type,
- "status": "Open"
- },
+
+def get_leaves_pending_approval_for_period(
+ employee: str, leave_type: str, from_date: str, to_date: str
+) -> float:
+ """Returns leaves that are pending for approval"""
+ leaves = frappe.get_all(
+ "Leave Application",
+ filters={"employee": employee, "leave_type": leave_type, "status": "Open"},
or_filters={
"from_date": ["between", (from_date, to_date)],
- "to_date": ["between", (from_date, to_date)]
- }, fields=['SUM(total_leave_days) as leaves'])[0]
- return leaves['leaves'] if leaves['leaves'] else 0.0
+ "to_date": ["between", (from_date, to_date)],
+ },
+ fields=["SUM(total_leave_days) as leaves"],
+ )[0]
+ return leaves["leaves"] if leaves["leaves"] else 0.0
+
+
+def get_remaining_leaves(
+ allocation: Dict, leaves_taken: float, date: str, cf_expiry: str
+) -> Dict[str, float]:
+ """Returns a dict of leave_balance and leave_balance_for_consumption
+ leave_balance returns the available leave balance
+ leave_balance_for_consumption returns the minimum leaves remaining after comparing with remaining days for allocation expiry
+ """
-def get_remaining_leaves(allocation, leaves_taken, date, expiry):
- ''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry '''
def _get_remaining_leaves(remaining_leaves, end_date):
-
+ """Returns minimum leaves remaining after comparing with remaining days for allocation expiry"""
if remaining_leaves > 0:
remaining_days = date_diff(end_date, date) + 1
remaining_leaves = min(remaining_days, remaining_leaves)
return remaining_leaves
- total_leaves = flt(allocation.total_leaves_allocated) + flt(leaves_taken)
+ leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt(
+ leaves_taken
+ )
- if expiry and allocation.unused_leaves:
- remaining_leaves = flt(allocation.unused_leaves) + flt(leaves_taken)
- remaining_leaves = _get_remaining_leaves(remaining_leaves, expiry)
+ # balance for carry forwarded leaves
+ if cf_expiry and allocation.unused_leaves:
+ cf_leaves = flt(allocation.unused_leaves) + flt(leaves_taken)
+ remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry)
- total_leaves = flt(allocation.new_leaves_allocated) + flt(remaining_leaves)
+ leave_balance = flt(allocation.new_leaves_allocated) + flt(cf_leaves)
+ leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves)
- return _get_remaining_leaves(total_leaves, allocation.to_date)
+ remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date)
+ return frappe._dict(leave_balance=leave_balance, leave_balance_for_consumption=remaining_leaves)
-def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_expired_leaves=False):
+
+def get_leaves_for_period(
+ employee: str, leave_type: str, from_date: str, to_date: str, skip_expired_leaves: bool = True
+) -> float:
leave_entries = get_leave_entries(employee, leave_type, from_date, to_date)
leave_days = 0
for leave_entry in leave_entries:
- inclusive_period = leave_entry.from_date >= getdate(from_date) and leave_entry.to_date <= getdate(to_date)
+ inclusive_period = leave_entry.from_date >= getdate(
+ from_date
+ ) and leave_entry.to_date <= getdate(to_date)
- if inclusive_period and leave_entry.transaction_type == 'Leave Encashment':
+ if inclusive_period and leave_entry.transaction_type == "Leave Encashment":
leave_days += leave_entry.leaves
- elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' and leave_entry.is_expired \
- and (do_not_skip_expired_leaves or not skip_expiry_leaves(leave_entry, to_date)):
+ elif (
+ inclusive_period
+ and leave_entry.transaction_type == "Leave Allocation"
+ and leave_entry.is_expired
+ and not skip_expired_leaves
+ ):
leave_days += leave_entry.leaves
- elif leave_entry.transaction_type == 'Leave Application':
+ elif leave_entry.transaction_type == "Leave Application":
if leave_entry.from_date < getdate(from_date):
leave_entry.from_date = from_date
if leave_entry.to_date > getdate(to_date):
@@ -661,23 +983,30 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_
# fetch half day date for leaves with half days
if leave_entry.leaves % 1:
half_day = 1
- half_day_date = frappe.db.get_value('Leave Application',
- {'name': leave_entry.transaction_name}, ['half_day_date'])
+ half_day_date = frappe.db.get_value(
+ "Leave Application", {"name": leave_entry.transaction_name}, ["half_day_date"]
+ )
- leave_days += get_number_of_leave_days(employee, leave_type,
- leave_entry.from_date, leave_entry.to_date, half_day, half_day_date, holiday_list=leave_entry.holiday_list) * -1
+ leave_days += (
+ get_number_of_leave_days(
+ employee,
+ leave_type,
+ leave_entry.from_date,
+ leave_entry.to_date,
+ half_day,
+ half_day_date,
+ holiday_list=leave_entry.holiday_list,
+ )
+ * -1
+ )
return leave_days
-def skip_expiry_leaves(leave_entry, date):
- ''' Checks whether the expired leaves coincide with the to_date of leave balance check.
- This allows backdated leave entry creation for non carry forwarded allocation '''
- end_date = frappe.db.get_value("Leave Allocation", {'name': leave_entry.transaction_name}, ['to_date'])
- return True if end_date == date and not leave_entry.is_carry_forward else False
def get_leave_entries(employee, leave_type, from_date, to_date):
- ''' Returns leave entries between from_date and to_date. '''
- return frappe.db.sql("""
+ """Returns leave entries between from_date and to_date."""
+ return frappe.db.sql(
+ """
SELECT
employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type, holiday_list,
is_carry_forward, is_expired
@@ -689,44 +1018,47 @@ def get_leave_entries(employee, leave_type, from_date, to_date):
AND (from_date between %(from_date)s AND %(to_date)s
OR to_date between %(from_date)s AND %(to_date)s
OR (from_date < %(from_date)s AND to_date > %(to_date)s))
- """, {
- "from_date": from_date,
- "to_date": to_date,
- "employee": employee,
- "leave_type": leave_type
- }, as_dict=1)
+ """,
+ {"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type},
+ as_dict=1,
+ )
+
@frappe.whitelist()
-def get_holidays(employee, from_date, to_date, holiday_list = None):
- '''get holidays between two dates for the given employee'''
+def get_holidays(employee, from_date, to_date, holiday_list=None):
+ """get holidays between two dates for the given employee"""
if not holiday_list:
holiday_list = get_holiday_list_for_employee(employee)
- holidays = frappe.db.sql("""select count(distinct holiday_date) from `tabHoliday` h1, `tabHoliday List` h2
+ holidays = frappe.db.sql(
+ """select count(distinct holiday_date) from `tabHoliday` h1, `tabHoliday List` h2
where h1.parent = h2.name and h1.holiday_date between %s and %s
- and h2.name = %s""", (from_date, to_date, holiday_list))[0][0]
+ and h2.name = %s""",
+ (from_date, to_date, holiday_list),
+ )[0][0]
return holidays
+
def is_lwp(leave_type):
lwp = frappe.db.sql("select is_lwp from `tabLeave Type` where name = %s", leave_type)
return lwp and cint(lwp[0][0]) or 0
+
@frappe.whitelist()
def get_events(start, end, filters=None):
from frappe.desk.reportview import get_filters_cond
+
events = []
- employee = frappe.db.get_value("Employee",
- filters={"user_id": frappe.session.user},
- fieldname=["name", "company"],
- as_dict=True
+ employee = frappe.db.get_value(
+ "Employee", filters={"user_id": frappe.session.user}, fieldname=["name", "company"], as_dict=True
)
if employee:
employee, company = employee.name, employee.company
else:
- employee = ''
+ employee = ""
company = frappe.db.get_value("Global Defaults", None, "default_company")
conditions = get_filters_cond("Leave Application", filters, [])
@@ -740,6 +1072,7 @@ def get_events(start, end, filters=None):
return events
+
def add_department_leaves(events, start, end, employee, company):
department = frappe.db.get_value("Employee", employee, "department")
@@ -747,18 +1080,24 @@ def add_department_leaves(events, start, end, employee, company):
return
# department leaves
- department_employees = frappe.db.sql_list("""select name from tabEmployee where department=%s
- and company=%s""", (department, company))
+ department_employees = frappe.db.sql_list(
+ """select name from tabEmployee where department=%s
+ and company=%s""",
+ (department, company),
+ )
- filter_conditions = " and employee in (\"%s\")" % '", "'.join(department_employees)
+ filter_conditions = ' and employee in ("%s")' % '", "'.join(department_employees)
add_leaves(events, start, end, filter_conditions=filter_conditions)
def add_leaves(events, start, end, filter_conditions=None):
from frappe.desk.reportview import build_match_conditions
+
conditions = []
- if not cint(frappe.db.get_value("HR Settings", None, "show_leaves_of_all_department_members_in_calendar")):
+ if not cint(
+ frappe.db.get_value("HR Settings", None, "show_leaves_of_all_department_members_in_calendar")
+ ):
match_conditions = build_match_conditions("Leave Application")
if match_conditions:
@@ -783,12 +1122,12 @@ def add_leaves(events, start, end, filter_conditions=None):
"""
if conditions:
- query += ' AND ' + ' AND '.join(conditions)
+ query += " AND " + " AND ".join(conditions)
if filter_conditions:
query += filter_conditions
- for d in frappe.db.sql(query, {"start":start, "end": end}, as_dict=True):
+ for d in frappe.db.sql(query, {"start": start, "end": end}, as_dict=True):
e = {
"name": d.name,
"doctype": "Leave Application",
@@ -797,7 +1136,9 @@ def add_leaves(events, start, end, filter_conditions=None):
"docstatus": d.docstatus,
"color": d.color,
"all_day": int(not d.half_day),
- "title": cstr(d.employee_name) + f' ({cstr(d.leave_type)})' + (' ' + _('(Half Day)') if d.half_day else ''),
+ "title": cstr(d.employee_name)
+ + f" ({cstr(d.leave_type)})"
+ + (" " + _("(Half Day)") if d.half_day else ""),
}
if e not in events:
events.append(e)
@@ -811,43 +1152,55 @@ def add_block_dates(events, start, end, employee, company):
block_dates = get_applicable_block_dates(start, end, employee, company, all_lists=True)
for block_date in block_dates:
- events.append({
- "doctype": "Leave Block List Date",
- "from_date": block_date.block_date,
- "to_date": block_date.block_date,
- "title": _("Leave Blocked") + ": " + block_date.reason,
- "name": "_" + str(cnt),
- })
- cnt+=1
+ events.append(
+ {
+ "doctype": "Leave Block List Date",
+ "from_date": block_date.block_date,
+ "to_date": block_date.block_date,
+ "title": _("Leave Blocked") + ": " + block_date.reason,
+ "name": "_" + str(cnt),
+ }
+ )
+ cnt += 1
+
def add_holidays(events, start, end, employee, company):
applicable_holiday_list = get_holiday_list_for_employee(employee, company)
if not applicable_holiday_list:
return
- for holiday in frappe.db.sql("""select name, holiday_date, description
+ for holiday in frappe.db.sql(
+ """select name, holiday_date, description
from `tabHoliday` where parent=%s and holiday_date between %s and %s""",
- (applicable_holiday_list, start, end), as_dict=True):
- events.append({
+ (applicable_holiday_list, start, end),
+ as_dict=True,
+ ):
+ events.append(
+ {
"doctype": "Holiday",
"from_date": holiday.holiday_date,
- "to_date": holiday.holiday_date,
+ "to_date": holiday.holiday_date,
"title": _("Holiday") + ": " + cstr(holiday.description),
- "name": holiday.name
- })
+ "name": holiday.name,
+ }
+ )
+
@frappe.whitelist()
def get_mandatory_approval(doctype):
mandatory = ""
if doctype == "Leave Application":
- mandatory = frappe.db.get_single_value('HR Settings',
- 'leave_approver_mandatory_in_leave_application')
+ mandatory = frappe.db.get_single_value(
+ "HR Settings", "leave_approver_mandatory_in_leave_application"
+ )
else:
- mandatory = frappe.db.get_single_value('HR Settings',
- 'expense_approver_mandatory_in_expense_claim')
+ mandatory = frappe.db.get_single_value(
+ "HR Settings", "expense_approver_mandatory_in_expense_claim"
+ )
return mandatory
+
def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
query = """
select employee, leave_type, from_date, to_date, total_leave_days
@@ -861,12 +1214,11 @@ def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
if leave_type:
query += "and leave_type=%(leave_type)s"
- leave_applications = frappe.db.sql(query,{
- "from_date": from_date,
- "to_date": to_date,
- "employee": employee,
- "leave_type": leave_type
- }, as_dict=1)
+ leave_applications = frappe.db.sql(
+ query,
+ {"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type},
+ as_dict=1,
+ )
leave_days = 0
for leave_app in leave_applications:
@@ -878,18 +1230,24 @@ def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
if leave_app.to_date > getdate(to_date):
leave_app.to_date = to_date
- leave_days += get_number_of_leave_days(employee, leave_type,
- leave_app.from_date, leave_app.to_date)
+ leave_days += get_number_of_leave_days(
+ employee, leave_type, leave_app.from_date, leave_app.to_date
+ )
return leave_days
+
@frappe.whitelist()
def get_leave_approver(employee):
- leave_approver, department = frappe.db.get_value("Employee",
- employee, ["leave_approver", "department"])
+ leave_approver, department = frappe.db.get_value(
+ "Employee", employee, ["leave_approver", "department"]
+ )
if not leave_approver and department:
- leave_approver = frappe.db.get_value('Department Approver', {'parent': department,
- 'parentfield': 'leave_approvers', 'idx': 1}, 'approver')
+ leave_approver = frappe.db.get_value(
+ "Department Approver",
+ {"parent": department, "parentfield": "leave_approvers", "idx": 1},
+ "approver",
+ )
return leave_approver
diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
index 9f667a68356..e755322efda 100644
--- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
+++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
@@ -4,11 +4,11 @@
{{ __("Leave Type") }}
-
{{ __("Total Allocated Leave") }}
-
{{ __("Expired Leave") }}
-
{{ __("Used Leave") }}
-
{{ __("Pending Leave") }}
-
{{ __("Available Leave") }}
+
{{ __("Total Allocated Leave(s)") }}
+
{{ __("Expired Leave(s)") }}
+
{{ __("Used Leave(s)") }}
+
{{ __("Leave(s) Pending Approval") }}
+
{{ __("Available Leave(s)") }}
@@ -18,7 +18,7 @@
{%= value["total_leaves"] %}
{%= value["expired_leaves"] %}
{%= value["leaves_taken"] %}
-
{%= value["pending_leaves"] %}
+
{%= value["leaves_pending_approval"] %}
{%= value["remaining_leaves"] %}
{% } %}
diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.py b/erpnext/hr/doctype/leave_application/leave_application_dashboard.py
index d56133b5660..ee5cbe99f31 100644
--- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.py
+++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.py
@@ -1,19 +1,9 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'leave_application',
- 'transactions': [
- {
- 'items': ['Attendance']
- }
- ],
- 'reports': [
- {
- 'label': _('Reports'),
- 'items': ['Employee Leave Balance']
- }
- ]
- }
+ "fieldname": "leave_application",
+ "transactions": [{"items": ["Attendance"]}],
+ "reports": [{"label": _("Reports"), "items": ["Employee Leave Balance"]}],
+ }
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 39356bdcf18..8924a57708e 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -17,12 +17,17 @@ from frappe.utils import (
)
from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
from erpnext.hr.doctype.leave_application.leave_application import (
+ InsufficientLeaveBalanceError,
+ LeaveAcrossAllocationsError,
LeaveDayBlockedError,
NotAnOptionalHoliday,
OverlapError,
+ get_leave_allocation_records,
get_leave_balance_on,
+ get_leave_details,
)
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
create_assignment_for_multiple_employees,
@@ -33,7 +38,7 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
make_leave_application,
)
-test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"]
+test_dependencies = ["Leave Type", "Leave Allocation", "Leave Block List", "Employee"]
_test_records = [
{
@@ -44,7 +49,7 @@ _test_records = [
"description": "_Test Reason",
"leave_type": "_Test Leave Type",
"posting_date": "2013-01-02",
- "to_date": "2013-05-05"
+ "to_date": "2013-05-05",
},
{
"company": "_Test Company",
@@ -54,7 +59,7 @@ _test_records = [
"description": "_Test Reason",
"leave_type": "_Test Leave Type",
"posting_date": "2013-01-02",
- "to_date": "2013-05-05"
+ "to_date": "2013-05-05",
},
{
"company": "_Test Company",
@@ -64,27 +69,40 @@ _test_records = [
"description": "_Test Reason",
"leave_type": "_Test Leave Type LWP",
"posting_date": "2013-01-02",
- "to_date": "2013-01-15"
- }
+ "to_date": "2013-01-15",
+ },
]
class TestLeaveApplication(unittest.TestCase):
def setUp(self):
for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
- frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec
+ frappe.db.delete(dt)
frappe.set_user("Administrator")
set_leave_approver()
- frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'")
+ frappe.db.delete("Attendance", {"employee": "_T-Employee-00001"})
+ frappe.db.set_value("Employee", "_T-Employee-00001", "holiday_list", "")
+
+ from_date = get_year_start(getdate())
+ to_date = get_year_ending(getdate())
+ self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
+
+ if not frappe.db.exists("Leave Type", "_Test Leave Type"):
+ frappe.get_doc(
+ dict(leave_type_name="_Test Leave Type", doctype="Leave Type", include_holiday=True)
+ ).insert()
def tearDown(self):
frappe.db.rollback()
+ frappe.set_user("Administrator")
def _clear_roles(self):
- frappe.db.sql("""delete from `tabHas Role` where parent in
- ("test@example.com", "test1@example.com", "test2@example.com")""")
+ frappe.db.sql(
+ """delete from `tabHas Role` where parent in
+ ("test@example.com", "test1@example.com", "test2@example.com")"""
+ )
def _clear_applications(self):
frappe.db.sql("""delete from `tabLeave Application`""")
@@ -95,77 +113,232 @@ class TestLeaveApplication(unittest.TestCase):
application.to_date = "2013-01-05"
return application
+ @set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+ def test_validate_application_across_allocations(self):
+ # Test validation for application dates when negative balance is disabled
+ frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1)
+ leave_type = frappe.get_doc(
+ dict(leave_type_name="Test Leave Validation", doctype="Leave Type", allow_negative=False)
+ ).insert()
+
+ employee = get_employee()
+ date = getdate()
+ first_sunday = get_first_sunday(self.holiday_list, for_date=get_year_start(date))
+
+ leave_application = frappe.get_doc(
+ dict(
+ doctype="Leave Application",
+ employee=employee.name,
+ leave_type=leave_type.name,
+ from_date=add_days(first_sunday, 1),
+ to_date=add_days(first_sunday, 4),
+ company="_Test Company",
+ status="Approved",
+ leave_approver="test@example.com",
+ )
+ )
+ # Application period cannot be outside leave allocation period
+ self.assertRaises(frappe.ValidationError, leave_application.insert)
+
+ make_allocation_record(
+ leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)
+ )
+
+ leave_application = frappe.get_doc(
+ dict(
+ doctype="Leave Application",
+ employee=employee.name,
+ leave_type=leave_type.name,
+ from_date=add_days(first_sunday, -10),
+ to_date=add_days(first_sunday, 1),
+ company="_Test Company",
+ status="Approved",
+ leave_approver="test@example.com",
+ )
+ )
+
+ # Application period cannot be across two allocation records
+ self.assertRaises(LeaveAcrossAllocationsError, leave_application.insert)
+
+ @set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+ def test_insufficient_leave_balance_validation(self):
+ # CASE 1: Validation when allow negative is disabled
+ frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1)
+ leave_type = frappe.get_doc(
+ dict(leave_type_name="Test Leave Validation", doctype="Leave Type", allow_negative=False)
+ ).insert()
+
+ employee = get_employee()
+ date = getdate()
+ first_sunday = get_first_sunday(self.holiday_list, for_date=get_year_start(date))
+
+ # allocate 2 leaves, apply for more
+ make_allocation_record(
+ leave_type=leave_type.name,
+ from_date=get_year_start(date),
+ to_date=get_year_ending(date),
+ leaves=2,
+ )
+ leave_application = frappe.get_doc(
+ dict(
+ doctype="Leave Application",
+ employee=employee.name,
+ leave_type=leave_type.name,
+ from_date=add_days(first_sunday, 1),
+ to_date=add_days(first_sunday, 3),
+ company="_Test Company",
+ status="Approved",
+ leave_approver="test@example.com",
+ )
+ )
+ self.assertRaises(InsufficientLeaveBalanceError, leave_application.insert)
+
+ # CASE 2: Allows creating application with a warning message when allow negative is enabled
+ frappe.db.set_value("Leave Type", "Test Leave Validation", "allow_negative", True)
+ make_leave_application(
+ employee.name, add_days(first_sunday, 1), add_days(first_sunday, 3), leave_type.name
+ )
+
+ @set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+ def test_separate_leave_ledger_entry_for_boundary_applications(self):
+ # When application falls in 2 different allocations and Allow Negative is enabled
+ # creates separate leave ledger entries
+ frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1)
+ leave_type = frappe.get_doc(
+ dict(
+ leave_type_name="Test Leave Validation",
+ doctype="Leave Type",
+ allow_negative=True,
+ include_holiday=True,
+ )
+ ).insert()
+
+ employee = get_employee()
+ date = getdate()
+ year_start = getdate(get_year_start(date))
+ year_end = getdate(get_year_ending(date))
+
+ make_allocation_record(leave_type=leave_type.name, from_date=year_start, to_date=year_end)
+ # application across allocations
+
+ # CASE 1: from date has no allocation, to date has an allocation / both dates have allocation
+ start_date = add_days(year_start, -10)
+ application = make_leave_application(
+ employee.name,
+ start_date,
+ add_days(year_start, 3),
+ leave_type.name,
+ half_day=1,
+ half_day_date=start_date,
+ )
+
+ # 2 separate leave ledger entries
+ ledgers = frappe.db.get_all(
+ "Leave Ledger Entry",
+ {"transaction_type": "Leave Application", "transaction_name": application.name},
+ ["leaves", "from_date", "to_date"],
+ order_by="from_date",
+ )
+ self.assertEqual(len(ledgers), 2)
+
+ self.assertEqual(ledgers[0].from_date, application.from_date)
+ self.assertEqual(ledgers[0].to_date, add_days(year_start, -1))
+
+ self.assertEqual(ledgers[1].from_date, year_start)
+ self.assertEqual(ledgers[1].to_date, application.to_date)
+
+ # CASE 2: from date has an allocation, to date has no allocation
+ application = make_leave_application(
+ employee.name, add_days(year_end, -3), add_days(year_end, 5), leave_type.name
+ )
+
+ # 2 separate leave ledger entries
+ ledgers = frappe.db.get_all(
+ "Leave Ledger Entry",
+ {"transaction_type": "Leave Application", "transaction_name": application.name},
+ ["leaves", "from_date", "to_date"],
+ order_by="from_date",
+ )
+ self.assertEqual(len(ledgers), 2)
+
+ self.assertEqual(ledgers[0].from_date, application.from_date)
+ self.assertEqual(ledgers[0].to_date, year_end)
+
+ self.assertEqual(ledgers[1].from_date, add_days(year_end, 1))
+ self.assertEqual(ledgers[1].to_date, application.to_date)
+
def test_overwrite_attendance(self):
- '''check attendance is automatically created on leave approval'''
+ """check attendance is automatically created on leave approval"""
make_allocation_record()
application = self.get_application(_test_records[0])
- application.status = 'Approved'
- application.from_date = '2018-01-01'
- application.to_date = '2018-01-03'
+ application.status = "Approved"
+ application.from_date = "2018-01-01"
+ application.to_date = "2018-01-03"
application.insert()
application.submit()
- attendance = frappe.get_all('Attendance', ['name', 'status', 'attendance_date'],
- dict(attendance_date=('between', ['2018-01-01', '2018-01-03']), docstatus=("!=", 2)))
+ attendance = frappe.get_all(
+ "Attendance",
+ ["name", "status", "attendance_date"],
+ dict(attendance_date=("between", ["2018-01-01", "2018-01-03"]), docstatus=("!=", 2)),
+ )
# attendance created for all 3 days
self.assertEqual(len(attendance), 3)
# all on leave
- self.assertTrue(all([d.status == 'On Leave' for d in attendance]))
+ self.assertTrue(all([d.status == "On Leave" for d in attendance]))
# dates
dates = [d.attendance_date for d in attendance]
- for d in ('2018-01-01', '2018-01-02', '2018-01-03'):
+ for d in ("2018-01-01", "2018-01-02", "2018-01-03"):
self.assertTrue(getdate(d) in dates)
+ @set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
def test_attendance_for_include_holidays(self):
# Case 1: leave type with 'Include holidays within leaves as leaves' enabled
frappe.delete_doc_if_exists("Leave Type", "Test Include Holidays", force=1)
- leave_type = frappe.get_doc(dict(
- leave_type_name="Test Include Holidays",
- doctype="Leave Type",
- include_holiday=True
- )).insert()
+ leave_type = frappe.get_doc(
+ dict(leave_type_name="Test Include Holidays", doctype="Leave Type", include_holiday=True)
+ ).insert()
date = getdate()
- make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
+ make_allocation_record(
+ leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)
+ )
- holiday_list = make_holiday_list()
employee = get_employee()
- frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list)
- first_sunday = get_first_sunday(holiday_list)
+ first_sunday = get_first_sunday(self.holiday_list)
- leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
+ leave_application = make_leave_application(
+ employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name
+ )
leave_application.reload()
self.assertEqual(leave_application.total_leave_days, 4)
- self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4)
+ self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 4)
leave_application.cancel()
+ @set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
def test_attendance_update_for_exclude_holidays(self):
# Case 2: leave type with 'Include holidays within leaves as leaves' disabled
frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1)
- leave_type = frappe.get_doc(dict(
- leave_type_name="Test Do Not Include Holidays",
- doctype="Leave Type",
- include_holiday=False
- )).insert()
+ leave_type = frappe.get_doc(
+ dict(
+ leave_type_name="Test Do Not Include Holidays", doctype="Leave Type", include_holiday=False
+ )
+ ).insert()
date = getdate()
- make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
+ make_allocation_record(
+ leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)
+ )
- holiday_list = make_holiday_list()
employee = get_employee()
- frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list)
- first_sunday = get_first_sunday(holiday_list)
+ first_sunday = get_first_sunday(self.holiday_list)
# already marked attendance on a holiday should be deleted in this case
- config = {
- "doctype": "Attendance",
- "employee": employee.name,
- "status": "Present"
- }
+ config = {"doctype": "Attendance", "employee": employee.name, "status": "Present"}
attendance_on_holiday = frappe.get_doc(config)
attendance_on_holiday.attendance_date = first_sunday
attendance_on_holiday.flags.ignore_validate = True
@@ -177,8 +350,11 @@ class TestLeaveApplication(unittest.TestCase):
attendance.flags.ignore_validate = True
attendance.save()
- leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
+ leave_application = make_leave_application(
+ employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name, employee.company
+ )
leave_application.reload()
+
# holiday should be excluded while marking attendance
self.assertEqual(leave_application.total_leave_days, 3)
self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 3)
@@ -193,11 +369,13 @@ class TestLeaveApplication(unittest.TestCase):
self._clear_roles()
from frappe.utils.user import add_role
+
add_role("test@example.com", "HR User")
clear_user_permissions_for_doctype("Employee")
- frappe.db.set_value("Department", "_Test Department - _TC",
- "leave_block_list", "_Test Leave Block List")
+ frappe.db.set_value(
+ "Department", "_Test Department - _TC", "leave_block_list", "_Test Leave Block List"
+ )
make_allocation_record()
@@ -220,6 +398,7 @@ class TestLeaveApplication(unittest.TestCase):
self._clear_applications()
from frappe.utils.user import add_role
+
add_role("test@example.com", "Employee")
frappe.set_user("test@example.com")
@@ -236,6 +415,7 @@ class TestLeaveApplication(unittest.TestCase):
self._clear_applications()
from frappe.utils.user import add_role
+
add_role("test@example.com", "Employee")
frappe.set_user("test@example.com")
@@ -269,6 +449,7 @@ class TestLeaveApplication(unittest.TestCase):
self._clear_applications()
from frappe.utils.user import add_role
+
add_role("test@example.com", "Employee")
frappe.set_user("test@example.com")
@@ -291,6 +472,7 @@ class TestLeaveApplication(unittest.TestCase):
self._clear_applications()
from frappe.utils.user import add_role
+
add_role("test@example.com", "Employee")
frappe.set_user("test@example.com")
@@ -320,51 +502,49 @@ class TestLeaveApplication(unittest.TestCase):
application.half_day_date = "2013-01-05"
application.insert()
+ @set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
def test_optional_leave(self):
leave_period = get_leave_period()
today = nowdate()
- holiday_list = 'Test Holiday List for Optional Holiday'
+ holiday_list = "Test Holiday List for Optional Holiday"
employee = get_employee()
- default_holiday_list = make_holiday_list()
- frappe.db.set_value("Company", employee.company, "default_holiday_list", default_holiday_list)
- first_sunday = get_first_sunday(default_holiday_list)
-
+ first_sunday = get_first_sunday(self.holiday_list)
optional_leave_date = add_days(first_sunday, 1)
- if not frappe.db.exists('Holiday List', holiday_list):
- frappe.get_doc(dict(
- doctype = 'Holiday List',
- holiday_list_name = holiday_list,
- from_date = add_months(today, -6),
- to_date = add_months(today, 6),
- holidays = [
- dict(holiday_date = optional_leave_date, description = 'Test')
- ]
- )).insert()
+ if not frappe.db.exists("Holiday List", holiday_list):
+ frappe.get_doc(
+ dict(
+ doctype="Holiday List",
+ holiday_list_name=holiday_list,
+ from_date=add_months(today, -6),
+ to_date=add_months(today, 6),
+ holidays=[dict(holiday_date=optional_leave_date, description="Test")],
+ )
+ ).insert()
- frappe.db.set_value('Leave Period', leave_period.name, 'optional_holiday_list', holiday_list)
- leave_type = 'Test Optional Type'
- if not frappe.db.exists('Leave Type', leave_type):
- frappe.get_doc(dict(
- leave_type_name = leave_type,
- doctype = 'Leave Type',
- is_optional_leave = 1
- )).insert()
+ frappe.db.set_value("Leave Period", leave_period.name, "optional_holiday_list", holiday_list)
+ leave_type = "Test Optional Type"
+ if not frappe.db.exists("Leave Type", leave_type):
+ frappe.get_doc(
+ dict(leave_type_name=leave_type, doctype="Leave Type", is_optional_leave=1)
+ ).insert()
allocate_leaves(employee, leave_period, leave_type, 10)
date = add_days(first_sunday, 2)
- leave_application = frappe.get_doc(dict(
- doctype = 'Leave Application',
- employee = employee.name,
- company = '_Test Company',
- description = "_Test Reason",
- leave_type = leave_type,
- from_date = date,
- to_date = date,
- ))
+ leave_application = frappe.get_doc(
+ dict(
+ doctype="Leave Application",
+ employee=employee.name,
+ company="_Test Company",
+ description="_Test Reason",
+ leave_type=leave_type,
+ from_date=date,
+ to_date=date,
+ )
+ )
# can only apply on optional holidays
self.assertRaises(NotAnOptionalHoliday, leave_application.insert)
@@ -382,118 +562,125 @@ class TestLeaveApplication(unittest.TestCase):
employee = get_employee()
leave_period = get_leave_period()
frappe.delete_doc_if_exists("Leave Type", "Test Leave Type", force=1)
- leave_type = frappe.get_doc(dict(
- leave_type_name = 'Test Leave Type',
- doctype = 'Leave Type',
- max_leaves_allowed = 5
- )).insert()
+ leave_type = frappe.get_doc(
+ dict(leave_type_name="Test Leave Type", doctype="Leave Type", max_leaves_allowed=5)
+ ).insert()
date = add_days(nowdate(), -7)
allocate_leaves(employee, leave_period, leave_type.name, 5)
- leave_application = frappe.get_doc(dict(
- doctype = 'Leave Application',
- employee = employee.name,
- leave_type = leave_type.name,
- description = "_Test Reason",
- from_date = date,
- to_date = add_days(date, 2),
- company = "_Test Company",
- docstatus = 1,
- status = "Approved"
- ))
+ leave_application = frappe.get_doc(
+ dict(
+ doctype="Leave Application",
+ employee=employee.name,
+ leave_type=leave_type.name,
+ description="_Test Reason",
+ from_date=date,
+ to_date=add_days(date, 2),
+ company="_Test Company",
+ docstatus=1,
+ status="Approved",
+ )
+ )
leave_application.submit()
- leave_application = frappe.get_doc(dict(
- doctype = 'Leave Application',
- employee = employee.name,
- leave_type = leave_type.name,
- description = "_Test Reason",
- from_date = add_days(date, 4),
- to_date = add_days(date, 8),
- company = "_Test Company",
- docstatus = 1,
- status = "Approved"
- ))
+ leave_application = frappe.get_doc(
+ dict(
+ doctype="Leave Application",
+ employee=employee.name,
+ leave_type=leave_type.name,
+ description="_Test Reason",
+ from_date=add_days(date, 4),
+ to_date=add_days(date, 8),
+ company="_Test Company",
+ docstatus=1,
+ status="Approved",
+ )
+ )
self.assertRaises(frappe.ValidationError, leave_application.insert)
def test_applicable_after(self):
employee = get_employee()
leave_period = get_leave_period()
frappe.delete_doc_if_exists("Leave Type", "Test Leave Type", force=1)
- leave_type = frappe.get_doc(dict(
- leave_type_name = 'Test Leave Type',
- doctype = 'Leave Type',
- applicable_after = 15
- )).insert()
+ leave_type = frappe.get_doc(
+ dict(leave_type_name="Test Leave Type", doctype="Leave Type", applicable_after=15)
+ ).insert()
date = add_days(nowdate(), -7)
- frappe.db.set_value('Employee', employee.name, "date_of_joining", date)
+ frappe.db.set_value("Employee", employee.name, "date_of_joining", date)
allocate_leaves(employee, leave_period, leave_type.name, 10)
- leave_application = frappe.get_doc(dict(
- doctype = 'Leave Application',
- employee = employee.name,
- leave_type = leave_type.name,
- description = "_Test Reason",
- from_date = date,
- to_date = add_days(date, 4),
- company = "_Test Company",
- docstatus = 1,
- status = "Approved"
- ))
+ leave_application = frappe.get_doc(
+ dict(
+ doctype="Leave Application",
+ employee=employee.name,
+ leave_type=leave_type.name,
+ description="_Test Reason",
+ from_date=date,
+ to_date=add_days(date, 4),
+ company="_Test Company",
+ docstatus=1,
+ status="Approved",
+ )
+ )
self.assertRaises(frappe.ValidationError, leave_application.insert)
frappe.delete_doc_if_exists("Leave Type", "Test Leave Type 1", force=1)
- leave_type_1 = frappe.get_doc(dict(
- leave_type_name = 'Test Leave Type 1',
- doctype = 'Leave Type'
- )).insert()
+ leave_type_1 = frappe.get_doc(
+ dict(leave_type_name="Test Leave Type 1", doctype="Leave Type")
+ ).insert()
allocate_leaves(employee, leave_period, leave_type_1.name, 10)
- leave_application = frappe.get_doc(dict(
- doctype = 'Leave Application',
- employee = employee.name,
- leave_type = leave_type_1.name,
- description = "_Test Reason",
- from_date = date,
- to_date = add_days(date, 4),
- company = "_Test Company",
- docstatus = 1,
- status = "Approved"
- ))
+ leave_application = frappe.get_doc(
+ dict(
+ doctype="Leave Application",
+ employee=employee.name,
+ leave_type=leave_type_1.name,
+ description="_Test Reason",
+ from_date=date,
+ to_date=add_days(date, 4),
+ company="_Test Company",
+ docstatus=1,
+ status="Approved",
+ )
+ )
self.assertTrue(leave_application.insert())
- frappe.db.set_value('Employee', employee.name, "date_of_joining", "2010-01-01")
+ frappe.db.set_value("Employee", employee.name, "date_of_joining", "2010-01-01")
def test_max_continuous_leaves(self):
employee = get_employee()
leave_period = get_leave_period()
frappe.delete_doc_if_exists("Leave Type", "Test Leave Type", force=1)
- leave_type = frappe.get_doc(dict(
- leave_type_name = 'Test Leave Type',
- doctype = 'Leave Type',
- max_leaves_allowed = 15,
- max_continuous_days_allowed = 3
- )).insert()
+ leave_type = frappe.get_doc(
+ dict(
+ leave_type_name="Test Leave Type",
+ doctype="Leave Type",
+ max_leaves_allowed=15,
+ max_continuous_days_allowed=3,
+ )
+ ).insert()
date = add_days(nowdate(), -7)
allocate_leaves(employee, leave_period, leave_type.name, 10)
- leave_application = frappe.get_doc(dict(
- doctype = 'Leave Application',
- employee = employee.name,
- leave_type = leave_type.name,
- description = "_Test Reason",
- from_date = date,
- to_date = add_days(date, 4),
- company = "_Test Company",
- docstatus = 1,
- status = "Approved"
- ))
+ leave_application = frappe.get_doc(
+ dict(
+ doctype="Leave Application",
+ employee=employee.name,
+ leave_type=leave_type.name,
+ description="_Test Reason",
+ from_date=date,
+ to_date=add_days(date, 4),
+ company="_Test Company",
+ docstatus=1,
+ status="Approved",
+ )
+ )
self.assertRaises(frappe.ValidationError, leave_application.insert)
@@ -502,57 +689,69 @@ class TestLeaveApplication(unittest.TestCase):
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
- expire_carry_forwarded_leaves_after_days=90)
- leave_type.submit()
+ expire_carry_forwarded_leaves_after_days=90,
+ )
+ leave_type.insert()
create_carry_forwarded_allocation(employee, leave_type)
+ details = get_leave_balance_on(
+ employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8), for_consumption=True
+ )
- self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21)
+ self.assertEqual(details.leave_balance_for_consumption, 21)
+ self.assertEqual(details.leave_balance, 30)
def test_earned_leaves_creation(self):
- frappe.db.sql('''delete from `tabLeave Period`''')
- frappe.db.sql('''delete from `tabLeave Policy Assignment`''')
- frappe.db.sql('''delete from `tabLeave Allocation`''')
- frappe.db.sql('''delete from `tabLeave Ledger Entry`''')
+ frappe.db.sql("""delete from `tabLeave Period`""")
+ frappe.db.sql("""delete from `tabLeave Policy Assignment`""")
+ frappe.db.sql("""delete from `tabLeave Allocation`""")
+ frappe.db.sql("""delete from `tabLeave Ledger Entry`""")
leave_period = get_leave_period()
employee = get_employee()
- leave_type = 'Test Earned Leave Type'
- frappe.delete_doc_if_exists("Leave Type", 'Test Earned Leave Type', force=1)
- frappe.get_doc(dict(
- leave_type_name = leave_type,
- doctype = 'Leave Type',
- is_earned_leave = 1,
- earned_leave_frequency = 'Monthly',
- rounding = 0.5,
- max_leaves_allowed = 6
- )).insert()
+ leave_type = "Test Earned Leave Type"
+ frappe.delete_doc_if_exists("Leave Type", "Test Earned Leave Type", force=1)
+ frappe.get_doc(
+ dict(
+ leave_type_name=leave_type,
+ doctype="Leave Type",
+ is_earned_leave=1,
+ earned_leave_frequency="Monthly",
+ rounding=0.5,
+ max_leaves_allowed=6,
+ )
+ ).insert()
- leave_policy = frappe.get_doc({
- "doctype": "Leave Policy",
- "leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}]
- }).insert()
+ leave_policy = frappe.get_doc(
+ {
+ "doctype": "Leave Policy",
+ "leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}],
+ }
+ ).insert()
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
- "leave_period": leave_period.name
+ "leave_period": leave_period.name,
}
- leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
+ leave_policy_assignments = create_assignment_for_multiple_employees(
+ [employee.name], frappe._dict(data)
+ )
from erpnext.hr.utils import allocate_earned_leaves
+
i = 0
- while(i<14):
+ while i < 14:
allocate_earned_leaves(ignore_duplicates=True)
i += 1
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
# validate earned leaves creation without maximum leaves
- frappe.db.set_value('Leave Type', leave_type, 'max_leaves_allowed', 0)
+ frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
i = 0
- while(i<6):
+ while i < 6:
allocate_earned_leaves(ignore_duplicates=True)
i += 1
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
@@ -560,28 +759,36 @@ class TestLeaveApplication(unittest.TestCase):
# test to not consider current leave in leave balance while submitting
def test_current_leave_on_submit(self):
employee = get_employee()
- leave_type = 'Sick leave'
- allocation = frappe.get_doc(dict(
- doctype = 'Leave Allocation',
- employee = employee.name,
- leave_type = leave_type,
- from_date = '2018-10-01',
- to_date = '2018-10-10',
- new_leaves_allocated = 1
- ))
+
+ leave_type = "Sick Leave"
+ if not frappe.db.exists("Leave Type", leave_type):
+ frappe.get_doc(dict(leave_type_name=leave_type, doctype="Leave Type")).insert()
+
+ allocation = frappe.get_doc(
+ dict(
+ doctype="Leave Allocation",
+ employee=employee.name,
+ leave_type=leave_type,
+ from_date="2018-10-01",
+ to_date="2018-10-10",
+ new_leaves_allocated=1,
+ )
+ )
allocation.insert(ignore_permissions=True)
allocation.submit()
- leave_application = frappe.get_doc(dict(
- doctype = 'Leave Application',
- employee = employee.name,
- leave_type = leave_type,
- description = "_Test Reason",
- from_date = '2018-10-02',
- to_date = '2018-10-02',
- company = '_Test Company',
- status = 'Approved',
- leave_approver = 'test@example.com'
- ))
+ leave_application = frappe.get_doc(
+ dict(
+ doctype="Leave Application",
+ employee=employee.name,
+ leave_type=leave_type,
+ description="_Test Reason",
+ from_date="2018-10-02",
+ to_date="2018-10-02",
+ company="_Test Company",
+ status="Approved",
+ leave_approver="test@example.com",
+ )
+ )
self.assertTrue(leave_application.insert())
leave_application.submit()
self.assertEqual(leave_application.docstatus, 1)
@@ -589,26 +796,31 @@ class TestLeaveApplication(unittest.TestCase):
def test_creation_of_leave_ledger_entry_on_submit(self):
employee = get_employee()
- leave_type = create_leave_type(leave_type_name = 'Test Leave Type 1')
+ leave_type = create_leave_type(leave_type_name="Test Leave Type 1")
leave_type.save()
- leave_allocation = create_leave_allocation(employee=employee.name, employee_name=employee.employee_name,
- leave_type=leave_type.name)
+ leave_allocation = create_leave_allocation(
+ employee=employee.name, employee_name=employee.employee_name, leave_type=leave_type.name
+ )
leave_allocation.submit()
- leave_application = frappe.get_doc(dict(
- doctype = 'Leave Application',
- employee = employee.name,
- leave_type = leave_type.name,
- from_date = add_days(nowdate(), 1),
- to_date = add_days(nowdate(), 4),
- description = "_Test Reason",
- company = "_Test Company",
- docstatus = 1,
- status = "Approved"
- ))
+ leave_application = frappe.get_doc(
+ dict(
+ doctype="Leave Application",
+ employee=employee.name,
+ leave_type=leave_type.name,
+ from_date=add_days(nowdate(), 1),
+ to_date=add_days(nowdate(), 4),
+ description="_Test Reason",
+ company="_Test Company",
+ docstatus=1,
+ status="Approved",
+ )
+ )
leave_application.submit()
- leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_application.name))
+ leave_ledger_entry = frappe.get_all(
+ "Leave Ledger Entry", fields="*", filters=dict(transaction_name=leave_application.name)
+ )
self.assertEqual(leave_ledger_entry[0].employee, leave_application.employee)
self.assertEqual(leave_ledger_entry[0].leave_type, leave_application.leave_type)
@@ -616,37 +828,47 @@ class TestLeaveApplication(unittest.TestCase):
# check if leave ledger entry is deleted on cancellation
leave_application.cancel()
- self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_application.name}))
+ self.assertFalse(
+ frappe.db.exists("Leave Ledger Entry", {"transaction_name": leave_application.name})
+ )
def test_ledger_entry_creation_on_intermediate_allocation_expiry(self):
employee = get_employee()
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
- expire_carry_forwarded_leaves_after_days=90)
+ expire_carry_forwarded_leaves_after_days=90,
+ include_holiday=True,
+ )
leave_type.submit()
create_carry_forwarded_allocation(employee, leave_type)
- leave_application = frappe.get_doc(dict(
- doctype = 'Leave Application',
- employee = employee.name,
- leave_type = leave_type.name,
- from_date = add_days(nowdate(), -3),
- to_date = add_days(nowdate(), 7),
- description = "_Test Reason",
- company = "_Test Company",
- docstatus = 1,
- status = "Approved"
- ))
+ leave_application = frappe.get_doc(
+ dict(
+ doctype="Leave Application",
+ employee=employee.name,
+ leave_type=leave_type.name,
+ from_date=add_days(nowdate(), -3),
+ to_date=add_days(nowdate(), 7),
+ half_day=1,
+ half_day_date=add_days(nowdate(), -3),
+ description="_Test Reason",
+ company="_Test Company",
+ docstatus=1,
+ status="Approved",
+ )
+ )
leave_application.submit()
- leave_ledger_entry = frappe.get_all('Leave Ledger Entry', '*', filters=dict(transaction_name=leave_application.name))
+ leave_ledger_entry = frappe.get_all(
+ "Leave Ledger Entry", "*", filters=dict(transaction_name=leave_application.name)
+ )
self.assertEqual(len(leave_ledger_entry), 2)
self.assertEqual(leave_ledger_entry[0].employee, leave_application.employee)
self.assertEqual(leave_ledger_entry[0].leave_type, leave_application.leave_type)
- self.assertEqual(leave_ledger_entry[0].leaves, -9)
+ self.assertEqual(leave_ledger_entry[0].leaves, -8.5)
self.assertEqual(leave_ledger_entry[1].leaves, -2)
def test_leave_application_creation_after_expiry(self):
@@ -655,12 +877,18 @@ class TestLeaveApplication(unittest.TestCase):
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
- expire_carry_forwarded_leaves_after_days=90)
+ expire_carry_forwarded_leaves_after_days=90,
+ )
leave_type.submit()
create_carry_forwarded_allocation(employee, leave_type)
- self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, add_days(nowdate(), -85), add_days(nowdate(), -84)), 0)
+ self.assertEqual(
+ get_leave_balance_on(
+ employee.name, leave_type.name, add_days(nowdate(), -85), add_days(nowdate(), -84)
+ ),
+ 0,
+ )
def test_leave_approver_perms(self):
employee = get_employee()
@@ -676,8 +904,8 @@ class TestLeaveApplication(unittest.TestCase):
make_allocation_record(employee.name)
application = self.get_application(_test_records[0])
- application.from_date = '2018-01-01'
- application.to_date = '2018-01-03'
+ application.from_date = "2018-01-01"
+ application.to_date = "2018-01-03"
application.leave_approver = user
application.insert()
self.assertTrue(application.name in frappe.share.get_shared("Leave Application", user))
@@ -703,92 +931,171 @@ class TestLeaveApplication(unittest.TestCase):
employee.leave_approver = ""
employee.save()
+ @set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+ def test_get_leave_details_for_dashboard(self):
+ employee = get_employee()
+ date = getdate()
+ year_start = getdate(get_year_start(date))
+ year_end = getdate(get_year_ending(date))
+
+ # ALLOCATION = 30
+ allocation = make_allocation_record(
+ employee=employee.name, from_date=year_start, to_date=year_end
+ )
+
+ # USED LEAVES = 4
+ first_sunday = get_first_sunday(self.holiday_list)
+ leave_application = make_leave_application(
+ employee.name, add_days(first_sunday, 1), add_days(first_sunday, 4), "_Test Leave Type"
+ )
+ leave_application.reload()
+
+ # LEAVES PENDING APPROVAL = 1
+ leave_application = make_leave_application(
+ employee.name,
+ add_days(first_sunday, 5),
+ add_days(first_sunday, 5),
+ "_Test Leave Type",
+ submit=False,
+ )
+ leave_application.status = "Open"
+ leave_application.save()
+
+ details = get_leave_details(employee.name, allocation.from_date)
+ leave_allocation = details["leave_allocation"]["_Test Leave Type"]
+ self.assertEqual(leave_allocation["total_leaves"], 30)
+ self.assertEqual(leave_allocation["leaves_taken"], 4)
+ self.assertEqual(leave_allocation["expired_leaves"], 0)
+ self.assertEqual(leave_allocation["leaves_pending_approval"], 1)
+ self.assertEqual(leave_allocation["remaining_leaves"], 26)
+
+ @set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+ def test_get_leave_allocation_records(self):
+ employee = get_employee()
+ leave_type = create_leave_type(
+ leave_type_name="_Test_CF_leave_expiry",
+ is_carry_forward=1,
+ expire_carry_forwarded_leaves_after_days=90,
+ )
+ leave_type.insert()
+
+ leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
+ details = get_leave_allocation_records(employee.name, getdate(), leave_type.name)
+ expected_data = {
+ "from_date": getdate(leave_alloc.from_date),
+ "to_date": getdate(leave_alloc.to_date),
+ "total_leaves_allocated": 30.0,
+ "unused_leaves": 15.0,
+ "new_leaves_allocated": 15.0,
+ "leave_type": leave_type.name,
+ }
+ self.assertEqual(details.get(leave_type.name), expected_data)
+
def create_carry_forwarded_allocation(employee, leave_type):
- # initial leave allocation
- leave_allocation = create_leave_allocation(
- leave_type="_Test_CF_leave_expiry",
- employee=employee.name,
- employee_name=employee.employee_name,
- from_date=add_months(nowdate(), -24),
- to_date=add_months(nowdate(), -12),
- carry_forward=0)
- leave_allocation.submit()
+ # initial leave allocation
+ leave_allocation = create_leave_allocation(
+ leave_type="_Test_CF_leave_expiry",
+ employee=employee.name,
+ employee_name=employee.employee_name,
+ from_date=add_months(nowdate(), -24),
+ to_date=add_months(nowdate(), -12),
+ carry_forward=0,
+ )
+ leave_allocation.submit()
- leave_allocation = create_leave_allocation(
- leave_type="_Test_CF_leave_expiry",
- employee=employee.name,
- employee_name=employee.employee_name,
- from_date=add_days(nowdate(), -84),
- to_date=add_days(nowdate(), 100),
- carry_forward=1)
- leave_allocation.submit()
+ leave_allocation = create_leave_allocation(
+ leave_type="_Test_CF_leave_expiry",
+ employee=employee.name,
+ employee_name=employee.employee_name,
+ from_date=add_days(nowdate(), -84),
+ to_date=add_days(nowdate(), 100),
+ carry_forward=1,
+ )
+ leave_allocation.submit()
-def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None):
- allocation = frappe.get_doc({
- "doctype": "Leave Allocation",
- "employee": employee or "_T-Employee-00001",
- "leave_type": leave_type or "_Test Leave Type",
- "from_date": from_date or "2013-01-01",
- "to_date": to_date or "2019-12-31",
- "new_leaves_allocated": 30
- })
+ return leave_allocation
+
+
+def make_allocation_record(
+ employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None
+):
+ allocation = frappe.get_doc(
+ {
+ "doctype": "Leave Allocation",
+ "employee": employee or "_T-Employee-00001",
+ "leave_type": leave_type or "_Test Leave Type",
+ "from_date": from_date or "2013-01-01",
+ "to_date": to_date or "2019-12-31",
+ "new_leaves_allocated": leaves or 30,
+ "carry_forward": carry_forward,
+ }
+ )
allocation.insert(ignore_permissions=True)
allocation.submit()
+ return allocation
+
+
def get_employee():
return frappe.get_doc("Employee", "_T-Employee-00001")
+
def set_leave_approver():
employee = get_employee()
dept_doc = frappe.get_doc("Department", employee.department)
- dept_doc.append('leave_approvers', {
- 'approver': 'test@example.com'
- })
+ dept_doc.append("leave_approvers", {"approver": "test@example.com"})
dept_doc.save(ignore_permissions=True)
+
def get_leave_period():
- leave_period_name = frappe.db.exists({
- "doctype": "Leave Period",
- "company": "_Test Company"
- })
+ leave_period_name = frappe.db.exists({"doctype": "Leave Period", "company": "_Test Company"})
if leave_period_name:
return frappe.get_doc("Leave Period", leave_period_name[0][0])
else:
- return frappe.get_doc(dict(
- name = 'Test Leave Period',
- doctype = 'Leave Period',
- from_date = add_months(nowdate(), -6),
- to_date = add_months(nowdate(), 6),
- company = "_Test Company",
- is_active = 1
- )).insert()
+ return frappe.get_doc(
+ dict(
+ name="Test Leave Period",
+ doctype="Leave Period",
+ from_date=add_months(nowdate(), -6),
+ to_date=add_months(nowdate(), 6),
+ company="_Test Company",
+ is_active=1,
+ )
+ ).insert()
+
def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, eligible_leaves=0):
- allocate_leave = frappe.get_doc({
- "doctype": "Leave Allocation",
- "__islocal": 1,
- "employee": employee.name,
- "employee_name": employee.employee_name,
- "leave_type": leave_type,
- "from_date": leave_period.from_date,
- "to_date": leave_period.to_date,
- "new_leaves_allocated": new_leaves_allocated,
- "docstatus": 1
- }).insert()
+ allocate_leave = frappe.get_doc(
+ {
+ "doctype": "Leave Allocation",
+ "__islocal": 1,
+ "employee": employee.name,
+ "employee_name": employee.employee_name,
+ "leave_type": leave_type,
+ "from_date": leave_period.from_date,
+ "to_date": leave_period.to_date,
+ "new_leaves_allocated": new_leaves_allocated,
+ "docstatus": 1,
+ }
+ ).insert()
allocate_leave.submit()
-def get_first_sunday(holiday_list):
- month_start_date = get_first_day(nowdate())
- month_end_date = get_last_day(nowdate())
- first_sunday = frappe.db.sql("""
+def get_first_sunday(holiday_list, for_date=None):
+ date = for_date or getdate()
+ month_start_date = get_first_day(date)
+ month_end_date = get_last_day(date)
+ first_sunday = frappe.db.sql(
+ """
select holiday_date from `tabHoliday`
where parent = %s
and holiday_date between %s and %s
order by holiday_date
- """, (holiday_list, month_start_date, month_end_date))[0][0]
+ """,
+ (holiday_list, month_start_date, month_end_date),
+ )[0][0]
- return first_sunday
\ No newline at end of file
+ return first_sunday
diff --git a/erpnext/hr/doctype/leave_block_list/leave_block_list.py b/erpnext/hr/doctype/leave_block_list/leave_block_list.py
index d6b77f984cf..a57ba84e38d 100644
--- a/erpnext/hr/doctype/leave_block_list/leave_block_list.py
+++ b/erpnext/hr/doctype/leave_block_list/leave_block_list.py
@@ -10,7 +10,6 @@ from frappe.model.document import Document
class LeaveBlockList(Document):
-
def validate(self):
dates = []
for d in self.get("leave_block_list_dates"):
@@ -20,23 +19,29 @@ class LeaveBlockList(Document):
frappe.msgprint(_("Date is repeated") + ":" + d.block_date, raise_exception=1)
dates.append(d.block_date)
+
@frappe.whitelist()
-def get_applicable_block_dates(from_date, to_date, employee=None,
- company=None, all_lists=False):
+def get_applicable_block_dates(from_date, to_date, employee=None, company=None, all_lists=False):
block_dates = []
for block_list in get_applicable_block_lists(employee, company, all_lists):
- block_dates.extend(frappe.db.sql("""select block_date, reason
+ block_dates.extend(
+ frappe.db.sql(
+ """select block_date, reason
from `tabLeave Block List Date` where parent=%s
- and block_date between %s and %s""", (block_list, from_date, to_date),
- as_dict=1))
+ and block_date between %s and %s""",
+ (block_list, from_date, to_date),
+ as_dict=1,
+ )
+ )
return block_dates
+
def get_applicable_block_lists(employee=None, company=None, all_lists=False):
block_lists = []
if not employee:
- employee = frappe.db.get_value("Employee", {"user_id":frappe.session.user})
+ employee = frappe.db.get_value("Employee", {"user_id": frappe.session.user})
if not employee:
return []
@@ -49,18 +54,25 @@ def get_applicable_block_lists(employee=None, company=None, all_lists=False):
block_lists.append(block_list)
# per department
- department = frappe.db.get_value("Employee",employee, "department")
+ department = frappe.db.get_value("Employee", employee, "department")
if department:
block_list = frappe.db.get_value("Department", department, "leave_block_list")
add_block_list(block_list)
# global
- for block_list in frappe.db.sql_list("""select name from `tabLeave Block List`
- where applies_to_all_departments=1 and company=%s""", company):
+ for block_list in frappe.db.sql_list(
+ """select name from `tabLeave Block List`
+ where applies_to_all_departments=1 and company=%s""",
+ company,
+ ):
add_block_list(block_list)
return list(set(block_lists))
+
def is_user_in_allow_list(block_list):
- return frappe.session.user in frappe.db.sql_list("""select allow_user
- from `tabLeave Block List Allow` where parent=%s""", block_list)
+ return frappe.session.user in frappe.db.sql_list(
+ """select allow_user
+ from `tabLeave Block List Allow` where parent=%s""",
+ block_list,
+ )
diff --git a/erpnext/hr/doctype/leave_block_list/leave_block_list_dashboard.py b/erpnext/hr/doctype/leave_block_list/leave_block_list_dashboard.py
index f91a8fe5201..afeb5ded39e 100644
--- a/erpnext/hr/doctype/leave_block_list/leave_block_list_dashboard.py
+++ b/erpnext/hr/doctype/leave_block_list/leave_block_list_dashboard.py
@@ -1,11 +1,2 @@
-
-
def get_data():
- return {
- 'fieldname': 'leave_block_list',
- 'transactions': [
- {
- 'items': ['Department']
- }
- ]
- }
+ return {"fieldname": "leave_block_list", "transactions": [{"items": ["Department"]}]}
diff --git a/erpnext/hr/doctype/leave_block_list/test_leave_block_list.py b/erpnext/hr/doctype/leave_block_list/test_leave_block_list.py
index afbabb66a4a..be85a354149 100644
--- a/erpnext/hr/doctype/leave_block_list/test_leave_block_list.py
+++ b/erpnext/hr/doctype/leave_block_list/test_leave_block_list.py
@@ -15,24 +15,36 @@ class TestLeaveBlockList(unittest.TestCase):
def test_get_applicable_block_dates(self):
frappe.set_user("test@example.com")
- frappe.db.set_value("Department", "_Test Department - _TC", "leave_block_list",
- "_Test Leave Block List")
- self.assertTrue(getdate("2013-01-02") in
- [d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03")])
+ frappe.db.set_value(
+ "Department", "_Test Department - _TC", "leave_block_list", "_Test Leave Block List"
+ )
+ self.assertTrue(
+ getdate("2013-01-02")
+ in [d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03")]
+ )
def test_get_applicable_block_dates_for_allowed_user(self):
frappe.set_user("test1@example.com")
- frappe.db.set_value("Department", "_Test Department 1 - _TC", "leave_block_list",
- "_Test Leave Block List")
- self.assertEqual([], [d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03")])
+ frappe.db.set_value(
+ "Department", "_Test Department 1 - _TC", "leave_block_list", "_Test Leave Block List"
+ )
+ self.assertEqual(
+ [], [d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03")]
+ )
def test_get_applicable_block_dates_all_lists(self):
frappe.set_user("test1@example.com")
- frappe.db.set_value("Department", "_Test Department 1 - _TC", "leave_block_list",
- "_Test Leave Block List")
- self.assertTrue(getdate("2013-01-02") in
- [d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03", all_lists=True)])
+ frappe.db.set_value(
+ "Department", "_Test Department 1 - _TC", "leave_block_list", "_Test Leave Block List"
+ )
+ self.assertTrue(
+ getdate("2013-01-02")
+ in [
+ d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03", all_lists=True)
+ ]
+ )
+
test_dependencies = ["Employee"]
-test_records = frappe.get_test_records('Leave Block List')
+test_records = frappe.get_test_records("Leave Block List")
diff --git a/erpnext/hr/doctype/leave_control_panel/leave_control_panel.py b/erpnext/hr/doctype/leave_control_panel/leave_control_panel.py
index 19f97b83d47..c57f8ae72bf 100644
--- a/erpnext/hr/doctype/leave_control_panel/leave_control_panel.py
+++ b/erpnext/hr/doctype/leave_control_panel/leave_control_panel.py
@@ -18,8 +18,12 @@ class LeaveControlPanel(Document):
condition_str = " and " + " and ".join(conditions) if len(conditions) else ""
- e = frappe.db.sql("select name from tabEmployee where status='Active' {condition}"
- .format(condition=condition_str), tuple(values))
+ e = frappe.db.sql(
+ "select name from tabEmployee where status='Active' {condition}".format(
+ condition=condition_str
+ ),
+ tuple(values),
+ )
return e
@@ -27,7 +31,7 @@ class LeaveControlPanel(Document):
for f in ["from_date", "to_date", "leave_type", "no_of_days"]:
if not self.get(f):
frappe.throw(_("{0} is required").format(self.meta.get_label(f)))
- self.validate_from_to_dates('from_date', 'to_date')
+ self.validate_from_to_dates("from_date", "to_date")
@frappe.whitelist()
def allocate_leave(self):
@@ -39,10 +43,10 @@ class LeaveControlPanel(Document):
for d in self.get_employees():
try:
- la = frappe.new_doc('Leave Allocation')
+ la = frappe.new_doc("Leave Allocation")
la.set("__islocal", 1)
la.employee = cstr(d[0])
- la.employee_name = frappe.db.get_value('Employee',cstr(d[0]),'employee_name')
+ la.employee_name = frappe.db.get_value("Employee", cstr(d[0]), "employee_name")
la.leave_type = self.leave_type
la.from_date = self.from_date
la.to_date = self.to_date
diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
index 8ef0e36fb8d..0f655e3e0fc 100644
--- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py
+++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
@@ -26,9 +26,12 @@ class LeaveEncashment(Document):
self.encashment_date = getdate(nowdate())
def validate_salary_structure(self):
- if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}):
- frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee))
-
+ if not frappe.db.exists("Salary Structure Assignment", {"employee": self.employee}):
+ frappe.throw(
+ _("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(
+ self.employee
+ )
+ )
def before_submit(self):
if self.encashment_amount <= 0:
@@ -36,7 +39,7 @@ class LeaveEncashment(Document):
def on_submit(self):
if not self.leave_allocation:
- self.leave_allocation = self.get_leave_allocation().get('name')
+ self.leave_allocation = self.get_leave_allocation().get("name")
additional_salary = frappe.new_doc("Additional Salary")
additional_salary.company = frappe.get_value("Employee", self.employee, "company")
additional_salary.employee = self.employee
@@ -52,8 +55,13 @@ class LeaveEncashment(Document):
additional_salary.submit()
# Set encashed leaves in Allocation
- frappe.db.set_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed",
- frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') + self.encashable_days)
+ frappe.db.set_value(
+ "Leave Allocation",
+ self.leave_allocation,
+ "total_leaves_encashed",
+ frappe.db.get_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed")
+ + self.encashable_days,
+ )
self.create_leave_ledger_entry()
@@ -63,40 +71,69 @@ class LeaveEncashment(Document):
self.db_set("additional_salary", "")
if self.leave_allocation:
- frappe.db.set_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed",
- frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') - self.encashable_days)
+ frappe.db.set_value(
+ "Leave Allocation",
+ self.leave_allocation,
+ "total_leaves_encashed",
+ frappe.db.get_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed")
+ - self.encashable_days,
+ )
self.create_leave_ledger_entry(submit=False)
@frappe.whitelist()
def get_leave_details_for_encashment(self):
- salary_structure = get_assigned_salary_structure(self.employee, self.encashment_date or getdate(nowdate()))
+ salary_structure = get_assigned_salary_structure(
+ self.employee, self.encashment_date or getdate(nowdate())
+ )
if not salary_structure:
- frappe.throw(_("No Salary Structure assigned for Employee {0} on given date {1}").format(self.employee, self.encashment_date))
+ frappe.throw(
+ _("No Salary Structure assigned for Employee {0} on given date {1}").format(
+ self.employee, self.encashment_date
+ )
+ )
- if not frappe.db.get_value("Leave Type", self.leave_type, 'allow_encashment'):
+ if not frappe.db.get_value("Leave Type", self.leave_type, "allow_encashment"):
frappe.throw(_("Leave Type {0} is not encashable").format(self.leave_type))
allocation = self.get_leave_allocation()
if not allocation:
- frappe.throw(_("No Leaves Allocated to Employee: {0} for Leave Type: {1}").format(self.employee, self.leave_type))
+ frappe.throw(
+ _("No Leaves Allocated to Employee: {0} for Leave Type: {1}").format(
+ self.employee, self.leave_type
+ )
+ )
- self.leave_balance = allocation.total_leaves_allocated - allocation.carry_forwarded_leaves_count\
+ self.leave_balance = (
+ allocation.total_leaves_allocated
+ - allocation.carry_forwarded_leaves_count
- get_unused_leaves(self.employee, self.leave_type, allocation.from_date, self.encashment_date)
+ )
- encashable_days = self.leave_balance - frappe.db.get_value('Leave Type', self.leave_type, 'encashment_threshold_days')
+ encashable_days = self.leave_balance - frappe.db.get_value(
+ "Leave Type", self.leave_type, "encashment_threshold_days"
+ )
self.encashable_days = encashable_days if encashable_days > 0 else 0
- per_day_encashment = frappe.db.get_value('Salary Structure', salary_structure , 'leave_encashment_amount_per_day')
- self.encashment_amount = self.encashable_days * per_day_encashment if per_day_encashment > 0 else 0
+ per_day_encashment = frappe.db.get_value(
+ "Salary Structure", salary_structure, "leave_encashment_amount_per_day"
+ )
+ self.encashment_amount = (
+ self.encashable_days * per_day_encashment if per_day_encashment > 0 else 0
+ )
self.leave_allocation = allocation.name
return True
def get_leave_allocation(self):
- leave_allocation = frappe.db.sql("""select name, to_date, total_leaves_allocated, carry_forwarded_leaves_count from `tabLeave Allocation` where '{0}'
+ leave_allocation = frappe.db.sql(
+ """select name, to_date, total_leaves_allocated, carry_forwarded_leaves_count from `tabLeave Allocation` where '{0}'
between from_date and to_date and docstatus=1 and leave_type='{1}'
- and employee= '{2}'""".format(self.encashment_date or getdate(nowdate()), self.leave_type, self.employee), as_dict=1) #nosec
+ and employee= '{2}'""".format(
+ self.encashment_date or getdate(nowdate()), self.leave_type, self.employee
+ ),
+ as_dict=1,
+ ) # nosec
return leave_allocation[0] if leave_allocation else None
@@ -105,7 +142,7 @@ class LeaveEncashment(Document):
leaves=self.encashable_days * -1,
from_date=self.encashment_date,
to_date=self.encashment_date,
- is_carry_forward=0
+ is_carry_forward=0,
)
create_leave_ledger_entry(self, args, submit)
@@ -114,27 +151,26 @@ class LeaveEncashment(Document):
if not leave_allocation:
return
- to_date = leave_allocation.get('to_date')
+ to_date = leave_allocation.get("to_date")
if to_date < getdate(nowdate()):
args = frappe._dict(
- leaves=self.encashable_days,
- from_date=to_date,
- to_date=to_date,
- is_carry_forward=0
+ leaves=self.encashable_days, from_date=to_date, to_date=to_date, is_carry_forward=0
)
create_leave_ledger_entry(self, args, submit)
def create_leave_encashment(leave_allocation):
- ''' Creates leave encashment for the given allocations '''
+ """Creates leave encashment for the given allocations"""
for allocation in leave_allocation:
if not get_assigned_salary_structure(allocation.employee, allocation.to_date):
continue
- leave_encashment = frappe.get_doc(dict(
- doctype="Leave Encashment",
- leave_period=allocation.leave_period,
- employee=allocation.employee,
- leave_type=allocation.leave_type,
- encashment_date=allocation.to_date
- ))
+ leave_encashment = frappe.get_doc(
+ dict(
+ doctype="Leave Encashment",
+ leave_period=allocation.leave_period,
+ employee=allocation.employee,
+ leave_type=allocation.leave_type,
+ encashment_date=allocation.to_date,
+ )
+ )
leave_encashment.insert(ignore_permissions=True)
diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
index 99a479d3e5c..83eb969feb0 100644
--- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
+++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
@@ -16,18 +16,19 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_
test_dependencies = ["Leave Type"]
+
class TestLeaveEncashment(unittest.TestCase):
def setUp(self):
- frappe.db.sql('''delete from `tabLeave Period`''')
- frappe.db.sql('''delete from `tabLeave Policy Assignment`''')
- frappe.db.sql('''delete from `tabLeave Allocation`''')
- frappe.db.sql('''delete from `tabLeave Ledger Entry`''')
- frappe.db.sql('''delete from `tabAdditional Salary`''')
+ frappe.db.sql("""delete from `tabLeave Period`""")
+ frappe.db.sql("""delete from `tabLeave Policy Assignment`""")
+ frappe.db.sql("""delete from `tabLeave Allocation`""")
+ frappe.db.sql("""delete from `tabLeave Ledger Entry`""")
+ frappe.db.sql("""delete from `tabAdditional Salary`""")
# create the leave policy
leave_policy = create_leave_policy(
- leave_type="_Test Leave Type Encashment",
- annual_allocation=10)
+ leave_type="_Test Leave Type Encashment", annual_allocation=10
+ )
leave_policy.submit()
# create employee, salary structure and assignment
@@ -38,28 +39,44 @@ class TestLeaveEncashment(unittest.TestCase):
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
- "leave_period": self.leave_period.name
+ "leave_period": self.leave_period.name,
}
- leave_policy_assignments = create_assignment_for_multiple_employees([self.employee], frappe._dict(data))
+ leave_policy_assignments = create_assignment_for_multiple_employees(
+ [self.employee], frappe._dict(data)
+ )
- salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", self.employee,
- other_details={"leave_encashment_amount_per_day": 50})
+ salary_structure = make_salary_structure(
+ "Salary Structure for Encashment",
+ "Monthly",
+ self.employee,
+ other_details={"leave_encashment_amount_per_day": 50},
+ )
def tearDown(self):
- for dt in ["Leave Period", "Leave Allocation", "Leave Ledger Entry", "Additional Salary", "Leave Encashment", "Salary Structure", "Leave Policy"]:
+ for dt in [
+ "Leave Period",
+ "Leave Allocation",
+ "Leave Ledger Entry",
+ "Additional Salary",
+ "Leave Encashment",
+ "Salary Structure",
+ "Leave Policy",
+ ]:
frappe.db.sql("delete from `tab%s`" % dt)
def test_leave_balance_value_and_amount(self):
- frappe.db.sql('''delete from `tabLeave Encashment`''')
- leave_encashment = frappe.get_doc(dict(
- doctype='Leave Encashment',
- employee=self.employee,
- leave_type="_Test Leave Type Encashment",
- leave_period=self.leave_period.name,
- payroll_date=today(),
- currency="INR"
- )).insert()
+ frappe.db.sql("""delete from `tabLeave Encashment`""")
+ leave_encashment = frappe.get_doc(
+ dict(
+ doctype="Leave Encashment",
+ employee=self.employee,
+ leave_type="_Test Leave Type Encashment",
+ leave_period=self.leave_period.name,
+ payroll_date=today(),
+ currency="INR",
+ )
+ ).insert()
self.assertEqual(leave_encashment.leave_balance, 10)
self.assertEqual(leave_encashment.encashable_days, 5)
@@ -68,23 +85,27 @@ class TestLeaveEncashment(unittest.TestCase):
leave_encashment.submit()
# assert links
- add_sal = frappe.get_all("Additional Salary", filters = {"ref_docname": leave_encashment.name})[0]
+ add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0]
self.assertTrue(add_sal)
def test_creation_of_leave_ledger_entry_on_submit(self):
- frappe.db.sql('''delete from `tabLeave Encashment`''')
- leave_encashment = frappe.get_doc(dict(
- doctype='Leave Encashment',
- employee=self.employee,
- leave_type="_Test Leave Type Encashment",
- leave_period=self.leave_period.name,
- payroll_date=today(),
- currency="INR"
- )).insert()
+ frappe.db.sql("""delete from `tabLeave Encashment`""")
+ leave_encashment = frappe.get_doc(
+ dict(
+ doctype="Leave Encashment",
+ employee=self.employee,
+ leave_type="_Test Leave Type Encashment",
+ leave_period=self.leave_period.name,
+ payroll_date=today(),
+ currency="INR",
+ )
+ ).insert()
leave_encashment.submit()
- leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_encashment.name))
+ leave_ledger_entry = frappe.get_all(
+ "Leave Ledger Entry", fields="*", filters=dict(transaction_name=leave_encashment.name)
+ )
self.assertEqual(len(leave_ledger_entry), 1)
self.assertEqual(leave_ledger_entry[0].employee, leave_encashment.employee)
@@ -93,7 +114,11 @@ class TestLeaveEncashment(unittest.TestCase):
# check if leave ledger entry is deleted on cancellation
- frappe.db.sql("Delete from `tabAdditional Salary` WHERE ref_docname = %s", (leave_encashment.name) )
+ frappe.db.sql(
+ "Delete from `tabAdditional Salary` WHERE ref_docname = %s", (leave_encashment.name)
+ )
leave_encashment.cancel()
- self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_encashment.name}))
+ self.assertFalse(
+ frappe.db.exists("Leave Ledger Entry", {"transaction_name": leave_encashment.name})
+ )
diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
index 5c5299ea7eb..fed9f770dfc 100644
--- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
+++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
@@ -20,9 +20,11 @@ class LeaveLedgerEntry(Document):
else:
frappe.throw(_("Only expired allocation can be cancelled"))
+
def validate_leave_allocation_against_leave_application(ledger):
- ''' Checks that leave allocation has no leave application against it '''
- leave_application_records = frappe.db.sql_list("""
+ """Checks that leave allocation has no leave application against it"""
+ leave_application_records = frappe.db.sql_list(
+ """
SELECT transaction_name
FROM `tabLeave Ledger Entry`
WHERE
@@ -31,15 +33,21 @@ def validate_leave_allocation_against_leave_application(ledger):
AND transaction_type='Leave Application'
AND from_date>=%s
AND to_date<=%s
- """, (ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date))
+ """,
+ (ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date),
+ )
if leave_application_records:
- frappe.throw(_("Leave allocation {0} is linked with the Leave Application {1}").format(
- ledger.transaction_name, ', '.join(leave_application_records)))
+ frappe.throw(
+ _("Leave allocation {0} is linked with the Leave Application {1}").format(
+ ledger.transaction_name, ", ".join(leave_application_records)
+ )
+ )
+
def create_leave_ledger_entry(ref_doc, args, submit=True):
ledger = frappe._dict(
- doctype='Leave Ledger Entry',
+ doctype="Leave Ledger Entry",
employee=ref_doc.employee,
employee_name=ref_doc.employee_name,
leave_type=ref_doc.leave_type,
@@ -47,7 +55,7 @@ def create_leave_ledger_entry(ref_doc, args, submit=True):
transaction_name=ref_doc.name,
is_carry_forward=0,
is_expired=0,
- is_lwp=0
+ is_lwp=0,
)
ledger.update(args)
@@ -58,54 +66,69 @@ def create_leave_ledger_entry(ref_doc, args, submit=True):
else:
delete_ledger_entry(ledger)
+
def delete_ledger_entry(ledger):
- ''' Delete ledger entry on cancel of leave application/allocation/encashment '''
+ """Delete ledger entry on cancel of leave application/allocation/encashment"""
if ledger.transaction_type == "Leave Allocation":
validate_leave_allocation_against_leave_application(ledger)
expired_entry = get_previous_expiry_ledger_entry(ledger)
- frappe.db.sql("""DELETE
+ frappe.db.sql(
+ """DELETE
FROM `tabLeave Ledger Entry`
WHERE
`transaction_name`=%s
- OR `name`=%s""", (ledger.transaction_name, expired_entry))
+ OR `name`=%s""",
+ (ledger.transaction_name, expired_entry),
+ )
+
def get_previous_expiry_ledger_entry(ledger):
- ''' Returns the expiry ledger entry having same creation date as the ledger entry to be cancelled '''
- creation_date = frappe.db.get_value("Leave Ledger Entry", filters={
- 'transaction_name': ledger.transaction_name,
- 'is_expired': 0,
- 'transaction_type': 'Leave Allocation'
- }, fieldname=['creation'])
+ """Returns the expiry ledger entry having same creation date as the ledger entry to be cancelled"""
+ creation_date = frappe.db.get_value(
+ "Leave Ledger Entry",
+ filters={
+ "transaction_name": ledger.transaction_name,
+ "is_expired": 0,
+ "transaction_type": "Leave Allocation",
+ },
+ fieldname=["creation"],
+ )
- creation_date = creation_date.strftime(DATE_FORMAT) if creation_date else ''
+ creation_date = creation_date.strftime(DATE_FORMAT) if creation_date else ""
+
+ return frappe.db.get_value(
+ "Leave Ledger Entry",
+ filters={
+ "creation": ("like", creation_date + "%"),
+ "employee": ledger.employee,
+ "leave_type": ledger.leave_type,
+ "is_expired": 1,
+ "docstatus": 1,
+ "is_carry_forward": 0,
+ },
+ fieldname=["name"],
+ )
- return frappe.db.get_value("Leave Ledger Entry", filters={
- 'creation': ('like', creation_date+"%"),
- 'employee': ledger.employee,
- 'leave_type': ledger.leave_type,
- 'is_expired': 1,
- 'docstatus': 1,
- 'is_carry_forward': 0
- }, fieldname=['name'])
def process_expired_allocation():
- ''' Check if a carry forwarded allocation has expired and create a expiry ledger entry
- Case 1: carry forwarded expiry period is set for the leave type,
- create a separate leave expiry entry against each entry of carry forwarded and non carry forwarded leaves
- Case 2: leave type has no specific expiry period for carry forwarded leaves
- and there is no carry forwarded leave allocation, create a single expiry against the remaining leaves.
- '''
+ """Check if a carry forwarded allocation has expired and create a expiry ledger entry
+ Case 1: carry forwarded expiry period is set for the leave type,
+ create a separate leave expiry entry against each entry of carry forwarded and non carry forwarded leaves
+ Case 2: leave type has no specific expiry period for carry forwarded leaves
+ and there is no carry forwarded leave allocation, create a single expiry against the remaining leaves.
+ """
# fetch leave type records that has carry forwarded leaves expiry
- leave_type_records = frappe.db.get_values("Leave Type", filters={
- 'expire_carry_forwarded_leaves_after_days': (">", 0)
- }, fieldname=['name'])
+ leave_type_records = frappe.db.get_values(
+ "Leave Type", filters={"expire_carry_forwarded_leaves_after_days": (">", 0)}, fieldname=["name"]
+ )
- leave_type = [record[0] for record in leave_type_records] or ['']
+ leave_type = [record[0] for record in leave_type_records] or [""]
# fetch non expired leave ledger entry of transaction_type allocation
- expire_allocation = frappe.db.sql("""
+ expire_allocation = frappe.db.sql(
+ """
SELECT
leaves, to_date, employee, leave_type,
is_carry_forward, transaction_name as name, transaction_type
@@ -123,32 +146,41 @@ def process_expired_allocation():
OR (is_carry_forward = 0 AND leave_type not in %s)
)))
AND transaction_type = 'Leave Allocation'
- AND to_date < %s""", (leave_type, today()), as_dict=1)
+ AND to_date < %s""",
+ (leave_type, today()),
+ as_dict=1,
+ )
if expire_allocation:
create_expiry_ledger_entry(expire_allocation)
+
def create_expiry_ledger_entry(allocations):
- ''' Create ledger entry for expired allocation '''
+ """Create ledger entry for expired allocation"""
for allocation in allocations:
if allocation.is_carry_forward:
expire_carried_forward_allocation(allocation)
else:
expire_allocation(allocation)
+
def get_remaining_leaves(allocation):
- ''' Returns remaining leaves from the given allocation '''
- return frappe.db.get_value("Leave Ledger Entry",
+ """Returns remaining leaves from the given allocation"""
+ return frappe.db.get_value(
+ "Leave Ledger Entry",
filters={
- 'employee': allocation.employee,
- 'leave_type': allocation.leave_type,
- 'to_date': ('<=', allocation.to_date),
- 'docstatus': 1
- }, fieldname=['SUM(leaves)'])
+ "employee": allocation.employee,
+ "leave_type": allocation.leave_type,
+ "to_date": ("<=", allocation.to_date),
+ "docstatus": 1,
+ },
+ fieldname=["SUM(leaves)"],
+ )
+
@frappe.whitelist()
def expire_allocation(allocation, expiry_date=None):
- ''' expires non-carry forwarded allocation '''
+ """expires non-carry forwarded allocation"""
leaves = get_remaining_leaves(allocation)
expiry_date = expiry_date if expiry_date else allocation.to_date
@@ -157,21 +189,28 @@ def expire_allocation(allocation, expiry_date=None):
args = dict(
leaves=flt(leaves) * -1,
transaction_name=allocation.name,
- transaction_type='Leave Allocation',
+ transaction_type="Leave Allocation",
from_date=expiry_date,
to_date=expiry_date,
is_carry_forward=0,
- is_expired=1
+ is_expired=1,
)
create_leave_ledger_entry(allocation, args)
frappe.db.set_value("Leave Allocation", allocation.name, "expired", 1)
+
def expire_carried_forward_allocation(allocation):
- ''' Expires remaining leaves in the on carried forward allocation '''
+ """Expires remaining leaves in the on carried forward allocation"""
from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period
- leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type,
- allocation.from_date, allocation.to_date, do_not_skip_expired_leaves=True)
+
+ leaves_taken = get_leaves_for_period(
+ allocation.employee,
+ allocation.leave_type,
+ allocation.from_date,
+ allocation.to_date,
+ skip_expired_leaves=False,
+ )
leaves = flt(allocation.leaves) + flt(leaves_taken)
# allow expired leaves entry to be created
@@ -183,6 +222,6 @@ def expire_carried_forward_allocation(allocation):
is_carry_forward=allocation.is_carry_forward,
is_expired=1,
from_date=allocation.to_date,
- to_date=allocation.to_date
+ to_date=allocation.to_date,
)
create_leave_ledger_entry(allocation, args)
diff --git a/erpnext/hr/doctype/leave_period/leave_period.py b/erpnext/hr/doctype/leave_period/leave_period.py
index b1cb6887d99..6e62bb58765 100644
--- a/erpnext/hr/doctype/leave_period/leave_period.py
+++ b/erpnext/hr/doctype/leave_period/leave_period.py
@@ -11,7 +11,6 @@ from erpnext.hr.utils import validate_overlap
class LeavePeriod(Document):
-
def validate(self):
self.validate_dates()
validate_overlap(self, self.from_date, self.to_date, self.company)
diff --git a/erpnext/hr/doctype/leave_period/leave_period_dashboard.py b/erpnext/hr/doctype/leave_period/leave_period_dashboard.py
index fbe56e2b700..854f988f35b 100644
--- a/erpnext/hr/doctype/leave_period/leave_period_dashboard.py
+++ b/erpnext/hr/doctype/leave_period/leave_period_dashboard.py
@@ -1,14 +1,8 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'leave_period',
- 'transactions': [
- {
- 'label': _('Transactions'),
- 'items': ['Leave Allocation']
- }
- ]
+ "fieldname": "leave_period",
+ "transactions": [{"label": _("Transactions"), "items": ["Leave Allocation"]}],
}
diff --git a/erpnext/hr/doctype/leave_period/test_leave_period.py b/erpnext/hr/doctype/leave_period/test_leave_period.py
index 10936dddc98..09235741b6f 100644
--- a/erpnext/hr/doctype/leave_period/test_leave_period.py
+++ b/erpnext/hr/doctype/leave_period/test_leave_period.py
@@ -9,23 +9,32 @@ import erpnext
test_dependencies = ["Employee", "Leave Type", "Leave Policy"]
+
class TestLeavePeriod(unittest.TestCase):
pass
+
def create_leave_period(from_date, to_date, company=None):
- leave_period = frappe.db.get_value('Leave Period',
- dict(company=company or erpnext.get_default_company(),
+ leave_period = frappe.db.get_value(
+ "Leave Period",
+ dict(
+ company=company or erpnext.get_default_company(),
from_date=from_date,
to_date=to_date,
- is_active=1), 'name')
+ is_active=1,
+ ),
+ "name",
+ )
if leave_period:
return frappe.get_doc("Leave Period", leave_period)
- leave_period = frappe.get_doc({
- "doctype": "Leave Period",
- "company": company or erpnext.get_default_company(),
- "from_date": from_date,
- "to_date": to_date,
- "is_active": 1
- }).insert()
+ leave_period = frappe.get_doc(
+ {
+ "doctype": "Leave Period",
+ "company": company or erpnext.get_default_company(),
+ "from_date": from_date,
+ "to_date": to_date,
+ "is_active": 1,
+ }
+ ).insert()
return leave_period
diff --git a/erpnext/hr/doctype/leave_policy/leave_policy.py b/erpnext/hr/doctype/leave_policy/leave_policy.py
index 80450d5d6e0..33c949354cc 100644
--- a/erpnext/hr/doctype/leave_policy/leave_policy.py
+++ b/erpnext/hr/doctype/leave_policy/leave_policy.py
@@ -11,6 +11,12 @@ class LeavePolicy(Document):
def validate(self):
if self.leave_policy_details:
for lp_detail in self.leave_policy_details:
- max_leaves_allowed = frappe.db.get_value("Leave Type", lp_detail.leave_type, "max_leaves_allowed")
+ max_leaves_allowed = frappe.db.get_value(
+ "Leave Type", lp_detail.leave_type, "max_leaves_allowed"
+ )
if max_leaves_allowed > 0 and lp_detail.annual_allocation > max_leaves_allowed:
- frappe.throw(_("Maximum leave allowed in the leave type {0} is {1}").format(lp_detail.leave_type, max_leaves_allowed))
+ frappe.throw(
+ _("Maximum leave allowed in the leave type {0} is {1}").format(
+ lp_detail.leave_type, max_leaves_allowed
+ )
+ )
diff --git a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
index 8311fd2f93e..57ea93ee466 100644
--- a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
+++ b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
@@ -1,14 +1,10 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'leave_policy',
- 'transactions': [
- {
- 'label': _('Leaves'),
- 'items': ['Leave Policy Assignment', 'Leave Allocation']
- },
- ]
+ "fieldname": "leave_policy",
+ "transactions": [
+ {"label": _("Leaves"), "items": ["Leave Policy Assignment", "Leave Allocation"]},
+ ],
}
diff --git a/erpnext/hr/doctype/leave_policy/test_leave_policy.py b/erpnext/hr/doctype/leave_policy/test_leave_policy.py
index 3dbbef857ec..0e1ccad6019 100644
--- a/erpnext/hr/doctype/leave_policy/test_leave_policy.py
+++ b/erpnext/hr/doctype/leave_policy/test_leave_policy.py
@@ -15,17 +15,24 @@ class TestLeavePolicy(unittest.TestCase):
leave_type.max_leaves_allowed = 2
leave_type.save()
- leave_policy = create_leave_policy(leave_type=leave_type.name, annual_allocation=leave_type.max_leaves_allowed + 1)
+ leave_policy = create_leave_policy(
+ leave_type=leave_type.name, annual_allocation=leave_type.max_leaves_allowed + 1
+ )
self.assertRaises(frappe.ValidationError, leave_policy.insert)
+
def create_leave_policy(**args):
- ''' Returns an object of leave policy '''
+ """Returns an object of leave policy"""
args = frappe._dict(args)
- return frappe.get_doc({
- "doctype": "Leave Policy",
- "leave_policy_details": [{
- "leave_type": args.leave_type or "_Test Leave Type",
- "annual_allocation": args.annual_allocation or 10
- }]
- })
+ return frappe.get_doc(
+ {
+ "doctype": "Leave Policy",
+ "leave_policy_details": [
+ {
+ "leave_type": args.leave_type or "_Test Leave Type",
+ "annual_allocation": args.annual_allocation or 10,
+ }
+ ],
+ }
+ )
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
index 6e6943f71aa..bb19ffa9d1e 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
@@ -8,36 +8,63 @@ from math import ceil
import frappe
from frappe import _, bold
from frappe.model.document import Document
-from frappe.utils import date_diff, flt, formatdate, get_last_day, getdate
+from frappe.utils import date_diff, flt, formatdate, get_last_day, get_link_to_form, getdate
from six import string_types
class LeavePolicyAssignment(Document):
def validate(self):
- self.validate_policy_assignment_overlap()
self.set_dates()
+ self.validate_policy_assignment_overlap()
+ self.warn_about_carry_forwarding()
def on_submit(self):
self.grant_leave_alloc_for_employee()
def set_dates(self):
if self.assignment_based_on == "Leave Period":
- self.effective_from, self.effective_to = frappe.db.get_value("Leave Period", self.leave_period, ["from_date", "to_date"])
+ self.effective_from, self.effective_to = frappe.db.get_value(
+ "Leave Period", self.leave_period, ["from_date", "to_date"]
+ )
elif self.assignment_based_on == "Joining Date":
self.effective_from = frappe.db.get_value("Employee", self.employee, "date_of_joining")
def validate_policy_assignment_overlap(self):
- leave_policy_assignments = frappe.get_all("Leave Policy Assignment", filters = {
- "employee": self.employee,
- "name": ("!=", self.name),
- "docstatus": 1,
- "effective_to": (">=", self.effective_from),
- "effective_from": ("<=", self.effective_to)
- })
+ leave_policy_assignments = frappe.get_all(
+ "Leave Policy Assignment",
+ filters={
+ "employee": self.employee,
+ "name": ("!=", self.name),
+ "docstatus": 1,
+ "effective_to": (">=", self.effective_from),
+ "effective_from": ("<=", self.effective_to),
+ },
+ )
if len(leave_policy_assignments):
- frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}")
- .format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to))))
+ frappe.throw(
+ _("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}").format(
+ bold(self.leave_policy),
+ bold(self.employee),
+ bold(formatdate(self.effective_from)),
+ bold(formatdate(self.effective_to)),
+ )
+ )
+
+ def warn_about_carry_forwarding(self):
+ if not self.carry_forward:
+ return
+
+ leave_types = get_leave_type_details()
+ leave_policy = frappe.get_doc("Leave Policy", self.leave_policy)
+
+ for policy in leave_policy.leave_policy_details:
+ leave_type = leave_types.get(policy.leave_type)
+ if not leave_type.is_carry_forward:
+ msg = _(
+ "Leaves for the Leave Type {0} won't be carry-forwarded since carry-forwarding is disabled."
+ ).format(frappe.bold(get_link_to_form("Leave Type", leave_type.name)))
+ frappe.msgprint(msg, indicator="orange", alert=True)
@frappe.whitelist()
def grant_leave_alloc_for_employee(self):
@@ -53,41 +80,54 @@ class LeavePolicyAssignment(Document):
for leave_policy_detail in leave_policy.leave_policy_details:
if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp:
leave_allocation, new_leaves_allocated = self.create_leave_allocation(
- leave_policy_detail.leave_type, leave_policy_detail.annual_allocation,
- leave_type_details, date_of_joining
+ leave_policy_detail.leave_type,
+ leave_policy_detail.annual_allocation,
+ leave_type_details,
+ date_of_joining,
)
- leave_allocations[leave_policy_detail.leave_type] = {"name": leave_allocation, "leaves": new_leaves_allocated}
+ leave_allocations[leave_policy_detail.leave_type] = {
+ "name": leave_allocation,
+ "leaves": new_leaves_allocated,
+ }
self.db_set("leaves_allocated", 1)
return leave_allocations
- def create_leave_allocation(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
+ def create_leave_allocation(
+ self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining
+ ):
# Creates leave allocation for the given employee in the provided leave period
carry_forward = self.carry_forward
if self.carry_forward and not leave_type_details.get(leave_type).is_carry_forward:
carry_forward = 0
- new_leaves_allocated = self.get_new_leaves(leave_type, new_leaves_allocated,
- leave_type_details, date_of_joining)
+ new_leaves_allocated = self.get_new_leaves(
+ leave_type, new_leaves_allocated, leave_type_details, date_of_joining
+ )
- allocation = frappe.get_doc(dict(
- doctype="Leave Allocation",
- employee=self.employee,
- leave_type=leave_type,
- from_date=self.effective_from,
- to_date=self.effective_to,
- new_leaves_allocated=new_leaves_allocated,
- leave_period=self.leave_period if self.assignment_based_on == "Leave Policy" else '',
- leave_policy_assignment = self.name,
- leave_policy = self.leave_policy,
- carry_forward=carry_forward
- ))
- allocation.save(ignore_permissions = True)
+ allocation = frappe.get_doc(
+ dict(
+ doctype="Leave Allocation",
+ employee=self.employee,
+ leave_type=leave_type,
+ from_date=self.effective_from,
+ to_date=self.effective_to,
+ new_leaves_allocated=new_leaves_allocated,
+ leave_period=self.leave_period if self.assignment_based_on == "Leave Policy" else "",
+ leave_policy_assignment=self.name,
+ leave_policy=self.leave_policy,
+ carry_forward=carry_forward,
+ )
+ )
+ allocation.save(ignore_permissions=True)
allocation.submit()
return allocation.name, new_leaves_allocated
def get_new_leaves(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
from frappe.model.meta import get_field_precision
- precision = get_field_precision(frappe.get_meta("Leave Allocation").get_field("new_leaves_allocated"))
+
+ precision = get_field_precision(
+ frappe.get_meta("Leave Allocation").get_field("new_leaves_allocated")
+ )
# Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
if leave_type_details.get(leave_type).is_compensatory == 1:
@@ -98,16 +138,22 @@ class LeavePolicyAssignment(Document):
new_leaves_allocated = 0
else:
# get leaves for past months if assignment is based on Leave Period / Joining Date
- new_leaves_allocated = self.get_leaves_for_passed_months(leave_type, new_leaves_allocated, leave_type_details, date_of_joining)
+ new_leaves_allocated = self.get_leaves_for_passed_months(
+ leave_type, new_leaves_allocated, leave_type_details, date_of_joining
+ )
# Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
elif getdate(date_of_joining) > getdate(self.effective_from):
- remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1))
+ remaining_period = (date_diff(self.effective_to, date_of_joining) + 1) / (
+ date_diff(self.effective_to, self.effective_from) + 1
+ )
new_leaves_allocated = ceil(new_leaves_allocated * remaining_period)
return flt(new_leaves_allocated, precision)
- def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
+ def get_leaves_for_passed_months(
+ self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining
+ ):
from erpnext.hr.utils import get_monthly_earned_leave
current_date = frappe.flags.current_date or getdate()
@@ -130,8 +176,11 @@ class LeavePolicyAssignment(Document):
months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj)
if months_passed > 0:
- monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated,
- leave_type_details.get(leave_type).earned_leave_frequency, leave_type_details.get(leave_type).rounding)
+ monthly_earned_leave = get_monthly_earned_leave(
+ new_leaves_allocated,
+ leave_type_details.get(leave_type).earned_leave_frequency,
+ leave_type_details.get(leave_type).rounding,
+ )
new_leaves_allocated = monthly_earned_leave * months_passed
else:
new_leaves_allocated = 0
@@ -160,7 +209,7 @@ def add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj
def create_assignment_for_multiple_employees(employees, data):
if isinstance(employees, string_types):
- employees= json.loads(employees)
+ employees = json.loads(employees)
if isinstance(data, string_types):
data = frappe._dict(json.loads(data))
@@ -187,11 +236,23 @@ def create_assignment_for_multiple_employees(employees, data):
return docs_name
+
def get_leave_type_details():
leave_type_details = frappe._dict()
- leave_types = frappe.get_all("Leave Type",
- fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "based_on_date_of_joining",
- "is_carry_forward", "expire_carry_forwarded_leaves_after_days", "earned_leave_frequency", "rounding"])
+ leave_types = frappe.get_all(
+ "Leave Type",
+ fields=[
+ "name",
+ "is_lwp",
+ "is_earned_leave",
+ "is_compensatory",
+ "based_on_date_of_joining",
+ "is_carry_forward",
+ "expire_carry_forwarded_leaves_after_days",
+ "earned_leave_frequency",
+ "rounding",
+ ],
+ )
for d in leave_types:
leave_type_details.setdefault(d.name, d)
return leave_type_details
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py
index ec6592cb72a..13b39c7ee6e 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py
@@ -1,14 +1,10 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'leave_policy_assignment',
- 'transactions': [
- {
- 'label': _('Leaves'),
- 'items': ['Leave Allocation']
- },
- ]
+ "fieldname": "leave_policy_assignment",
+ "transactions": [
+ {"label": _("Leaves"), "items": ["Leave Allocation"]},
+ ],
}
diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
index 8d7b27ee5af..20249b38ef7 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
@@ -4,7 +4,7 @@
import unittest
import frappe
-from frappe.utils import add_months, get_first_day, get_last_day, getdate
+from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate
from erpnext.hr.doctype.leave_application.test_leave_application import (
get_employee,
@@ -17,9 +17,16 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
test_dependencies = ["Employee"]
+
class TestLeavePolicyAssignment(unittest.TestCase):
def setUp(self):
- for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
+ for doctype in [
+ "Leave Period",
+ "Leave Application",
+ "Leave Allocation",
+ "Leave Policy Assignment",
+ "Leave Ledger Entry",
+ ]:
frappe.db.delete(doctype)
employee = get_employee()
@@ -35,16 +42,25 @@ class TestLeavePolicyAssignment(unittest.TestCase):
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
- "leave_period": leave_period.name
+ "leave_period": leave_period.name,
}
- leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
- self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 1)
+ leave_policy_assignments = create_assignment_for_multiple_employees(
+ [self.employee.name], frappe._dict(data)
+ )
+ self.assertEqual(
+ frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"),
+ 1,
+ )
- leave_allocation = frappe.get_list("Leave Allocation", filters={
- "employee": self.employee.name,
- "leave_policy":leave_policy.name,
- "leave_policy_assignment": leave_policy_assignments[0],
- "docstatus": 1})[0]
+ leave_allocation = frappe.get_list(
+ "Leave Allocation",
+ filters={
+ "employee": self.employee.name,
+ "leave_policy": leave_policy.name,
+ "leave_policy_assignment": leave_policy_assignments[0],
+ "docstatus": 1,
+ },
+ )[0]
leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation)
self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10)
@@ -63,64 +79,93 @@ class TestLeavePolicyAssignment(unittest.TestCase):
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
- "leave_period": leave_period.name
+ "leave_period": leave_period.name,
}
- leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
+ leave_policy_assignments = create_assignment_for_multiple_employees(
+ [self.employee.name], frappe._dict(data)
+ )
# every leave is allocated no more leave can be granted now
- self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 1)
- leave_allocation = frappe.get_list("Leave Allocation", filters={
- "employee": self.employee.name,
- "leave_policy":leave_policy.name,
- "leave_policy_assignment": leave_policy_assignments[0],
- "docstatus": 1})[0]
+ self.assertEqual(
+ frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"),
+ 1,
+ )
+ leave_allocation = frappe.get_list(
+ "Leave Allocation",
+ filters={
+ "employee": self.employee.name,
+ "leave_policy": leave_policy.name,
+ "leave_policy_assignment": leave_policy_assignments[0],
+ "docstatus": 1,
+ },
+ )[0]
leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation)
leave_alloc_doc.cancel()
leave_alloc_doc.delete()
- self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 0)
+ self.assertEqual(
+ frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"),
+ 0,
+ )
def test_earned_leave_allocation(self):
leave_period = create_leave_period("Test Earned Leave Period")
leave_type = create_earned_leave_type("Test Earned Leave")
- leave_policy = frappe.get_doc({
- "doctype": "Leave Policy",
- "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}]
- }).submit()
+ leave_policy = frappe.get_doc(
+ {
+ "doctype": "Leave Policy",
+ "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}],
+ }
+ ).submit()
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
- "leave_period": leave_period.name
+ "leave_period": leave_period.name,
}
- leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
+ # second last day of the month
# leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency
- leaves_allocated = frappe.db.get_value("Leave Allocation", {
- "leave_policy_assignment": leave_policy_assignments[0]
- }, "total_leaves_allocated")
+ frappe.flags.current_date = add_days(get_last_day(getdate()), -1)
+ leave_policy_assignments = create_assignment_for_multiple_employees(
+ [self.employee.name], frappe._dict(data)
+ )
+
+ leaves_allocated = frappe.db.get_value(
+ "Leave Allocation",
+ {"leave_policy_assignment": leave_policy_assignments[0]},
+ "total_leaves_allocated",
+ )
self.assertEqual(leaves_allocated, 0)
def test_earned_leave_alloc_for_passed_months_based_on_leave_period(self):
- leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -1)))
+ leave_period, leave_policy = setup_leave_period_and_policy(
+ get_first_day(add_months(getdate(), -1))
+ )
# Case 1: assignment created one month after the leave period, should allocate 1 leave
frappe.flags.current_date = get_first_day(getdate())
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
- "leave_period": leave_period.name
+ "leave_period": leave_period.name,
}
- leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
+ leave_policy_assignments = create_assignment_for_multiple_employees(
+ [self.employee.name], frappe._dict(data)
+ )
- leaves_allocated = frappe.db.get_value("Leave Allocation", {
- "leave_policy_assignment": leave_policy_assignments[0]
- }, "total_leaves_allocated")
+ leaves_allocated = frappe.db.get_value(
+ "Leave Allocation",
+ {"leave_policy_assignment": leave_policy_assignments[0]},
+ "total_leaves_allocated",
+ )
self.assertEqual(leaves_allocated, 1)
def test_earned_leave_alloc_for_passed_months_on_month_end_based_on_leave_period(self):
- leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)))
+ leave_period, leave_policy = setup_leave_period_and_policy(
+ get_first_day(add_months(getdate(), -2))
+ )
# Case 2: assignment created on the last day of the leave period's latter month
# should allocate 1 leave for current month even though the month has not ended
# since the daily job might have already executed
@@ -129,32 +174,48 @@ class TestLeavePolicyAssignment(unittest.TestCase):
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
- "leave_period": leave_period.name
+ "leave_period": leave_period.name,
}
- leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
+ leave_policy_assignments = create_assignment_for_multiple_employees(
+ [self.employee.name], frappe._dict(data)
+ )
- leaves_allocated = frappe.db.get_value("Leave Allocation", {
- "leave_policy_assignment": leave_policy_assignments[0]
- }, "total_leaves_allocated")
+ leaves_allocated = frappe.db.get_value(
+ "Leave Allocation",
+ {"leave_policy_assignment": leave_policy_assignments[0]},
+ "total_leaves_allocated",
+ )
self.assertEqual(leaves_allocated, 3)
# if the daily job is not completed yet, there is another check present
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import allocate_earned_leaves
+
allocate_earned_leaves()
- leaves_allocated = frappe.db.get_value("Leave Allocation", {
- "leave_policy_assignment": leave_policy_assignments[0]
- }, "total_leaves_allocated")
+ leaves_allocated = frappe.db.get_value(
+ "Leave Allocation",
+ {"leave_policy_assignment": leave_policy_assignments[0]},
+ "total_leaves_allocated",
+ )
self.assertEqual(leaves_allocated, 3)
def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self):
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
- leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)))
+ leave_period, leave_policy = setup_leave_period_and_policy(
+ get_first_day(add_months(getdate(), -2))
+ )
# initial leave allocation = 5
- leave_allocation = create_leave_allocation(employee=self.employee.name, employee_name=self.employee.employee_name, leave_type="Test Earned Leave",
- from_date=add_months(getdate(), -12), to_date=add_months(getdate(), -3), new_leaves_allocated=5, carry_forward=0)
+ leave_allocation = create_leave_allocation(
+ employee=self.employee.name,
+ employee_name=self.employee.employee_name,
+ leave_type="Test Earned Leave",
+ from_date=add_months(getdate(), -12),
+ to_date=add_months(getdate(), -3),
+ new_leaves_allocated=5,
+ carry_forward=0,
+ )
leave_allocation.submit()
# Case 3: assignment created on the last day of the leave period's latter month with carry forwarding
@@ -163,14 +224,19 @@ class TestLeavePolicyAssignment(unittest.TestCase):
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": leave_period.name,
- "carry_forward": 1
+ "carry_forward": 1,
}
# carry forwarded leaves = 5, 3 leaves allocated for passed months
- leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
+ leave_policy_assignments = create_assignment_for_multiple_employees(
+ [self.employee.name], frappe._dict(data)
+ )
- details = frappe.db.get_value("Leave Allocation", {
- "leave_policy_assignment": leave_policy_assignments[0]
- }, ["total_leaves_allocated", "new_leaves_allocated", "unused_leaves", "name"], as_dict=True)
+ details = frappe.db.get_value(
+ "Leave Allocation",
+ {"leave_policy_assignment": leave_policy_assignments[0]},
+ ["total_leaves_allocated", "new_leaves_allocated", "unused_leaves", "name"],
+ as_dict=True,
+ )
self.assertEqual(details.new_leaves_allocated, 2)
self.assertEqual(details.unused_leaves, 5)
self.assertEqual(details.total_leaves_allocated, 7)
@@ -178,20 +244,27 @@ class TestLeavePolicyAssignment(unittest.TestCase):
# if the daily job is not completed yet, there is another check present
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import is_earned_leave_already_allocated
+
frappe.flags.current_date = get_last_day(getdate())
allocation = frappe.get_doc("Leave Allocation", details.name)
# 1 leave is still pending to be allocated, irrespective of carry forwarded leaves
- self.assertFalse(is_earned_leave_already_allocated(allocation, leave_policy.leave_policy_details[0].annual_allocation))
+ self.assertFalse(
+ is_earned_leave_already_allocated(
+ allocation, leave_policy.leave_policy_details[0].annual_allocation
+ )
+ )
def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self):
# tests leave alloc for earned leaves for assignment based on joining date in policy assignment
leave_type = create_earned_leave_type("Test Earned Leave")
- leave_policy = frappe.get_doc({
- "doctype": "Leave Policy",
- "title": "Test Leave Policy",
- "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
- }).submit()
+ leave_policy = frappe.get_doc(
+ {
+ "doctype": "Leave Policy",
+ "title": "Test Leave Policy",
+ "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}],
+ }
+ ).submit()
# joining date set to 2 months back
self.employee.date_of_joining = get_first_day(add_months(getdate(), -2))
@@ -199,29 +272,39 @@ class TestLeavePolicyAssignment(unittest.TestCase):
# assignment created on the last day of the current month
frappe.flags.current_date = get_last_day(getdate())
- data = {
- "assignment_based_on": "Joining Date",
- "leave_policy": leave_policy.name
- }
- leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
- leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]},
- "total_leaves_allocated")
- effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from")
+ data = {"assignment_based_on": "Joining Date", "leave_policy": leave_policy.name}
+ leave_policy_assignments = create_assignment_for_multiple_employees(
+ [self.employee.name], frappe._dict(data)
+ )
+ leaves_allocated = frappe.db.get_value(
+ "Leave Allocation",
+ {"leave_policy_assignment": leave_policy_assignments[0]},
+ "total_leaves_allocated",
+ )
+ effective_from = frappe.db.get_value(
+ "Leave Policy Assignment", leave_policy_assignments[0], "effective_from"
+ )
self.assertEqual(effective_from, self.employee.date_of_joining)
self.assertEqual(leaves_allocated, 3)
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import allocate_earned_leaves
+
frappe.flags.current_date = get_last_day(getdate())
allocate_earned_leaves()
- leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]},
- "total_leaves_allocated")
+ leaves_allocated = frappe.db.get_value(
+ "Leave Allocation",
+ {"leave_policy_assignment": leave_policy_assignments[0]},
+ "total_leaves_allocated",
+ )
self.assertEqual(leaves_allocated, 3)
def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self):
# tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type
- leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)), based_on_doj=True)
+ leave_period, leave_policy = setup_leave_period_and_policy(
+ get_first_day(add_months(getdate(), -2)), based_on_doj=True
+ )
# joining date set to 2 months back
self.employee.date_of_joining = get_first_day(add_months(getdate(), -2))
@@ -233,34 +316,43 @@ class TestLeavePolicyAssignment(unittest.TestCase):
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
- "leave_period": leave_period.name
+ "leave_period": leave_period.name,
}
- leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
+ leave_policy_assignments = create_assignment_for_multiple_employees(
+ [self.employee.name], frappe._dict(data)
+ )
- leaves_allocated = frappe.db.get_value("Leave Allocation", {
- "leave_policy_assignment": leave_policy_assignments[0]
- }, "total_leaves_allocated")
+ leaves_allocated = frappe.db.get_value(
+ "Leave Allocation",
+ {"leave_policy_assignment": leave_policy_assignments[0]},
+ "total_leaves_allocated",
+ )
self.assertEqual(leaves_allocated, 3)
# if the daily job is not completed yet, there is another check present
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import allocate_earned_leaves
+
frappe.flags.current_date = get_first_day(getdate())
allocate_earned_leaves()
- leaves_allocated = frappe.db.get_value("Leave Allocation", {
- "leave_policy_assignment": leave_policy_assignments[0]
- }, "total_leaves_allocated")
+ leaves_allocated = frappe.db.get_value(
+ "Leave Allocation",
+ {"leave_policy_assignment": leave_policy_assignments[0]},
+ "total_leaves_allocated",
+ )
self.assertEqual(leaves_allocated, 3)
def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self):
# tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type
leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True)
- leave_policy = frappe.get_doc({
- "doctype": "Leave Policy",
- "title": "Test Leave Policy",
- "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
- }).submit()
+ leave_policy = frappe.get_doc(
+ {
+ "doctype": "Leave Policy",
+ "title": "Test Leave Policy",
+ "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}],
+ }
+ ).submit()
# joining date set to 2 months back
# leave should be allocated for current month too since this day is same as the joining day
@@ -269,24 +361,32 @@ class TestLeavePolicyAssignment(unittest.TestCase):
# assignment created on the first day of the current month
frappe.flags.current_date = get_first_day(getdate())
- data = {
- "assignment_based_on": "Joining Date",
- "leave_policy": leave_policy.name
- }
- leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data))
- leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]},
- "total_leaves_allocated")
- effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from")
+ data = {"assignment_based_on": "Joining Date", "leave_policy": leave_policy.name}
+ leave_policy_assignments = create_assignment_for_multiple_employees(
+ [self.employee.name], frappe._dict(data)
+ )
+ leaves_allocated = frappe.db.get_value(
+ "Leave Allocation",
+ {"leave_policy_assignment": leave_policy_assignments[0]},
+ "total_leaves_allocated",
+ )
+ effective_from = frappe.db.get_value(
+ "Leave Policy Assignment", leave_policy_assignments[0], "effective_from"
+ )
self.assertEqual(effective_from, self.employee.date_of_joining)
self.assertEqual(leaves_allocated, 3)
# to ensure leave is not already allocated to avoid duplication
from erpnext.hr.utils import allocate_earned_leaves
+
frappe.flags.current_date = get_first_day(getdate())
allocate_earned_leaves()
- leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]},
- "total_leaves_allocated")
+ leaves_allocated = frappe.db.get_value(
+ "Leave Allocation",
+ {"leave_policy_assignment": leave_policy_assignments[0]},
+ "total_leaves_allocated",
+ )
self.assertEqual(leaves_allocated, 3)
def tearDown(self):
@@ -298,15 +398,17 @@ class TestLeavePolicyAssignment(unittest.TestCase):
def create_earned_leave_type(leave_type, based_on_doj=False):
frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
- return frappe.get_doc(dict(
- leave_type_name=leave_type,
- doctype="Leave Type",
- is_earned_leave=1,
- earned_leave_frequency="Monthly",
- rounding=0.5,
- is_carry_forward=1,
- based_on_date_of_joining=based_on_doj
- )).insert()
+ return frappe.get_doc(
+ dict(
+ leave_type_name=leave_type,
+ doctype="Leave Type",
+ is_earned_leave=1,
+ earned_leave_frequency="Monthly",
+ rounding=0.5,
+ is_carry_forward=1,
+ based_on_date_of_joining=based_on_doj,
+ )
+ ).insert()
def create_leave_period(name, start_date=None):
@@ -314,24 +416,27 @@ def create_leave_period(name, start_date=None):
if not start_date:
start_date = get_first_day(getdate())
- return frappe.get_doc(dict(
- name=name,
- doctype="Leave Period",
- from_date=start_date,
- to_date=add_months(start_date, 12),
- company="_Test Company",
- is_active=1
- )).insert()
+ return frappe.get_doc(
+ dict(
+ name=name,
+ doctype="Leave Period",
+ from_date=start_date,
+ to_date=add_months(start_date, 12),
+ company="_Test Company",
+ is_active=1,
+ )
+ ).insert()
def setup_leave_period_and_policy(start_date, based_on_doj=False):
leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj)
- leave_period = create_leave_period("Test Earned Leave Period",
- start_date=start_date)
- leave_policy = frappe.get_doc({
- "doctype": "Leave Policy",
- "title": "Test Leave Policy",
- "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
- }).insert()
+ leave_period = create_leave_period("Test Earned Leave Period", start_date=start_date)
+ leave_policy = frappe.get_doc(
+ {
+ "doctype": "Leave Policy",
+ "title": "Test Leave Policy",
+ "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}],
+ }
+ ).insert()
- return leave_period, leave_policy
\ No newline at end of file
+ return leave_period, leave_policy
diff --git a/erpnext/hr/doctype/leave_type/leave_type.py b/erpnext/hr/doctype/leave_type/leave_type.py
index 4b59c2c09b4..82b9bd65753 100644
--- a/erpnext/hr/doctype/leave_type/leave_type.py
+++ b/erpnext/hr/doctype/leave_type/leave_type.py
@@ -11,17 +11,23 @@ from frappe.utils import today
class LeaveType(Document):
def validate(self):
if self.is_lwp:
- leave_allocation = frappe.get_all("Leave Allocation", filters={
- 'leave_type': self.name,
- 'from_date': ("<=", today()),
- 'to_date': (">=", today())
- }, fields=['name'])
- leave_allocation = [l['name'] for l in leave_allocation]
+ leave_allocation = frappe.get_all(
+ "Leave Allocation",
+ filters={"leave_type": self.name, "from_date": ("<=", today()), "to_date": (">=", today())},
+ fields=["name"],
+ )
+ leave_allocation = [l["name"] for l in leave_allocation]
if leave_allocation:
- frappe.throw(_('Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay').format(", ".join(leave_allocation))) #nosec
+ frappe.throw(
+ _(
+ "Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay"
+ ).format(", ".join(leave_allocation))
+ ) # nosec
if self.is_lwp and self.is_ppl:
frappe.throw(_("Leave Type can be either without pay or partial pay"))
- if self.is_ppl and (self.fraction_of_daily_salary_per_leave < 0 or self.fraction_of_daily_salary_per_leave > 1):
+ if self.is_ppl and (
+ self.fraction_of_daily_salary_per_leave < 0 or self.fraction_of_daily_salary_per_leave > 1
+ ):
frappe.throw(_("The fraction of Daily Salary per Leave should be between 0 and 1"))
diff --git a/erpnext/hr/doctype/leave_type/leave_type_dashboard.py b/erpnext/hr/doctype/leave_type/leave_type_dashboard.py
index 8dc9402d1eb..269a1ecc696 100644
--- a/erpnext/hr/doctype/leave_type/leave_type_dashboard.py
+++ b/erpnext/hr/doctype/leave_type/leave_type_dashboard.py
@@ -1,14 +1,10 @@
-
-
def get_data():
return {
- 'fieldname': 'leave_type',
- 'transactions': [
+ "fieldname": "leave_type",
+ "transactions": [
{
- 'items': ['Leave Allocation', 'Leave Application'],
+ "items": ["Leave Allocation", "Leave Application"],
},
- {
- 'items': ['Attendance', 'Leave Encashment']
- }
- ]
+ {"items": ["Attendance", "Leave Encashment"]},
+ ],
}
diff --git a/erpnext/hr/doctype/leave_type/test_leave_type.py b/erpnext/hr/doctype/leave_type/test_leave_type.py
index c1b64e99eff..69f9e125203 100644
--- a/erpnext/hr/doctype/leave_type/test_leave_type.py
+++ b/erpnext/hr/doctype/leave_type/test_leave_type.py
@@ -3,27 +3,30 @@
import frappe
-test_records = frappe.get_test_records('Leave Type')
+test_records = frappe.get_test_records("Leave Type")
+
def create_leave_type(**args):
- args = frappe._dict(args)
- if frappe.db.exists("Leave Type", args.leave_type_name):
- return frappe.get_doc("Leave Type", args.leave_type_name)
- leave_type = frappe.get_doc({
- "doctype": "Leave Type",
- "leave_type_name": args.leave_type_name or "_Test Leave Type",
- "include_holiday": args.include_holidays or 1,
- "allow_encashment": args.allow_encashment or 0,
- "is_earned_leave": args.is_earned_leave or 0,
- "is_lwp": args.is_lwp or 0,
- "is_ppl":args.is_ppl or 0,
- "is_carry_forward": args.is_carry_forward or 0,
- "expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0,
- "encashment_threshold_days": args.encashment_threshold_days or 5,
- "earning_component": "Leave Encashment"
- })
+ args = frappe._dict(args)
+ if frappe.db.exists("Leave Type", args.leave_type_name):
+ return frappe.get_doc("Leave Type", args.leave_type_name)
+ leave_type = frappe.get_doc(
+ {
+ "doctype": "Leave Type",
+ "leave_type_name": args.leave_type_name or "_Test Leave Type",
+ "include_holiday": args.include_holidays or 1,
+ "allow_encashment": args.allow_encashment or 0,
+ "is_earned_leave": args.is_earned_leave or 0,
+ "is_lwp": args.is_lwp or 0,
+ "is_ppl": args.is_ppl or 0,
+ "is_carry_forward": args.is_carry_forward or 0,
+ "expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0,
+ "encashment_threshold_days": args.encashment_threshold_days or 5,
+ "earning_component": "Leave Encashment",
+ }
+ )
- if leave_type.is_ppl:
- leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5
+ if leave_type.is_ppl:
+ leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5
- return leave_type
+ return leave_type
diff --git a/erpnext/hr/doctype/offer_term/test_offer_term.py b/erpnext/hr/doctype/offer_term/test_offer_term.py
index 2e5ed75438b..2bea7b2597f 100644
--- a/erpnext/hr/doctype/offer_term/test_offer_term.py
+++ b/erpnext/hr/doctype/offer_term/test_offer_term.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Offer Term')
+
class TestOfferTerm(unittest.TestCase):
pass
diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
index 517730281fc..868be6ef719 100644
--- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py
+++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
@@ -20,7 +20,7 @@ class ShiftAssignment(Document):
self.validate_overlapping_dates()
if self.end_date:
- self.validate_from_to_dates('start_date', 'end_date')
+ self.validate_from_to_dates("start_date", "end_date")
def validate_overlapping_dates(self):
if not self.name:
@@ -33,7 +33,7 @@ class ShiftAssignment(Document):
"""
if self.end_date:
- condition += """ or
+ condition += """ or
%(end_date)s between start_date and end_date
or
start_date between %(start_date)s and %(end_date)s
@@ -41,7 +41,8 @@ class ShiftAssignment(Document):
else:
condition += """ ) """
- assigned_shifts = frappe.db.sql("""
+ assigned_shifts = frappe.db.sql(
+ """
select name, shift_type, start_date ,end_date, docstatus, status
from `tabShift Assignment`
where
@@ -49,13 +50,18 @@ class ShiftAssignment(Document):
and name != %(name)s
and status = "Active"
{0}
- """.format(condition), {
- "employee": self.employee,
- "shift_type": self.shift_type,
- "start_date": self.start_date,
- "end_date": self.end_date,
- "name": self.name
- }, as_dict = 1)
+ """.format(
+ condition
+ ),
+ {
+ "employee": self.employee,
+ "shift_type": self.shift_type,
+ "start_date": self.start_date,
+ "end_date": self.end_date,
+ "name": self.name,
+ },
+ as_dict=1,
+ )
if len(assigned_shifts):
self.throw_overlap_error(assigned_shifts[0])
@@ -63,7 +69,9 @@ class ShiftAssignment(Document):
def throw_overlap_error(self, shift_details):
shift_details = frappe._dict(shift_details)
if shift_details.docstatus == 1 and shift_details.status == "Active":
- msg = _("Employee {0} already has Active Shift {1}: {2}").format(frappe.bold(self.employee), frappe.bold(self.shift_type), frappe.bold(shift_details.name))
+ msg = _("Employee {0} already has Active Shift {1}: {2}").format(
+ frappe.bold(self.employee), frappe.bold(self.shift_type), frappe.bold(shift_details.name)
+ )
if shift_details.start_date:
msg += _(" from {0}").format(getdate(self.start_date).strftime("%d-%m-%Y"))
title = "Ongoing Shift"
@@ -73,35 +81,41 @@ class ShiftAssignment(Document):
if msg:
frappe.throw(msg, title=title)
+
@frappe.whitelist()
def get_events(start, end, filters=None):
- events = []
+ from frappe.desk.calendar import get_event_conditions
- employee = frappe.db.get_value("Employee", {"user_id": frappe.session.user}, ["name", "company"],
- as_dict=True)
+ employee = frappe.db.get_value(
+ "Employee", {"user_id": frappe.session.user}, ["name", "company"], as_dict=True
+ )
if employee:
employee, company = employee.name, employee.company
else:
- employee=''
- company=frappe.db.get_value("Global Defaults", None, "default_company")
+ employee = ""
+ company = frappe.db.get_value("Global Defaults", None, "default_company")
- from frappe.desk.reportview import get_filters_cond
- conditions = get_filters_cond("Shift Assignment", filters, [])
- add_assignments(events, start, end, conditions=conditions)
+ conditions = get_event_conditions("Shift Assignment", filters)
+ events = add_assignments(start, end, conditions=conditions)
return events
-def add_assignments(events, start, end, conditions=None):
+
+def add_assignments(start, end, conditions=None):
+ events = []
+
query = """select name, start_date, end_date, employee_name,
employee, docstatus, shift_type
from `tabShift Assignment` where
- start_date >= %(start_date)s
- or end_date <= %(end_date)s
- or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date)
+ (
+ start_date >= %(start_date)s
+ or end_date <= %(end_date)s
+ or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date)
+ )
and docstatus = 1"""
if conditions:
query += conditions
- records = frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True)
+ records = frappe.db.sql(query, {"start_date": start, "end_date": end}, as_dict=True)
shift_timing_map = get_shift_type_timing([d.shift_type for d in records])
for d in records:
@@ -109,27 +123,33 @@ def add_assignments(events, start, end, conditions=None):
daily_event_end = d.end_date if d.end_date else getdate()
delta = timedelta(days=1)
while daily_event_start <= daily_event_end:
- start_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['start_time']
- end_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['end_time']
+ start_timing = (
+ frappe.utils.get_datetime(daily_event_start) + shift_timing_map[d.shift_type]["start_time"]
+ )
+ end_timing = (
+ frappe.utils.get_datetime(daily_event_start) + shift_timing_map[d.shift_type]["end_time"]
+ )
daily_event_start += delta
e = {
"name": d.name,
"doctype": "Shift Assignment",
"start_date": start_timing,
"end_date": end_timing,
- "title": cstr(d.employee_name) + ": "+ \
- cstr(d.shift_type),
+ "title": cstr(d.employee_name) + ": " + cstr(d.shift_type),
"docstatus": d.docstatus,
- "allDay": 0
+ "allDay": 0,
}
if e not in events:
events.append(e)
return events
+
def get_shift_type_timing(shift_types):
shift_timing_map = {}
- data = frappe.get_all("Shift Type", filters = {"name": ("IN", shift_types)}, fields = ['name', 'start_time', 'end_time'])
+ data = frappe.get_all(
+ "Shift Type", filters={"name": ("IN", shift_types)}, fields=["name", "start_time", "end_time"]
+ )
for d in data:
shift_timing_map[d.name] = d
@@ -137,7 +157,9 @@ def get_shift_type_timing(shift_types):
return shift_timing_map
-def get_employee_shift(employee, for_date=None, consider_default_shift=False, next_shift_direction=None):
+def get_employee_shift(
+ employee, for_date=None, consider_default_shift=False, next_shift_direction=None
+):
"""Returns a Shift Type for the given employee on the given date. (excluding the holidays)
:param employee: Employee for which shift is required.
@@ -147,21 +169,25 @@ def get_employee_shift(employee, for_date=None, consider_default_shift=False, ne
"""
if for_date is None:
for_date = nowdate()
- default_shift = frappe.db.get_value('Employee', employee, 'default_shift')
+ default_shift = frappe.db.get_value("Employee", employee, "default_shift")
shift_type_name = None
- shift_assignment_details = frappe.db.get_value('Shift Assignment', {'employee':employee, 'start_date':('<=', for_date), 'docstatus': '1', 'status': "Active"}, ['shift_type', 'end_date'])
+ shift_assignment_details = frappe.db.get_value(
+ "Shift Assignment",
+ {"employee": employee, "start_date": ("<=", for_date), "docstatus": "1", "status": "Active"},
+ ["shift_type", "end_date"],
+ )
if shift_assignment_details:
shift_type_name = shift_assignment_details[0]
# if end_date present means that shift is over after end_date else it is a ongoing shift.
- if shift_assignment_details[1] and for_date >= shift_assignment_details[1] :
+ if shift_assignment_details[1] and for_date >= shift_assignment_details[1]:
shift_type_name = None
if not shift_type_name and consider_default_shift:
shift_type_name = default_shift
if shift_type_name:
- holiday_list_name = frappe.db.get_value('Shift Type', shift_type_name, 'holiday_list')
+ holiday_list_name = frappe.db.get_value("Shift Type", shift_type_name, "holiday_list")
if not holiday_list_name:
holiday_list_name = get_holiday_list_for_employee(employee, False)
if holiday_list_name and is_holiday(holiday_list_name, for_date):
@@ -170,22 +196,30 @@ def get_employee_shift(employee, for_date=None, consider_default_shift=False, ne
if not shift_type_name and next_shift_direction:
MAX_DAYS = 366
if consider_default_shift and default_shift:
- direction = -1 if next_shift_direction == 'reverse' else +1
+ direction = -1 if next_shift_direction == "reverse" else +1
for i in range(MAX_DAYS):
- date = for_date+timedelta(days=direction*(i+1))
+ date = for_date + timedelta(days=direction * (i + 1))
shift_details = get_employee_shift(employee, date, consider_default_shift, None)
if shift_details:
shift_type_name = shift_details.shift_type.name
for_date = date
break
else:
- direction = '<' if next_shift_direction == 'reverse' else '>'
- sort_order = 'desc' if next_shift_direction == 'reverse' else 'asc'
- dates = frappe.db.get_all('Shift Assignment',
- ['start_date', 'end_date'],
- {'employee':employee, 'start_date':(direction, for_date), 'docstatus': '1', "status": "Active"},
+ direction = "<" if next_shift_direction == "reverse" else ">"
+ sort_order = "desc" if next_shift_direction == "reverse" else "asc"
+ dates = frappe.db.get_all(
+ "Shift Assignment",
+ ["start_date", "end_date"],
+ {
+ "employee": employee,
+ "start_date": (direction, for_date),
+ "docstatus": "1",
+ "status": "Active",
+ },
as_list=True,
- limit=MAX_DAYS, order_by="start_date "+sort_order)
+ limit=MAX_DAYS,
+ order_by="start_date " + sort_order,
+ )
if dates:
for date in dates:
@@ -201,35 +235,57 @@ def get_employee_shift(employee, for_date=None, consider_default_shift=False, ne
def get_employee_shift_timings(employee, for_timestamp=None, consider_default_shift=False):
- """Returns previous shift, current/upcoming shift, next_shift for the given timestamp and employee
- """
+ """Returns previous shift, current/upcoming shift, next_shift for the given timestamp and employee"""
if for_timestamp is None:
for_timestamp = now_datetime()
# write and verify a test case for midnight shift.
prev_shift = curr_shift = next_shift = None
- curr_shift = get_employee_shift(employee, for_timestamp.date(), consider_default_shift, 'forward')
+ curr_shift = get_employee_shift(employee, for_timestamp.date(), consider_default_shift, "forward")
if curr_shift:
- next_shift = get_employee_shift(employee, curr_shift.start_datetime.date()+timedelta(days=1), consider_default_shift, 'forward')
- prev_shift = get_employee_shift(employee, for_timestamp.date()+timedelta(days=-1), consider_default_shift, 'reverse')
+ next_shift = get_employee_shift(
+ employee,
+ curr_shift.start_datetime.date() + timedelta(days=1),
+ consider_default_shift,
+ "forward",
+ )
+ prev_shift = get_employee_shift(
+ employee, for_timestamp.date() + timedelta(days=-1), consider_default_shift, "reverse"
+ )
if curr_shift:
if prev_shift:
- curr_shift.actual_start = prev_shift.end_datetime if curr_shift.actual_start < prev_shift.end_datetime else curr_shift.actual_start
- prev_shift.actual_end = curr_shift.actual_start if prev_shift.actual_end > curr_shift.actual_start else prev_shift.actual_end
+ curr_shift.actual_start = (
+ prev_shift.end_datetime
+ if curr_shift.actual_start < prev_shift.end_datetime
+ else curr_shift.actual_start
+ )
+ prev_shift.actual_end = (
+ curr_shift.actual_start
+ if prev_shift.actual_end > curr_shift.actual_start
+ else prev_shift.actual_end
+ )
if next_shift:
- next_shift.actual_start = curr_shift.end_datetime if next_shift.actual_start < curr_shift.end_datetime else next_shift.actual_start
- curr_shift.actual_end = next_shift.actual_start if curr_shift.actual_end > next_shift.actual_start else curr_shift.actual_end
+ next_shift.actual_start = (
+ curr_shift.end_datetime
+ if next_shift.actual_start < curr_shift.end_datetime
+ else next_shift.actual_start
+ )
+ curr_shift.actual_end = (
+ next_shift.actual_start
+ if curr_shift.actual_end > next_shift.actual_start
+ else curr_shift.actual_end
+ )
return prev_shift, curr_shift, next_shift
def get_shift_details(shift_type_name, for_date=None):
"""Returns Shift Details which contain some additional information as described below.
'shift_details' contains the following keys:
- 'shift_type' - Object of DocType Shift Type,
- 'start_datetime' - Date and Time of shift start on given date,
- 'end_datetime' - Date and Time of shift end on given date,
- 'actual_start' - datetime of shift start after adding 'begin_check_in_before_shift_start_time',
- 'actual_end' - datetime of shift end after adding 'allow_check_out_after_shift_end_time'(None is returned if this is zero)
+ 'shift_type' - Object of DocType Shift Type,
+ 'start_datetime' - Date and Time of shift start on given date,
+ 'end_datetime' - Date and Time of shift end on given date,
+ 'actual_start' - datetime of shift start after adding 'begin_check_in_before_shift_start_time',
+ 'actual_end' - datetime of shift end after adding 'allow_check_out_after_shift_end_time'(None is returned if this is zero)
:param shift_type_name: shift type name for which shift_details is required.
:param for_date: Date on which shift_details are required
@@ -238,30 +294,38 @@ def get_shift_details(shift_type_name, for_date=None):
return None
if not for_date:
for_date = nowdate()
- shift_type = frappe.get_doc('Shift Type', shift_type_name)
+ shift_type = frappe.get_doc("Shift Type", shift_type_name)
start_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.start_time
- for_date = for_date + timedelta(days=1) if shift_type.start_time > shift_type.end_time else for_date
+ for_date = (
+ for_date + timedelta(days=1) if shift_type.start_time > shift_type.end_time else for_date
+ )
end_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.end_time
- actual_start = start_datetime - timedelta(minutes=shift_type.begin_check_in_before_shift_start_time)
+ actual_start = start_datetime - timedelta(
+ minutes=shift_type.begin_check_in_before_shift_start_time
+ )
actual_end = end_datetime + timedelta(minutes=shift_type.allow_check_out_after_shift_end_time)
- return frappe._dict({
- 'shift_type': shift_type,
- 'start_datetime': start_datetime,
- 'end_datetime': end_datetime,
- 'actual_start': actual_start,
- 'actual_end': actual_end
- })
+ return frappe._dict(
+ {
+ "shift_type": shift_type,
+ "start_datetime": start_datetime,
+ "end_datetime": end_datetime,
+ "actual_start": actual_start,
+ "actual_end": actual_end,
+ }
+ )
def get_actual_start_end_datetime_of_shift(employee, for_datetime, consider_default_shift=False):
"""Takes a datetime and returns the 'actual' start datetime and end datetime of the shift in which the timestamp belongs.
- Here 'actual' means - taking in to account the "begin_check_in_before_shift_start_time" and "allow_check_out_after_shift_end_time".
- None is returned if the timestamp is outside any actual shift timings.
- Shift Details is also returned(current/upcoming i.e. if timestamp not in any actual shift then details of next shift returned)
+ Here 'actual' means - taking in to account the "begin_check_in_before_shift_start_time" and "allow_check_out_after_shift_end_time".
+ None is returned if the timestamp is outside any actual shift timings.
+ Shift Details is also returned(current/upcoming i.e. if timestamp not in any actual shift then details of next shift returned)
"""
actual_shift_start = actual_shift_end = shift_details = None
- shift_timings_as_per_timestamp = get_employee_shift_timings(employee, for_datetime, consider_default_shift)
+ shift_timings_as_per_timestamp = get_employee_shift_timings(
+ employee, for_datetime, consider_default_shift
+ )
timestamp_list = []
for shift in shift_timings_as_per_timestamp:
if shift:
@@ -273,11 +337,11 @@ def get_actual_start_end_datetime_of_shift(employee, for_datetime, consider_defa
if timestamp and for_datetime <= timestamp:
timestamp_index = index
break
- if timestamp_index and timestamp_index%2 == 1:
- shift_details = shift_timings_as_per_timestamp[int((timestamp_index-1)/2)]
+ if timestamp_index and timestamp_index % 2 == 1:
+ shift_details = shift_timings_as_per_timestamp[int((timestamp_index - 1) / 2)]
actual_shift_start = shift_details.actual_start
actual_shift_end = shift_details.actual_end
elif timestamp_index:
- shift_details = shift_timings_as_per_timestamp[int(timestamp_index/2)]
+ shift_details = shift_timings_as_per_timestamp[int(timestamp_index / 2)]
return actual_shift_start, actual_shift_end, shift_details
diff --git a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py
index d4900814ffe..ea37424762b 100644
--- a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py
+++ b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py
@@ -4,23 +4,33 @@
import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, nowdate
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.shift_assignment.shift_assignment import get_events
+
test_dependencies = ["Shift Type"]
-class TestShiftAssignment(unittest.TestCase):
+class TestShiftAssignment(FrappeTestCase):
def setUp(self):
- frappe.db.sql("delete from `tabShift Assignment`")
+ frappe.db.delete("Shift Assignment")
+ if not frappe.db.exists("Shift Type", "Day Shift"):
+ frappe.get_doc(
+ {"doctype": "Shift Type", "name": "Day Shift", "start_time": "9:00:00", "end_time": "18:00:00"}
+ ).insert()
def test_make_shift_assignment(self):
- shift_assignment = frappe.get_doc({
- "doctype": "Shift Assignment",
- "shift_type": "Day Shift",
- "company": "_Test Company",
- "employee": "_T-Employee-00001",
- "start_date": nowdate()
- }).insert()
+ shift_assignment = frappe.get_doc(
+ {
+ "doctype": "Shift Assignment",
+ "shift_type": "Day Shift",
+ "company": "_Test Company",
+ "employee": "_T-Employee-00001",
+ "start_date": nowdate(),
+ }
+ ).insert()
shift_assignment.submit()
self.assertEqual(shift_assignment.docstatus, 1)
@@ -28,52 +38,92 @@ class TestShiftAssignment(unittest.TestCase):
def test_overlapping_for_ongoing_shift(self):
# shift should be Ongoing if Only start_date is present and status = Active
- shift_assignment_1 = frappe.get_doc({
- "doctype": "Shift Assignment",
- "shift_type": "Day Shift",
- "company": "_Test Company",
- "employee": "_T-Employee-00001",
- "start_date": nowdate(),
- "status": 'Active'
- }).insert()
+ shift_assignment_1 = frappe.get_doc(
+ {
+ "doctype": "Shift Assignment",
+ "shift_type": "Day Shift",
+ "company": "_Test Company",
+ "employee": "_T-Employee-00001",
+ "start_date": nowdate(),
+ "status": "Active",
+ }
+ ).insert()
shift_assignment_1.submit()
self.assertEqual(shift_assignment_1.docstatus, 1)
- shift_assignment = frappe.get_doc({
- "doctype": "Shift Assignment",
- "shift_type": "Day Shift",
- "company": "_Test Company",
- "employee": "_T-Employee-00001",
- "start_date": add_days(nowdate(), 2)
- })
+ shift_assignment = frappe.get_doc(
+ {
+ "doctype": "Shift Assignment",
+ "shift_type": "Day Shift",
+ "company": "_Test Company",
+ "employee": "_T-Employee-00001",
+ "start_date": add_days(nowdate(), 2),
+ }
+ )
self.assertRaises(frappe.ValidationError, shift_assignment.save)
def test_overlapping_for_fixed_period_shift(self):
# shift should is for Fixed period if Only start_date and end_date both are present and status = Active
- shift_assignment_1 = frappe.get_doc({
+ shift_assignment_1 = frappe.get_doc(
+ {
"doctype": "Shift Assignment",
"shift_type": "Day Shift",
"company": "_Test Company",
"employee": "_T-Employee-00001",
"start_date": nowdate(),
"end_date": add_days(nowdate(), 30),
- "status": 'Active'
- }).insert()
- shift_assignment_1.submit()
+ "status": "Active",
+ }
+ ).insert()
+ shift_assignment_1.submit()
-
- # it should not allowed within period of any shift.
- shift_assignment_3 = frappe.get_doc({
+ # it should not allowed within period of any shift.
+ shift_assignment_3 = frappe.get_doc(
+ {
"doctype": "Shift Assignment",
"shift_type": "Day Shift",
"company": "_Test Company",
"employee": "_T-Employee-00001",
- "start_date":add_days(nowdate(), 10),
+ "start_date": add_days(nowdate(), 10),
"end_date": add_days(nowdate(), 35),
- "status": 'Active'
- })
+ "status": "Active",
+ }
+ )
- self.assertRaises(frappe.ValidationError, shift_assignment_3.save)
+ self.assertRaises(frappe.ValidationError, shift_assignment_3.save)
+
+ def test_shift_assignment_calendar(self):
+ employee1 = make_employee("test_shift_assignment1@example.com", company="_Test Company")
+ employee2 = make_employee("test_shift_assignment2@example.com", company="_Test Company")
+ date = nowdate()
+
+ shift_1 = frappe.get_doc(
+ {
+ "doctype": "Shift Assignment",
+ "shift_type": "Day Shift",
+ "company": "_Test Company",
+ "employee": employee1,
+ "start_date": date,
+ "status": "Active",
+ }
+ ).submit()
+
+ frappe.get_doc(
+ {
+ "doctype": "Shift Assignment",
+ "shift_type": "Day Shift",
+ "company": "_Test Company",
+ "employee": employee2,
+ "start_date": date,
+ "status": "Active",
+ }
+ ).submit()
+
+ events = get_events(
+ start=date, end=date, filters=[["Shift Assignment", "employee", "=", employee1, False]]
+ )
+ self.assertEqual(len(events), 1)
+ self.assertEqual(events[0]["name"], shift_1.name)
diff --git a/erpnext/hr/doctype/shift_request/shift_request.py b/erpnext/hr/doctype/shift_request/shift_request.py
index d4fcf99d7d8..1e3e8ff6464 100644
--- a/erpnext/hr/doctype/shift_request/shift_request.py
+++ b/erpnext/hr/doctype/shift_request/shift_request.py
@@ -10,7 +10,9 @@ from frappe.utils import formatdate, getdate
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):
def validate(self):
@@ -39,24 +41,35 @@ class ShiftRequest(Document):
assignment_doc.insert()
assignment_doc.submit()
- frappe.msgprint(_("Shift Assignment: {0} created for Employee: {1}").format(frappe.bold(assignment_doc.name), frappe.bold(self.employee)))
+ frappe.msgprint(
+ _("Shift Assignment: {0} created for Employee: {1}").format(
+ frappe.bold(assignment_doc.name), frappe.bold(self.employee)
+ )
+ )
def on_cancel(self):
- shift_assignment_list = frappe.get_list("Shift Assignment", {'employee': self.employee, 'shift_request': self.name})
+ shift_assignment_list = frappe.get_list(
+ "Shift Assignment", {"employee": self.employee, "shift_request": self.name}
+ )
if shift_assignment_list:
for shift in shift_assignment_list:
- shift_assignment_doc = frappe.get_doc("Shift Assignment", shift['name'])
+ shift_assignment_doc = frappe.get_doc("Shift Assignment", shift["name"])
shift_assignment_doc.cancel()
def validate_default_shift(self):
default_shift = frappe.get_value("Employee", self.employee, "default_shift")
if self.shift_type == default_shift:
- frappe.throw(_("You can not request for your Default Shift: {0}").format(frappe.bold(self.shift_type)))
+ frappe.throw(
+ _("You can not request for your Default Shift: {0}").format(frappe.bold(self.shift_type))
+ )
def validate_approver(self):
department = frappe.get_value("Employee", self.employee, "department")
shift_approver = frappe.get_value("Employee", self.employee, "shift_request_approver")
- approvers = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))
+ approvers = frappe.db.sql(
+ """select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""",
+ (department),
+ )
approvers = [approver[0] for approver in approvers]
approvers.append(shift_approver)
if self.approver not in approvers:
@@ -67,10 +80,11 @@ class ShiftRequest(Document):
frappe.throw(_("To date cannot be before from date"))
def validate_shift_request_overlap_dates(self):
- if not self.name:
- self.name = "New Shift Request"
+ if not self.name:
+ self.name = "New Shift Request"
- d = frappe.db.sql("""
+ d = frappe.db.sql(
+ """
select
name, shift_type, from_date, to_date
from `tabShift Request`
@@ -79,20 +93,23 @@ class ShiftRequest(Document):
and %(from_date)s <= to_date) or
( %(to_date)s >= from_date
and %(to_date)s <= to_date ))
- and name != %(name)s""", {
- "employee": self.employee,
- "shift_type": self.shift_type,
- "from_date": self.from_date,
- "to_date": self.to_date,
- "name": self.name
- }, as_dict=1)
+ and name != %(name)s""",
+ {
+ "employee": self.employee,
+ "shift_type": self.shift_type,
+ "from_date": self.from_date,
+ "to_date": self.to_date,
+ "name": self.name,
+ },
+ as_dict=1,
+ )
- for date_overlap in d:
- if date_overlap ['name']:
- self.throw_overlap_error(date_overlap)
+ for date_overlap in d:
+ if date_overlap["name"]:
+ self.throw_overlap_error(date_overlap)
def throw_overlap_error(self, d):
- msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(self.employee,
- d['shift_type'], formatdate(d['from_date']), formatdate(d['to_date'])) \
- + """ {0}""".format(d["name"])
+ msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(
+ self.employee, d["shift_type"], formatdate(d["from_date"]), formatdate(d["to_date"])
+ ) + """ {0}""".format(d["name"])
frappe.throw(msg, OverlapError)
diff --git a/erpnext/hr/doctype/shift_request/shift_request_dashboard.py b/erpnext/hr/doctype/shift_request/shift_request_dashboard.py
index cd4519e8797..2859b8f7717 100644
--- a/erpnext/hr/doctype/shift_request/shift_request_dashboard.py
+++ b/erpnext/hr/doctype/shift_request/shift_request_dashboard.py
@@ -1,11 +1,7 @@
-
-
def get_data():
- return {
- 'fieldname': 'shift_request',
- 'transactions': [
- {
- 'items': ['Shift Assignment']
- },
- ],
- }
+ return {
+ "fieldname": "shift_request",
+ "transactions": [
+ {"items": ["Shift Assignment"]},
+ ],
+ }
diff --git a/erpnext/hr/doctype/shift_request/test_shift_request.py b/erpnext/hr/doctype/shift_request/test_shift_request.py
index 3633c9b3003..b4f51772159 100644
--- a/erpnext/hr/doctype/shift_request/test_shift_request.py
+++ b/erpnext/hr/doctype/shift_request/test_shift_request.py
@@ -10,6 +10,7 @@ from erpnext.hr.doctype.employee.test_employee import make_employee
test_dependencies = ["Shift Type"]
+
class TestShiftRequest(unittest.TestCase):
def setUp(self):
for doctype in ["Shift Request", "Shift Assignment"]:
@@ -20,9 +21,12 @@ class TestShiftRequest(unittest.TestCase):
def test_make_shift_request(self):
"Test creation/updation of Shift Assignment from Shift Request."
- department = frappe.get_value("Employee", "_T-Employee-00001", 'department')
+ department = frappe.get_value("Employee", "_T-Employee-00001", "department")
set_shift_approver(department)
- approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0]
+ approver = frappe.db.sql(
+ """select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""",
+ (department),
+ )[0][0]
shift_request = make_shift_request(approver)
@@ -31,7 +35,7 @@ class TestShiftRequest(unittest.TestCase):
"Shift Assignment",
filters={"shift_request": shift_request.name},
fieldname=["employee", "docstatus"],
- as_dict=True
+ as_dict=True,
)
self.assertEqual(shift_request.employee, shift_assignment.employee)
self.assertEqual(shift_assignment.docstatus, 1)
@@ -39,9 +43,7 @@ class TestShiftRequest(unittest.TestCase):
shift_request.cancel()
shift_assignment_docstatus = frappe.db.get_value(
- "Shift Assignment",
- filters={"shift_request": shift_request.name},
- fieldname="docstatus"
+ "Shift Assignment", filters={"shift_request": shift_request.name}, fieldname="docstatus"
)
self.assertEqual(shift_assignment_docstatus, 2)
@@ -62,7 +64,10 @@ class TestShiftRequest(unittest.TestCase):
shift_request.reload()
department = frappe.get_value("Employee", "_T-Employee-00001", "department")
set_shift_approver(department)
- department_approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0]
+ department_approver = frappe.db.sql(
+ """select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""",
+ (department),
+ )[0][0]
shift_request.approver = department_approver
shift_request.save()
self.assertTrue(shift_request.name not in frappe.share.get_shared("Shift Request", user))
@@ -85,22 +90,25 @@ class TestShiftRequest(unittest.TestCase):
def set_shift_approver(department):
department_doc = frappe.get_doc("Department", department)
- department_doc.append('shift_request_approver',{'approver': "test1@example.com"})
+ department_doc.append("shift_request_approver", {"approver": "test1@example.com"})
department_doc.save()
department_doc.reload()
+
def make_shift_request(approver, do_not_submit=0):
- shift_request = frappe.get_doc({
- "doctype": "Shift Request",
- "shift_type": "Day Shift",
- "company": "_Test Company",
- "employee": "_T-Employee-00001",
- "employee_name": "_Test Employee",
- "from_date": nowdate(),
- "to_date": add_days(nowdate(), 10),
- "approver": approver,
- "status": "Approved"
- }).insert()
+ shift_request = frappe.get_doc(
+ {
+ "doctype": "Shift Request",
+ "shift_type": "Day Shift",
+ "company": "_Test Company",
+ "employee": "_T-Employee-00001",
+ "employee_name": "_Test Employee",
+ "from_date": nowdate(),
+ "to_date": add_days(nowdate(), 10),
+ "approver": approver,
+ "status": "Approved",
+ }
+ ).insert()
if do_not_submit:
return shift_request
diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py
index 562a5739d67..2000eeb5443 100644
--- a/erpnext/hr/doctype/shift_type/shift_type.py
+++ b/erpnext/hr/doctype/shift_type/shift_type.py
@@ -24,20 +24,45 @@ from erpnext.hr.doctype.shift_assignment.shift_assignment import (
class ShiftType(Document):
@frappe.whitelist()
def process_auto_attendance(self):
- if not cint(self.enable_auto_attendance) or not self.process_attendance_after or not self.last_sync_of_checkin:
+ if (
+ not cint(self.enable_auto_attendance)
+ or not self.process_attendance_after
+ or not self.last_sync_of_checkin
+ ):
return
filters = {
- 'skip_auto_attendance':'0',
- 'attendance':('is', 'not set'),
- 'time':('>=', self.process_attendance_after),
- 'shift_actual_end': ('<', self.last_sync_of_checkin),
- 'shift': self.name
+ "skip_auto_attendance": "0",
+ "attendance": ("is", "not set"),
+ "time": (">=", self.process_attendance_after),
+ "shift_actual_end": ("<", self.last_sync_of_checkin),
+ "shift": self.name,
}
- logs = frappe.db.get_list('Employee Checkin', fields="*", filters=filters, order_by="employee,time")
- for key, group in itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])):
+ logs = frappe.db.get_list(
+ "Employee Checkin", fields="*", filters=filters, order_by="employee,time"
+ )
+ for key, group in itertools.groupby(
+ logs, key=lambda x: (x["employee"], x["shift_actual_start"])
+ ):
single_shift_logs = list(group)
- attendance_status, working_hours, late_entry, early_exit, in_time, out_time = self.get_attendance(single_shift_logs)
- mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, late_entry, early_exit, in_time, out_time, self.name)
+ (
+ attendance_status,
+ working_hours,
+ late_entry,
+ early_exit,
+ in_time,
+ out_time,
+ ) = self.get_attendance(single_shift_logs)
+ mark_attendance_and_link_log(
+ single_shift_logs,
+ attendance_status,
+ key[1].date(),
+ working_hours,
+ late_entry,
+ early_exit,
+ in_time,
+ out_time,
+ self.name,
+ )
for employee in self.get_assigned_employee(self.process_attendance_after, True):
self.mark_absent_for_dates_with_no_attendance(employee)
@@ -45,36 +70,66 @@ class ShiftType(Document):
"""Return attendance_status, working_hours, late_entry, early_exit, in_time, out_time
for a set of logs belonging to a single shift.
Assumtion:
- 1. These logs belongs to an single shift, single employee and is not in a holiday date.
- 2. Logs are in chronological order
+ 1. These logs belongs to an single shift, single employee and is not in a holiday date.
+ 2. Logs are in chronological order
"""
late_entry = early_exit = False
- total_working_hours, in_time, out_time = calculate_working_hours(logs, self.determine_check_in_and_check_out, self.working_hours_calculation_based_on)
- if cint(self.enable_entry_grace_period) and in_time and in_time > logs[0].shift_start + timedelta(minutes=cint(self.late_entry_grace_period)):
+ total_working_hours, in_time, out_time = calculate_working_hours(
+ logs, self.determine_check_in_and_check_out, self.working_hours_calculation_based_on
+ )
+ if (
+ cint(self.enable_entry_grace_period)
+ and in_time
+ and in_time > logs[0].shift_start + timedelta(minutes=cint(self.late_entry_grace_period))
+ ):
late_entry = True
- if cint(self.enable_exit_grace_period) and out_time and out_time < logs[0].shift_end - timedelta(minutes=cint(self.early_exit_grace_period)):
+ if (
+ cint(self.enable_exit_grace_period)
+ and out_time
+ and out_time < logs[0].shift_end - timedelta(minutes=cint(self.early_exit_grace_period))
+ ):
early_exit = True
- if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent:
- return 'Absent', total_working_hours, late_entry, early_exit, in_time, out_time
- if self.working_hours_threshold_for_half_day and total_working_hours < self.working_hours_threshold_for_half_day:
- return 'Half Day', total_working_hours, late_entry, early_exit, in_time, out_time
- return 'Present', total_working_hours, late_entry, early_exit, in_time, out_time
+ if (
+ self.working_hours_threshold_for_absent
+ and total_working_hours < self.working_hours_threshold_for_absent
+ ):
+ return "Absent", total_working_hours, late_entry, early_exit, in_time, out_time
+ if (
+ self.working_hours_threshold_for_half_day
+ and total_working_hours < self.working_hours_threshold_for_half_day
+ ):
+ return "Half Day", total_working_hours, late_entry, early_exit, in_time, out_time
+ return "Present", total_working_hours, late_entry, early_exit, in_time, out_time
def mark_absent_for_dates_with_no_attendance(self, employee):
"""Marks Absents for the given employee on working days in this shift which have no attendance marked.
The Absent is marked starting from 'process_attendance_after' or employee creation date.
"""
- date_of_joining, relieving_date, employee_creation = frappe.db.get_value("Employee", employee, ["date_of_joining", "relieving_date", "creation"])
+ date_of_joining, relieving_date, employee_creation = frappe.db.get_value(
+ "Employee", employee, ["date_of_joining", "relieving_date", "creation"]
+ )
if not date_of_joining:
date_of_joining = employee_creation.date()
start_date = max(getdate(self.process_attendance_after), date_of_joining)
- actual_shift_datetime = get_actual_start_end_datetime_of_shift(employee, get_datetime(self.last_sync_of_checkin), True)
- last_shift_time = actual_shift_datetime[0] if actual_shift_datetime[0] else get_datetime(self.last_sync_of_checkin)
- prev_shift = get_employee_shift(employee, last_shift_time.date()-timedelta(days=1), True, 'reverse')
+ actual_shift_datetime = get_actual_start_end_datetime_of_shift(
+ employee, get_datetime(self.last_sync_of_checkin), True
+ )
+ last_shift_time = (
+ actual_shift_datetime[0]
+ if actual_shift_datetime[0]
+ else get_datetime(self.last_sync_of_checkin)
+ )
+ prev_shift = get_employee_shift(
+ employee, last_shift_time.date() - timedelta(days=1), True, "reverse"
+ )
if prev_shift:
- end_date = min(prev_shift.start_datetime.date(), relieving_date) if relieving_date else prev_shift.start_datetime.date()
+ end_date = (
+ min(prev_shift.start_datetime.date(), relieving_date)
+ if relieving_date
+ else prev_shift.start_datetime.date()
+ )
else:
return
holiday_list_name = self.holiday_list
@@ -84,37 +139,50 @@ class ShiftType(Document):
for date in dates:
shift_details = get_employee_shift(employee, date, True)
if shift_details and shift_details.shift_type.name == self.name:
- mark_attendance(employee, date, 'Absent', self.name)
+ attendance = mark_attendance(employee, date, "Absent", self.name)
+ if attendance:
+ frappe.get_doc(
+ {
+ "doctype": "Comment",
+ "comment_type": "Comment",
+ "reference_doctype": "Attendance",
+ "reference_name": attendance,
+ "content": frappe._("Employee was marked Absent due to missing Employee Checkins."),
+ }
+ ).insert(ignore_permissions=True)
def get_assigned_employee(self, from_date=None, consider_default_shift=False):
- filters = {'start_date':('>', from_date), 'shift_type': self.name, 'docstatus': '1'}
+ filters = {"start_date": (">", from_date), "shift_type": self.name, "docstatus": "1"}
if not from_date:
del filters["start_date"]
- assigned_employees = frappe.get_all('Shift Assignment', 'employee', filters, as_list=True)
+ assigned_employees = frappe.get_all("Shift Assignment", "employee", filters, as_list=True)
assigned_employees = [x[0] for x in assigned_employees]
if consider_default_shift:
- filters = {'default_shift': self.name, 'status': ['!=', 'Inactive']}
- default_shift_employees = frappe.get_all('Employee', 'name', filters, as_list=True)
+ filters = {"default_shift": self.name, "status": ["!=", "Inactive"]}
+ default_shift_employees = frappe.get_all("Employee", "name", filters, as_list=True)
default_shift_employees = [x[0] for x in default_shift_employees]
- return list(set(assigned_employees+default_shift_employees))
+ return list(set(assigned_employees + default_shift_employees))
return assigned_employees
+
def process_auto_attendance_for_all_shifts():
- shift_list = frappe.get_all('Shift Type', 'name', {'enable_auto_attendance':'1'}, as_list=True)
+ shift_list = frappe.get_all("Shift Type", "name", {"enable_auto_attendance": "1"}, as_list=True)
for shift in shift_list:
- doc = frappe.get_doc('Shift Type', shift[0])
+ doc = frappe.get_doc("Shift Type", shift[0])
doc.process_auto_attendance()
-def get_filtered_date_list(employee, start_date, end_date, filter_attendance=True, holiday_list=None):
- """Returns a list of dates after removing the dates with attendance and holidays
- """
+
+def get_filtered_date_list(
+ employee, start_date, end_date, filter_attendance=True, holiday_list=None
+):
+ """Returns a list of dates after removing the dates with attendance and holidays"""
base_dates_query = """select adddate(%(start_date)s, t2.i*100 + t1.i*10 + t0.i) selected_date from
(select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t0,
(select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t1,
(select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t2"""
- condition_query = ''
+ condition_query = ""
if filter_attendance:
condition_query += """ and a.selected_date not in (
select attendance_date from `tabAttendance`
@@ -126,10 +194,20 @@ def get_filtered_date_list(employee, start_date, end_date, filter_attendance=Tru
parentfield = 'holidays' and parent = %(holiday_list)s
and holiday_date between %(start_date)s and %(end_date)s)"""
- dates = frappe.db.sql("""select * from
+ dates = frappe.db.sql(
+ """select * from
({base_dates_query}) as a
where a.selected_date <= %(end_date)s {condition_query}
- """.format(base_dates_query=base_dates_query, condition_query=condition_query),
- {"employee":employee, "start_date":start_date, "end_date":end_date, "holiday_list":holiday_list}, as_list=True)
+ """.format(
+ base_dates_query=base_dates_query, condition_query=condition_query
+ ),
+ {
+ "employee": employee,
+ "start_date": start_date,
+ "end_date": end_date,
+ "holiday_list": holiday_list,
+ },
+ as_list=True,
+ )
return [getdate(date[0]) for date in dates]
diff --git a/erpnext/hr/doctype/shift_type/shift_type_dashboard.py b/erpnext/hr/doctype/shift_type/shift_type_dashboard.py
index b523f0e01ce..920d8fd5475 100644
--- a/erpnext/hr/doctype/shift_type/shift_type_dashboard.py
+++ b/erpnext/hr/doctype/shift_type/shift_type_dashboard.py
@@ -1,15 +1,8 @@
-
-
def get_data():
return {
- 'fieldname': 'shift',
- 'non_standard_fieldnames': {
- 'Shift Request': 'shift_type',
- 'Shift Assignment': 'shift_type'
- },
- 'transactions': [
- {
- 'items': ['Attendance', 'Employee Checkin', 'Shift Request', 'Shift Assignment']
- }
- ]
+ "fieldname": "shift",
+ "non_standard_fieldnames": {"Shift Request": "shift_type", "Shift Assignment": "shift_type"},
+ "transactions": [
+ {"items": ["Attendance", "Employee Checkin", "Shift Request", "Shift Assignment"]}
+ ],
}
diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
index 7b2ea215ad8..93a493c9d25 100644
--- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py
+++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
@@ -9,8 +9,13 @@ from frappe.utils import cint, flt, getdate, nowdate
from frappe.utils.nestedset import get_descendants_of
-class SubsidiaryCompanyError(frappe.ValidationError): pass
-class ParentCompanyError(frappe.ValidationError): pass
+class SubsidiaryCompanyError(frappe.ValidationError):
+ pass
+
+
+class ParentCompanyError(frappe.ValidationError):
+ pass
+
class StaffingPlan(Document):
def validate(self):
@@ -33,11 +38,11 @@ class StaffingPlan(Document):
self.total_estimated_budget = 0
for detail in self.get("staffing_details"):
- #Set readonly fields
+ # Set readonly fields
self.set_number_of_positions(detail)
designation_counts = get_designation_counts(detail.designation, self.company)
- detail.current_count = designation_counts['employee_count']
- detail.current_openings = designation_counts['job_openings']
+ detail.current_count = designation_counts["employee_count"]
+ detail.current_openings = designation_counts["job_openings"]
detail.total_estimated_cost = 0
if detail.number_of_positions > 0:
@@ -52,80 +57,122 @@ class StaffingPlan(Document):
def validate_overlap(self, staffing_plan_detail):
# Validate if any submitted Staffing Plan exist for any Designations in this plan
# and spd.vacancies>0 ?
- overlap = frappe.db.sql("""select spd.parent
+ overlap = frappe.db.sql(
+ """select spd.parent
from `tabStaffing Plan Detail` spd join `tabStaffing Plan` sp on spd.parent=sp.name
where spd.designation=%s and sp.docstatus=1
and sp.to_date >= %s and sp.from_date <= %s and sp.company = %s
- """, (staffing_plan_detail.designation, self.from_date, self.to_date, self.company))
- if overlap and overlap [0][0]:
- frappe.throw(_("Staffing Plan {0} already exist for designation {1}")
- .format(overlap[0][0], staffing_plan_detail.designation))
+ """,
+ (staffing_plan_detail.designation, self.from_date, self.to_date, self.company),
+ )
+ if overlap and overlap[0][0]:
+ frappe.throw(
+ _("Staffing Plan {0} already exist for designation {1}").format(
+ overlap[0][0], staffing_plan_detail.designation
+ )
+ )
def validate_with_parent_plan(self, staffing_plan_detail):
- if not frappe.get_cached_value('Company', self.company, "parent_company"):
- return # No parent, nothing to validate
+ if not frappe.get_cached_value("Company", self.company, "parent_company"):
+ return # No parent, nothing to validate
# Get staffing plan applicable for the company (Parent Company)
- parent_plan_details = get_active_staffing_plan_details(self.company, staffing_plan_detail.designation, self.from_date, self.to_date)
+ parent_plan_details = get_active_staffing_plan_details(
+ self.company, staffing_plan_detail.designation, self.from_date, self.to_date
+ )
if not parent_plan_details:
- return #no staffing plan for any parent Company in hierarchy
+ return # no staffing plan for any parent Company in hierarchy
# Fetch parent company which owns the staffing plan. NOTE: Parent could be higher up in the hierarchy
parent_company = frappe.db.get_value("Staffing Plan", parent_plan_details[0].name, "company")
# Parent plan available, validate with parent, siblings as well as children of staffing plan Company
- if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \
- flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost):
- frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \
- for {2} as per staffing plan {3} for parent company {4}.").format(
+ if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or flt(
+ staffing_plan_detail.total_estimated_cost
+ ) > flt(parent_plan_details[0].total_estimated_cost):
+ frappe.throw(
+ _(
+ "You can only plan for upto {0} vacancies and budget {1} \
+ for {2} as per staffing plan {3} for parent company {4}."
+ ).format(
cint(parent_plan_details[0].vacancies),
parent_plan_details[0].total_estimated_cost,
frappe.bold(staffing_plan_detail.designation),
parent_plan_details[0].name,
- parent_company), ParentCompanyError)
+ parent_company,
+ ),
+ ParentCompanyError,
+ )
- #Get vacanices already planned for all companies down the hierarchy of Parent Company
- lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"])
- all_sibling_details = frappe.db.sql("""select sum(spd.vacancies) as vacancies,
+ # Get vacanices already planned for all companies down the hierarchy of Parent Company
+ lft, rgt = frappe.get_cached_value("Company", parent_company, ["lft", "rgt"])
+ all_sibling_details = frappe.db.sql(
+ """select sum(spd.vacancies) as vacancies,
sum(spd.total_estimated_cost) as total_estimated_cost
from `tabStaffing Plan Detail` spd join `tabStaffing Plan` sp on spd.parent=sp.name
where spd.designation=%s and sp.docstatus=1
and sp.to_date >= %s and sp.from_date <=%s
and sp.company in (select name from tabCompany where lft > %s and rgt < %s)
- """, (staffing_plan_detail.designation, self.from_date, self.to_date, lft, rgt), as_dict = 1)[0]
+ """,
+ (staffing_plan_detail.designation, self.from_date, self.to_date, lft, rgt),
+ as_dict=1,
+ )[0]
- if (cint(parent_plan_details[0].vacancies) < \
- (cint(staffing_plan_detail.vacancies) + cint(all_sibling_details.vacancies))) or \
- (flt(parent_plan_details[0].total_estimated_cost) < \
- (flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))):
- frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \
- You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format(
+ if (
+ cint(parent_plan_details[0].vacancies)
+ < (cint(staffing_plan_detail.vacancies) + cint(all_sibling_details.vacancies))
+ ) or (
+ flt(parent_plan_details[0].total_estimated_cost)
+ < (
+ flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost)
+ )
+ ):
+ frappe.throw(
+ _(
+ "{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \
+ You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}."
+ ).format(
cint(all_sibling_details.vacancies),
all_sibling_details.total_estimated_cost,
frappe.bold(staffing_plan_detail.designation),
parent_company,
cint(parent_plan_details[0].vacancies),
parent_plan_details[0].total_estimated_cost,
- parent_plan_details[0].name))
+ parent_plan_details[0].name,
+ )
+ )
def validate_with_subsidiary_plans(self, staffing_plan_detail):
- #Valdate this plan with all child company plan
- children_details = frappe.db.sql("""select sum(spd.vacancies) as vacancies,
+ # Valdate this plan with all child company plan
+ children_details = frappe.db.sql(
+ """select sum(spd.vacancies) as vacancies,
sum(spd.total_estimated_cost) as total_estimated_cost
from `tabStaffing Plan Detail` spd join `tabStaffing Plan` sp on spd.parent=sp.name
where spd.designation=%s and sp.docstatus=1
and sp.to_date >= %s and sp.from_date <=%s
and sp.company in (select name from tabCompany where parent_company = %s)
- """, (staffing_plan_detail.designation, self.from_date, self.to_date, self.company), as_dict = 1)[0]
+ """,
+ (staffing_plan_detail.designation, self.from_date, self.to_date, self.company),
+ as_dict=1,
+ )[0]
- if children_details and \
- cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \
- flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost):
- frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \
- Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format(
+ if (
+ children_details
+ and cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies)
+ or flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost)
+ ):
+ frappe.throw(
+ _(
+ "Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \
+ Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies"
+ ).format(
self.company,
cint(children_details.vacancies),
children_details.total_estimated_cost,
- frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError)
+ frappe.bold(staffing_plan_detail.designation),
+ ),
+ SubsidiaryCompanyError,
+ )
+
@frappe.whitelist()
def get_designation_counts(designation, company):
@@ -133,25 +180,24 @@ def get_designation_counts(designation, company):
return False
employee_counts = {}
- company_set = get_descendants_of('Company', company)
+ company_set = get_descendants_of("Company", company)
company_set.append(company)
- employee_counts["employee_count"] = frappe.db.get_value("Employee",
- filters={
- 'designation': designation,
- 'status': 'Active',
- 'company': ('in', company_set)
- }, fieldname=['count(name)'])
+ employee_counts["employee_count"] = frappe.db.get_value(
+ "Employee",
+ filters={"designation": designation, "status": "Active", "company": ("in", company_set)},
+ fieldname=["count(name)"],
+ )
- employee_counts['job_openings'] = frappe.db.get_value("Job Opening",
- filters={
- 'designation': designation,
- 'status': 'Open',
- 'company': ('in', company_set)
- }, fieldname=['count(name)'])
+ employee_counts["job_openings"] = frappe.db.get_value(
+ "Job Opening",
+ filters={"designation": designation, "status": "Open", "company": ("in", company_set)},
+ fieldname=["count(name)"],
+ )
return employee_counts
+
@frappe.whitelist()
def get_active_staffing_plan_details(company, designation, from_date=None, to_date=None):
if from_date is None:
@@ -161,17 +207,22 @@ def get_active_staffing_plan_details(company, designation, from_date=None, to_da
if not company or not designation:
frappe.throw(_("Please select Company and Designation"))
- staffing_plan = frappe.db.sql("""
+ staffing_plan = frappe.db.sql(
+ """
select sp.name, spd.vacancies, spd.total_estimated_cost
from `tabStaffing Plan Detail` spd join `tabStaffing Plan` sp on spd.parent=sp.name
where company=%s and spd.designation=%s and sp.docstatus=1
- and to_date >= %s and from_date <= %s """, (company, designation, from_date, to_date), as_dict = 1)
+ and to_date >= %s and from_date <= %s """,
+ (company, designation, from_date, to_date),
+ as_dict=1,
+ )
if not staffing_plan:
- parent_company = frappe.get_cached_value('Company', company, "parent_company")
+ parent_company = frappe.get_cached_value("Company", company, "parent_company")
if parent_company:
- staffing_plan = get_active_staffing_plan_details(parent_company,
- designation, from_date, to_date)
+ staffing_plan = get_active_staffing_plan_details(
+ parent_company, designation, from_date, to_date
+ )
# Only a single staffing plan can be active for a designation on given date
return staffing_plan if staffing_plan else None
diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan_dashboard.py b/erpnext/hr/doctype/staffing_plan/staffing_plan_dashboard.py
index c04e5853a52..0f555d9db2d 100644
--- a/erpnext/hr/doctype/staffing_plan/staffing_plan_dashboard.py
+++ b/erpnext/hr/doctype/staffing_plan/staffing_plan_dashboard.py
@@ -1,11 +1,5 @@
-
-
def get_data():
- return {
- 'fieldname': 'staffing_plan',
- 'transactions': [
- {
- 'items': ['Job Opening']
- }
- ],
- }
+ return {
+ "fieldname": "staffing_plan",
+ "transactions": [{"items": ["Job Opening"]}],
+ }
diff --git a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py
index 8ff0dbbc28e..a3adbbd56a5 100644
--- a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py
+++ b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py
@@ -13,6 +13,7 @@ from erpnext.hr.doctype.staffing_plan.staffing_plan import (
test_dependencies = ["Designation"]
+
class TestStaffingPlan(unittest.TestCase):
def test_staffing_plan(self):
_set_up()
@@ -24,11 +25,10 @@ class TestStaffingPlan(unittest.TestCase):
staffing_plan.name = "Test"
staffing_plan.from_date = nowdate()
staffing_plan.to_date = add_days(nowdate(), 10)
- staffing_plan.append("staffing_details", {
- "designation": "Designer",
- "vacancies": 6,
- "estimated_cost_per_position": 50000
- })
+ staffing_plan.append(
+ "staffing_details",
+ {"designation": "Designer", "vacancies": 6, "estimated_cost_per_position": 50000},
+ )
staffing_plan.insert()
staffing_plan.submit()
self.assertEqual(staffing_plan.total_estimated_budget, 300000.00)
@@ -42,11 +42,10 @@ class TestStaffingPlan(unittest.TestCase):
staffing_plan.name = "Test 1"
staffing_plan.from_date = nowdate()
staffing_plan.to_date = add_days(nowdate(), 10)
- staffing_plan.append("staffing_details", {
- "designation": "Designer",
- "vacancies": 3,
- "estimated_cost_per_position": 45000
- })
+ staffing_plan.append(
+ "staffing_details",
+ {"designation": "Designer", "vacancies": 3, "estimated_cost_per_position": 45000},
+ )
self.assertRaises(SubsidiaryCompanyError, staffing_plan.insert)
def test_staffing_plan_parent_company(self):
@@ -58,11 +57,10 @@ class TestStaffingPlan(unittest.TestCase):
staffing_plan.name = "Test"
staffing_plan.from_date = nowdate()
staffing_plan.to_date = add_days(nowdate(), 10)
- staffing_plan.append("staffing_details", {
- "designation": "Designer",
- "vacancies": 7,
- "estimated_cost_per_position": 50000
- })
+ staffing_plan.append(
+ "staffing_details",
+ {"designation": "Designer", "vacancies": 7, "estimated_cost_per_position": 50000},
+ )
staffing_plan.insert()
staffing_plan.submit()
self.assertEqual(staffing_plan.total_estimated_budget, 350000.00)
@@ -73,19 +71,20 @@ class TestStaffingPlan(unittest.TestCase):
staffing_plan.name = "Test 1"
staffing_plan.from_date = nowdate()
staffing_plan.to_date = add_days(nowdate(), 10)
- staffing_plan.append("staffing_details", {
- "designation": "Designer",
- "vacancies": 7,
- "estimated_cost_per_position": 60000
- })
+ staffing_plan.append(
+ "staffing_details",
+ {"designation": "Designer", "vacancies": 7, "estimated_cost_per_position": 60000},
+ )
staffing_plan.insert()
self.assertRaises(ParentCompanyError, staffing_plan.submit)
+
def _set_up():
for doctype in ["Staffing Plan", "Staffing Plan Detail"]:
frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype))
make_company()
+
def make_company():
if frappe.db.exists("Company", "_Test Company 10"):
return
diff --git a/erpnext/hr/doctype/training_event/test_training_event.py b/erpnext/hr/doctype/training_event/test_training_event.py
index f4329c9fe70..ec7eb74da9e 100644
--- a/erpnext/hr/doctype/training_event/test_training_event.py
+++ b/erpnext/hr/doctype/training_event/test_training_event.py
@@ -14,10 +14,7 @@ class TestTrainingEvent(unittest.TestCase):
create_training_program("Basic Training")
employee = make_employee("robert_loan@trainig.com")
employee2 = make_employee("suzie.tan@trainig.com")
- self.attendees = [
- {"employee": employee},
- {"employee": employee2}
- ]
+ self.attendees = [{"employee": employee}, {"employee": employee2}]
def test_training_event_status_update(self):
training_event = create_training_event(self.attendees)
@@ -43,20 +40,25 @@ class TestTrainingEvent(unittest.TestCase):
def create_training_program(training_program):
if not frappe.db.get_value("Training Program", training_program):
- frappe.get_doc({
- "doctype": "Training Program",
- "training_program": training_program,
- "description": training_program
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Training Program",
+ "training_program": training_program,
+ "description": training_program,
+ }
+ ).insert()
+
def create_training_event(attendees):
- return frappe.get_doc({
- "doctype": "Training Event",
- "event_name": "Basic Training Event",
- "training_program": "Basic Training",
- "location": "Union Square",
- "start_time": add_days(today(), 5),
- "end_time": add_days(today(), 6),
- "introduction": "Welcome to the Basic Training Event",
- "employees": attendees
- }).insert()
+ return frappe.get_doc(
+ {
+ "doctype": "Training Event",
+ "event_name": "Basic Training Event",
+ "training_program": "Basic Training",
+ "location": "Union Square",
+ "start_time": add_days(today(), 5),
+ "end_time": add_days(today(), 6),
+ "introduction": "Welcome to the Basic Training Event",
+ "employees": attendees,
+ }
+ ).insert()
diff --git a/erpnext/hr/doctype/training_event/training_event.py b/erpnext/hr/doctype/training_event/training_event.py
index c8c8bbe7339..59972bb2f3f 100644
--- a/erpnext/hr/doctype/training_event/training_event.py
+++ b/erpnext/hr/doctype/training_event/training_event.py
@@ -19,21 +19,20 @@ class TrainingEvent(Document):
self.set_status_for_attendees()
def set_employee_emails(self):
- self.employee_emails = ', '.join(get_employee_emails([d.employee
- for d in self.employees]))
+ self.employee_emails = ", ".join(get_employee_emails([d.employee for d in self.employees]))
def validate_period(self):
if time_diff_in_seconds(self.end_time, self.start_time) <= 0:
- frappe.throw(_('End time cannot be before start time'))
+ frappe.throw(_("End time cannot be before start time"))
def set_status_for_attendees(self):
- if self.event_status == 'Completed':
+ if self.event_status == "Completed":
for employee in self.employees:
- if employee.attendance == 'Present' and employee.status != 'Feedback Submitted':
- employee.status = 'Completed'
+ if employee.attendance == "Present" and employee.status != "Feedback Submitted":
+ employee.status = "Completed"
- elif self.event_status == 'Scheduled':
+ elif self.event_status == "Scheduled":
for employee in self.employees:
- employee.status = 'Open'
+ employee.status = "Open"
self.db_update_all()
diff --git a/erpnext/hr/doctype/training_event/training_event_dashboard.py b/erpnext/hr/doctype/training_event/training_event_dashboard.py
index 8c4162d5010..ca13938e585 100644
--- a/erpnext/hr/doctype/training_event/training_event_dashboard.py
+++ b/erpnext/hr/doctype/training_event/training_event_dashboard.py
@@ -1,11 +1,7 @@
-
-
def get_data():
- return {
- 'fieldname': 'training_event',
- 'transactions': [
- {
- 'items': ['Training Result', 'Training Feedback']
- },
- ],
- }
+ return {
+ "fieldname": "training_event",
+ "transactions": [
+ {"items": ["Training Result", "Training Feedback"]},
+ ],
+ }
diff --git a/erpnext/hr/doctype/training_feedback/test_training_feedback.py b/erpnext/hr/doctype/training_feedback/test_training_feedback.py
index 58ed6231003..c787b7038fe 100644
--- a/erpnext/hr/doctype/training_feedback/test_training_feedback.py
+++ b/erpnext/hr/doctype/training_feedback/test_training_feedback.py
@@ -32,10 +32,9 @@ class TestTrainingFeedback(unittest.TestCase):
self.assertRaises(frappe.ValidationError, feedback.save)
# cannot record feedback for absent employee
- employee = frappe.db.get_value("Training Event Employee", {
- "parent": training_event.name,
- "employee": self.employee
- }, "name")
+ employee = frappe.db.get_value(
+ "Training Event Employee", {"parent": training_event.name, "employee": self.employee}, "name"
+ )
frappe.db.set_value("Training Event Employee", employee, "attendance", "Absent")
feedback = create_training_feedback(training_event.name, self.employee)
@@ -52,10 +51,9 @@ class TestTrainingFeedback(unittest.TestCase):
feedback = create_training_feedback(training_event.name, self.employee)
feedback.submit()
- status = frappe.db.get_value("Training Event Employee", {
- "parent": training_event.name,
- "employee": self.employee
- }, "status")
+ status = frappe.db.get_value(
+ "Training Event Employee", {"parent": training_event.name, "employee": self.employee}, "status"
+ )
self.assertEqual(status, "Feedback Submitted")
@@ -64,9 +62,11 @@ class TestTrainingFeedback(unittest.TestCase):
def create_training_feedback(event, employee):
- return frappe.get_doc({
- "doctype": "Training Feedback",
- "training_event": event,
- "employee": employee,
- "feedback": "Test"
- })
+ return frappe.get_doc(
+ {
+ "doctype": "Training Feedback",
+ "training_event": event,
+ "employee": employee,
+ "feedback": "Test",
+ }
+ )
diff --git a/erpnext/hr/doctype/training_feedback/training_feedback.py b/erpnext/hr/doctype/training_feedback/training_feedback.py
index 1f9ec3b0b8a..d5de28ed2d5 100644
--- a/erpnext/hr/doctype/training_feedback/training_feedback.py
+++ b/erpnext/hr/doctype/training_feedback/training_feedback.py
@@ -13,32 +13,35 @@ class TrainingFeedback(Document):
if training_event.docstatus != 1:
frappe.throw(_("{0} must be submitted").format(_("Training Event")))
- emp_event_details = frappe.db.get_value("Training Event Employee", {
- "parent": self.training_event,
- "employee": self.employee
- }, ["name", "attendance"], as_dict=True)
+ emp_event_details = frappe.db.get_value(
+ "Training Event Employee",
+ {"parent": self.training_event, "employee": self.employee},
+ ["name", "attendance"],
+ as_dict=True,
+ )
if not emp_event_details:
- frappe.throw(_("Employee {0} not found in Training Event Participants.").format(
- frappe.bold(self.employee_name)))
+ frappe.throw(
+ _("Employee {0} not found in Training Event Participants.").format(
+ frappe.bold(self.employee_name)
+ )
+ )
if emp_event_details.attendance == "Absent":
frappe.throw(_("Feedback cannot be recorded for an absent Employee."))
def on_submit(self):
- employee = frappe.db.get_value("Training Event Employee", {
- "parent": self.training_event,
- "employee": self.employee
- })
+ employee = frappe.db.get_value(
+ "Training Event Employee", {"parent": self.training_event, "employee": self.employee}
+ )
if employee:
frappe.db.set_value("Training Event Employee", employee, "status", "Feedback Submitted")
def on_cancel(self):
- employee = frappe.db.get_value("Training Event Employee", {
- "parent": self.training_event,
- "employee": self.employee
- })
+ employee = frappe.db.get_value(
+ "Training Event Employee", {"parent": self.training_event, "employee": self.employee}
+ )
if employee:
frappe.db.set_value("Training Event Employee", employee, "status", "Completed")
diff --git a/erpnext/hr/doctype/training_program/training_program_dashboard.py b/erpnext/hr/doctype/training_program/training_program_dashboard.py
index 51137d162c7..1735db18e12 100644
--- a/erpnext/hr/doctype/training_program/training_program_dashboard.py
+++ b/erpnext/hr/doctype/training_program/training_program_dashboard.py
@@ -1,14 +1,10 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'training_program',
- 'transactions': [
- {
- 'label': _('Training Events'),
- 'items': ['Training Event']
- },
- ]
+ "fieldname": "training_program",
+ "transactions": [
+ {"label": _("Training Events"), "items": ["Training Event"]},
+ ],
}
diff --git a/erpnext/hr/doctype/training_result/test_training_result.py b/erpnext/hr/doctype/training_result/test_training_result.py
index 1735ff4e341..136543cbe1a 100644
--- a/erpnext/hr/doctype/training_result/test_training_result.py
+++ b/erpnext/hr/doctype/training_result/test_training_result.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Training Result')
+
class TestTrainingResult(unittest.TestCase):
pass
diff --git a/erpnext/hr/doctype/training_result/training_result.py b/erpnext/hr/doctype/training_result/training_result.py
index bb5c71e7a15..48a5b2c2e9b 100644
--- a/erpnext/hr/doctype/training_result/training_result.py
+++ b/erpnext/hr/doctype/training_result/training_result.py
@@ -13,22 +13,22 @@ class TrainingResult(Document):
def validate(self):
training_event = frappe.get_doc("Training Event", self.training_event)
if training_event.docstatus != 1:
- frappe.throw(_('{0} must be submitted').format(_('Training Event')))
+ frappe.throw(_("{0} must be submitted").format(_("Training Event")))
- self.employee_emails = ', '.join(get_employee_emails([d.employee
- for d in self.employees]))
+ self.employee_emails = ", ".join(get_employee_emails([d.employee for d in self.employees]))
def on_submit(self):
training_event = frappe.get_doc("Training Event", self.training_event)
- training_event.status = 'Completed'
+ training_event.status = "Completed"
for e in self.employees:
for e1 in training_event.employees:
if e1.employee == e.employee:
- e1.status = 'Completed'
+ e1.status = "Completed"
break
training_event.save()
+
@frappe.whitelist()
def get_employees(training_event):
return frappe.get_doc("Training Event", training_event).employees
diff --git a/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py b/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py
index 4c7bd805f98..537c20633be 100644
--- a/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py
+++ b/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py
@@ -10,12 +10,15 @@ import erpnext
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.upload_attendance.upload_attendance import get_data
-test_dependencies = ['Holiday List']
+test_dependencies = ["Holiday List"]
+
class TestUploadAttendance(unittest.TestCase):
@classmethod
def setUpClass(cls):
- frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", '_Test Holiday List')
+ frappe.db.set_value(
+ "Company", erpnext.get_default_company(), "default_holiday_list", "_Test Holiday List"
+ )
def test_date_range(self):
employee = make_employee("test_employee@company.com")
@@ -27,14 +30,13 @@ class TestUploadAttendance(unittest.TestCase):
employee_doc.date_of_joining = date_of_joining
employee_doc.relieving_date = relieving_date
employee_doc.save()
- args = {
- "from_date": from_date,
- "to_date": to_date
- }
+ args = {"from_date": from_date, "to_date": to_date}
data = get_data(args)
filtered_data = []
for row in data:
if row[1] == employee:
filtered_data.append(row)
for row in filtered_data:
- self.assertTrue(getdate(row[3]) >= getdate(date_of_joining) and getdate(row[3]) <= getdate(relieving_date))
+ self.assertTrue(
+ getdate(row[3]) >= getdate(date_of_joining) and getdate(row[3]) <= getdate(relieving_date)
+ )
diff --git a/erpnext/hr/doctype/upload_attendance/upload_attendance.py b/erpnext/hr/doctype/upload_attendance/upload_attendance.py
index 94eb3001009..a66a48124da 100644
--- a/erpnext/hr/doctype/upload_attendance/upload_attendance.py
+++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.py
@@ -17,6 +17,7 @@ from erpnext.hr.utils import get_holiday_dates_for_employee
class UploadAttendance(Document):
pass
+
@frappe.whitelist()
def get_template():
if not frappe.has_permission("Attendance", "create"):
@@ -38,29 +39,37 @@ def get_template():
return
# write out response as a type csv
- frappe.response['result'] = cstr(w.getvalue())
- frappe.response['type'] = 'csv'
- frappe.response['doctype'] = "Attendance"
+ frappe.response["result"] = cstr(w.getvalue())
+ frappe.response["type"] = "csv"
+ frappe.response["doctype"] = "Attendance"
+
def add_header(w):
- status = ", ".join((frappe.get_meta("Attendance").get_field("status").options or "").strip().split("\n"))
+ status = ", ".join(
+ (frappe.get_meta("Attendance").get_field("status").options or "").strip().split("\n")
+ )
w.writerow(["Notes:"])
w.writerow(["Please do not change the template headings"])
w.writerow(["Status should be one of these values: " + status])
w.writerow(["If you are overwriting existing attendance records, 'ID' column mandatory"])
- w.writerow(["ID", "Employee", "Employee Name", "Date", "Status", "Leave Type",
- "Company", "Naming Series"])
+ w.writerow(
+ ["ID", "Employee", "Employee Name", "Date", "Status", "Leave Type", "Company", "Naming Series"]
+ )
return w
+
def add_data(w, args):
data = get_data(args)
writedata(w, data)
return w
+
def get_data(args):
dates = get_dates(args)
employees = get_active_employees()
- holidays = get_holidays_for_employees([employee.name for employee in employees], args["from_date"], args["to_date"])
+ holidays = get_holidays_for_employees(
+ [employee.name for employee in employees], args["from_date"], args["to_date"]
+ )
existing_attendance_records = get_existing_attendance_records(args)
data = []
for date in dates:
@@ -71,27 +80,33 @@ def get_data(args):
if getdate(date) > getdate(employee.relieving_date):
continue
existing_attendance = {}
- if existing_attendance_records \
- and tuple([getdate(date), employee.name]) in existing_attendance_records \
- and getdate(employee.date_of_joining) <= getdate(date) \
- and getdate(employee.relieving_date) >= getdate(date):
- existing_attendance = existing_attendance_records[tuple([getdate(date), employee.name])]
+ if (
+ existing_attendance_records
+ and tuple([getdate(date), employee.name]) in existing_attendance_records
+ and getdate(employee.date_of_joining) <= getdate(date)
+ and getdate(employee.relieving_date) >= getdate(date)
+ ):
+ existing_attendance = existing_attendance_records[tuple([getdate(date), employee.name])]
employee_holiday_list = get_holiday_list_for_employee(employee.name)
row = [
existing_attendance and existing_attendance.name or "",
- employee.name, employee.employee_name, date,
+ employee.name,
+ employee.employee_name,
+ date,
existing_attendance and existing_attendance.status or "",
- existing_attendance and existing_attendance.leave_type or "", employee.company,
+ existing_attendance and existing_attendance.leave_type or "",
+ employee.company,
existing_attendance and existing_attendance.naming_series or get_naming_series(),
]
if date in holidays[employee_holiday_list]:
- row[4] = "Holiday"
+ row[4] = "Holiday"
data.append(row)
return data
+
def get_holidays_for_employees(employees, from_date, to_date):
holidays = {}
for employee in employees:
@@ -102,30 +117,35 @@ def get_holidays_for_employees(employees, from_date, to_date):
return holidays
+
def writedata(w, data):
for row in data:
w.writerow(row)
+
def get_dates(args):
"""get list of dates in between from date and to date"""
no_of_days = date_diff(add_days(args["to_date"], 1), args["from_date"])
dates = [add_days(args["from_date"], i) for i in range(0, no_of_days)]
return dates
+
def get_active_employees():
- employees = frappe.db.get_all('Employee',
- fields=['name', 'employee_name', 'date_of_joining', 'company', 'relieving_date'],
- filters={
- 'docstatus': ['<', 2],
- 'status': 'Active'
- }
+ employees = frappe.db.get_all(
+ "Employee",
+ fields=["name", "employee_name", "date_of_joining", "company", "relieving_date"],
+ filters={"docstatus": ["<", 2], "status": "Active"},
)
return employees
+
def get_existing_attendance_records(args):
- attendance = frappe.db.sql("""select name, attendance_date, employee, status, leave_type, naming_series
+ attendance = frappe.db.sql(
+ """select name, attendance_date, employee, status, leave_type, naming_series
from `tabAttendance` where attendance_date between %s and %s and docstatus < 2""",
- (args["from_date"], args["to_date"]), as_dict=1)
+ (args["from_date"], args["to_date"]),
+ as_dict=1,
+ )
existing_attendance = {}
for att in attendance:
@@ -133,6 +153,7 @@ def get_existing_attendance_records(args):
return existing_attendance
+
def get_naming_series():
series = frappe.get_meta("Attendance").get_field("naming_series").options.strip().split("\n")
if not series:
@@ -146,15 +167,16 @@ def upload():
raise frappe.PermissionError
from frappe.utils.csvutils import read_csv_content
+
rows = read_csv_content(frappe.local.uploaded_file)
if not rows:
frappe.throw(_("Please select a csv file"))
frappe.enqueue(import_attendances, rows=rows, now=True if len(rows) < 200 else False)
-def import_attendances(rows):
+def import_attendances(rows):
def remove_holidays(rows):
- rows = [ row for row in rows if row[4] != "Holiday"]
+ rows = [row for row in rows if row[4] != "Holiday"]
return rows
from frappe.modules import scrub
@@ -172,7 +194,8 @@ def import_attendances(rows):
from frappe.utils.csvutils import check_record, import_doc
for i, row in enumerate(rows):
- if not row: continue
+ if not row:
+ continue
row_idx = i + 5
d = frappe._dict(zip(columns, row))
@@ -183,16 +206,12 @@ def import_attendances(rows):
try:
check_record(d)
ret.append(import_doc(d, "Attendance", 1, row_idx, submit=True))
- frappe.publish_realtime('import_attendance', dict(
- progress=i,
- total=len(rows)
- ))
+ frappe.publish_realtime("import_attendance", dict(progress=i, total=len(rows)))
except AttributeError:
pass
except Exception as e:
error = True
- ret.append('Error for row (#%d) %s : %s' % (row_idx,
- len(row)>1 and row[1] or "", cstr(e)))
+ ret.append("Error for row (#%d) %s : %s" % (row_idx, len(row) > 1 and row[1] or "", cstr(e)))
frappe.errprint(frappe.get_traceback())
if error:
@@ -200,7 +219,4 @@ def import_attendances(rows):
else:
frappe.db.commit()
- frappe.publish_realtime('import_attendance', dict(
- messages=ret,
- error=error
- ))
+ frappe.publish_realtime("import_attendance", dict(messages=ret, error=error))
diff --git a/erpnext/hr/doctype/vehicle/test_vehicle.py b/erpnext/hr/doctype/vehicle/test_vehicle.py
index c5ea5a38c86..97fe651122c 100644
--- a/erpnext/hr/doctype/vehicle/test_vehicle.py
+++ b/erpnext/hr/doctype/vehicle/test_vehicle.py
@@ -8,18 +8,21 @@ from frappe.utils import random_string
# test_records = frappe.get_test_records('Vehicle')
+
class TestVehicle(unittest.TestCase):
def test_make_vehicle(self):
- vehicle = frappe.get_doc({
- "doctype": "Vehicle",
- "license_plate": random_string(10).upper(),
- "make": "Maruti",
- "model": "PCM",
- "last_odometer":5000,
- "acquisition_date":frappe.utils.nowdate(),
- "location": "Mumbai",
- "chassis_no": "1234ABCD",
- "uom": "Litre",
- "vehicle_value":frappe.utils.flt(500000)
- })
+ vehicle = frappe.get_doc(
+ {
+ "doctype": "Vehicle",
+ "license_plate": random_string(10).upper(),
+ "make": "Maruti",
+ "model": "PCM",
+ "last_odometer": 5000,
+ "acquisition_date": frappe.utils.nowdate(),
+ "location": "Mumbai",
+ "chassis_no": "1234ABCD",
+ "uom": "Litre",
+ "vehicle_value": frappe.utils.flt(500000),
+ }
+ )
vehicle.insert()
diff --git a/erpnext/hr/doctype/vehicle/vehicle.py b/erpnext/hr/doctype/vehicle/vehicle.py
index 946233b5481..22c14c37278 100644
--- a/erpnext/hr/doctype/vehicle/vehicle.py
+++ b/erpnext/hr/doctype/vehicle/vehicle.py
@@ -15,9 +15,15 @@ class Vehicle(Document):
if getdate(self.carbon_check_date) > getdate():
frappe.throw(_("Last carbon check date cannot be a future date"))
+
def get_timeline_data(doctype, name):
- '''Return timeline for vehicle log'''
- return dict(frappe.db.sql('''select unix_timestamp(date), count(*)
+ """Return timeline for vehicle log"""
+ return dict(
+ frappe.db.sql(
+ """select unix_timestamp(date), count(*)
from `tabVehicle Log` where license_plate=%s
and date > date_sub(curdate(), interval 1 year)
- group by date''', name))
+ group by date""",
+ name,
+ )
+ )
diff --git a/erpnext/hr/doctype/vehicle/vehicle_dashboard.py b/erpnext/hr/doctype/vehicle/vehicle_dashboard.py
index bb38ab9d6b5..758dfbd60a4 100644
--- a/erpnext/hr/doctype/vehicle/vehicle_dashboard.py
+++ b/erpnext/hr/doctype/vehicle/vehicle_dashboard.py
@@ -1,21 +1,13 @@
-
from frappe import _
def get_data():
return {
- 'heatmap': True,
- 'heatmap_message': _('This is based on logs against this Vehicle. See timeline below for details'),
- 'fieldname': 'license_plate',
- 'non_standard_fieldnames':{
- 'Delivery Trip': 'vehicle'
- },
- 'transactions': [
- {
- 'items': ['Vehicle Log']
- },
- {
- 'items': ['Delivery Trip']
- }
- ]
+ "heatmap": True,
+ "heatmap_message": _(
+ "This is based on logs against this Vehicle. See timeline below for details"
+ ),
+ "fieldname": "license_plate",
+ "non_standard_fieldnames": {"Delivery Trip": "vehicle"},
+ "transactions": [{"items": ["Vehicle Log"]}, {"items": ["Delivery Trip"]}],
}
diff --git a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
index acd50f278cd..7c6fd8cb212 100644
--- a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
+++ b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
@@ -12,7 +12,9 @@ from erpnext.hr.doctype.vehicle_log.vehicle_log import make_expense_claim
class TestVehicleLog(unittest.TestCase):
def setUp(self):
- employee_id = frappe.db.sql("""select name from `tabEmployee` where name='testdriver@example.com'""")
+ employee_id = frappe.db.sql(
+ """select name from `tabEmployee` where name='testdriver@example.com'"""
+ )
self.employee_id = employee_id[0][0] if employee_id else None
if not self.employee_id:
@@ -27,11 +29,11 @@ class TestVehicleLog(unittest.TestCase):
def test_make_vehicle_log_and_syncing_of_odometer_value(self):
vehicle_log = make_vehicle_log(self.license_plate, self.employee_id)
- #checking value of vehicle odometer value on submit.
+ # checking value of vehicle odometer value on submit.
vehicle = frappe.get_doc("Vehicle", self.license_plate)
self.assertEqual(vehicle.last_odometer, vehicle_log.odometer)
- #checking value vehicle odometer on vehicle log cancellation.
+ # checking value vehicle odometer on vehicle log cancellation.
last_odometer = vehicle_log.last_odometer
current_odometer = vehicle_log.odometer
distance_travelled = current_odometer - last_odometer
@@ -48,7 +50,7 @@ class TestVehicleLog(unittest.TestCase):
expense_claim = make_expense_claim(vehicle_log.name)
fuel_expense = expense_claim.expenses[0].amount
- self.assertEqual(fuel_expense, 50*500)
+ self.assertEqual(fuel_expense, 50 * 500)
vehicle_log.cancel()
frappe.delete_doc("Expense Claim", expense_claim.name)
@@ -67,8 +69,9 @@ class TestVehicleLog(unittest.TestCase):
def get_vehicle(employee_id):
- license_plate=random_string(10).upper()
- vehicle = frappe.get_doc({
+ license_plate = random_string(10).upper()
+ vehicle = frappe.get_doc(
+ {
"doctype": "Vehicle",
"license_plate": cstr(license_plate),
"make": "Maruti",
@@ -79,8 +82,9 @@ def get_vehicle(employee_id):
"location": "Mumbai",
"chassis_no": "1234ABCD",
"uom": "Litre",
- "vehicle_value": flt(500000)
- })
+ "vehicle_value": flt(500000),
+ }
+ )
try:
vehicle.insert()
except frappe.DuplicateEntryError:
@@ -89,29 +93,37 @@ def get_vehicle(employee_id):
def make_vehicle_log(license_plate, employee_id, with_services=False):
- vehicle_log = frappe.get_doc({
- "doctype": "Vehicle Log",
- "license_plate": cstr(license_plate),
- "employee": employee_id,
- "date": nowdate(),
- "odometer": 5010,
- "fuel_qty": flt(50),
- "price": flt(500)
- })
+ vehicle_log = frappe.get_doc(
+ {
+ "doctype": "Vehicle Log",
+ "license_plate": cstr(license_plate),
+ "employee": employee_id,
+ "date": nowdate(),
+ "odometer": 5010,
+ "fuel_qty": flt(50),
+ "price": flt(500),
+ }
+ )
if with_services:
- vehicle_log.append("service_detail", {
- "service_item": "Oil Change",
- "type": "Inspection",
- "frequency": "Mileage",
- "expense_amount": flt(500)
- })
- vehicle_log.append("service_detail", {
- "service_item": "Wheels",
- "type": "Change",
- "frequency": "Half Yearly",
- "expense_amount": flt(1500)
- })
+ vehicle_log.append(
+ "service_detail",
+ {
+ "service_item": "Oil Change",
+ "type": "Inspection",
+ "frequency": "Mileage",
+ "expense_amount": flt(500),
+ },
+ )
+ vehicle_log.append(
+ "service_detail",
+ {
+ "service_item": "Wheels",
+ "type": "Change",
+ "frequency": "Half Yearly",
+ "expense_amount": flt(1500),
+ },
+ )
vehicle_log.save()
vehicle_log.submit()
diff --git a/erpnext/hr/doctype/vehicle_log/vehicle_log.py b/erpnext/hr/doctype/vehicle_log/vehicle_log.py
index e414141efb5..2c1d9a4efec 100644
--- a/erpnext/hr/doctype/vehicle_log/vehicle_log.py
+++ b/erpnext/hr/doctype/vehicle_log/vehicle_log.py
@@ -11,17 +11,24 @@ from frappe.utils import flt
class VehicleLog(Document):
def validate(self):
if flt(self.odometer) < flt(self.last_odometer):
- frappe.throw(_("Current Odometer Value should be greater than Last Odometer Value {0}").format(self.last_odometer))
+ frappe.throw(
+ _("Current Odometer Value should be greater than Last Odometer Value {0}").format(
+ self.last_odometer
+ )
+ )
def on_submit(self):
frappe.db.set_value("Vehicle", self.license_plate, "last_odometer", self.odometer)
def on_cancel(self):
distance_travelled = self.odometer - self.last_odometer
- if(distance_travelled > 0):
- updated_odometer_value = int(frappe.db.get_value("Vehicle", self.license_plate, "last_odometer")) - distance_travelled
+ if distance_travelled > 0:
+ updated_odometer_value = (
+ int(frappe.db.get_value("Vehicle", self.license_plate, "last_odometer")) - distance_travelled
+ )
frappe.db.set_value("Vehicle", self.license_plate, "last_odometer", updated_odometer_value)
+
@frappe.whitelist()
def make_expense_claim(docname):
expense_claim = frappe.db.exists("Expense Claim", {"vehicle_log": docname})
@@ -39,9 +46,8 @@ def make_expense_claim(docname):
exp_claim.employee = vehicle_log.employee
exp_claim.vehicle_log = vehicle_log.name
exp_claim.remark = _("Expense Claim for Vehicle Log {0}").format(vehicle_log.name)
- exp_claim.append("expenses", {
- "expense_date": vehicle_log.date,
- "description": _("Vehicle Expenses"),
- "amount": claim_amount
- })
+ exp_claim.append(
+ "expenses",
+ {"expense_date": vehicle_log.date, "description": _("Vehicle Expenses"), "amount": claim_amount},
+ )
return exp_claim.as_dict()
diff --git a/erpnext/hr/notification/training_feedback/training_feedback.py b/erpnext/hr/notification/training_feedback/training_feedback.py
index 19b550feea7..02e3e933330 100644
--- a/erpnext/hr/notification/training_feedback/training_feedback.py
+++ b/erpnext/hr/notification/training_feedback/training_feedback.py
@@ -1,5 +1,3 @@
-
-
def get_context(context):
# do your magic here
pass
diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.py b/erpnext/hr/notification/training_scheduled/training_scheduled.py
index 19b550feea7..02e3e933330 100644
--- a/erpnext/hr/notification/training_scheduled/training_scheduled.py
+++ b/erpnext/hr/notification/training_scheduled/training_scheduled.py
@@ -1,5 +1,3 @@
-
-
def get_context(context):
# do your magic here
pass
diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.py b/erpnext/hr/page/organizational_chart/organizational_chart.py
index 01d95a7051a..3674912dc06 100644
--- a/erpnext/hr/page/organizational_chart/organizational_chart.py
+++ b/erpnext/hr/page/organizational_chart/organizational_chart.py
@@ -1,29 +1,29 @@
-
import frappe
@frappe.whitelist()
def get_children(parent=None, company=None, exclude_node=None):
- filters = [['status', '!=', 'Left']]
- if company and company != 'All Companies':
- filters.append(['company', '=', company])
+ filters = [["status", "!=", "Left"]]
+ if company and company != "All Companies":
+ filters.append(["company", "=", company])
if parent and company and parent != company:
- filters.append(['reports_to', '=', parent])
+ filters.append(["reports_to", "=", parent])
else:
- filters.append(['reports_to', '=', ''])
+ filters.append(["reports_to", "=", ""])
if exclude_node:
- filters.append(['name', '!=', exclude_node])
+ filters.append(["name", "!=", exclude_node])
- employees = frappe.get_list('Employee',
- fields=['employee_name as name', 'name as id', 'reports_to', 'image', 'designation as title'],
+ employees = frappe.get_list(
+ "Employee",
+ fields=["employee_name as name", "name as id", "reports_to", "image", "designation as title"],
filters=filters,
- order_by='name'
+ order_by="name",
)
for employee in employees:
- is_expandable = frappe.db.count('Employee', filters={'reports_to': employee.get('id')})
+ is_expandable = frappe.db.count("Employee", filters={"reports_to": employee.get("id")})
employee.connections = get_connections(employee.id)
employee.expandable = 1 if is_expandable else 0
@@ -33,16 +33,12 @@ def get_children(parent=None, company=None, exclude_node=None):
def get_connections(employee):
num_connections = 0
- nodes_to_expand = frappe.get_list('Employee', filters=[
- ['reports_to', '=', employee]
- ])
+ nodes_to_expand = frappe.get_list("Employee", filters=[["reports_to", "=", employee]])
num_connections += len(nodes_to_expand)
while nodes_to_expand:
parent = nodes_to_expand.pop(0)
- descendants = frappe.get_list('Employee', filters=[
- ['reports_to', '=', parent.name]
- ])
+ descendants = frappe.get_list("Employee", filters=[["reports_to", "=", parent.name]])
num_connections += len(descendants)
nodes_to_expand.extend(descendants)
diff --git a/erpnext/hr/page/team_updates/team_updates.py b/erpnext/hr/page/team_updates/team_updates.py
index 126ed898c97..c1fcb735850 100644
--- a/erpnext/hr/page/team_updates/team_updates.py
+++ b/erpnext/hr/page/team_updates/team_updates.py
@@ -1,19 +1,23 @@
-
import frappe
from email_reply_parser import EmailReplyParser
@frappe.whitelist()
def get_data(start=0):
- #frappe.only_for('Employee', 'System Manager')
- data = frappe.get_all('Communication',
- fields=('content', 'text_content', 'sender', 'creation'),
- filters=dict(reference_doctype='Daily Work Summary'),
- order_by='creation desc', limit=40, start=start)
+ # frappe.only_for('Employee', 'System Manager')
+ data = frappe.get_all(
+ "Communication",
+ fields=("content", "text_content", "sender", "creation"),
+ filters=dict(reference_doctype="Daily Work Summary"),
+ order_by="creation desc",
+ limit=40,
+ start=start,
+ )
for d in data:
- d.sender_name = frappe.db.get_value("Employee", {"user_id": d.sender},
- "employee_name") or d.sender
+ d.sender_name = (
+ frappe.db.get_value("Employee", {"user_id": d.sender}, "employee_name") or d.sender
+ )
if d.text_content:
d.content = frappe.utils.md_to_html(EmailReplyParser.parse_reply(d.text_content))
diff --git a/erpnext/hr/report/daily_work_summary_replies/daily_work_summary_replies.py b/erpnext/hr/report/daily_work_summary_replies/daily_work_summary_replies.py
index 63764bb8a9c..d93688a4925 100644
--- a/erpnext/hr/report/daily_work_summary_replies/daily_work_summary_replies.py
+++ b/erpnext/hr/report/daily_work_summary_replies/daily_work_summary_replies.py
@@ -9,51 +9,53 @@ from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_user_em
def execute(filters=None):
- if not filters.group: return [], []
+ if not filters.group:
+ return [], []
columns, data = get_columns(), get_data(filters)
return columns, data
+
def get_columns(filters=None):
columns = [
- {
- "label": _("User"),
- "fieldname": "user",
- "fieldtype": "Data",
- "width": 300
- },
+ {"label": _("User"), "fieldname": "user", "fieldtype": "Data", "width": 300},
{
"label": _("Replies"),
"fieldname": "count",
"fieldtype": "data",
"width": 100,
- "align": 'right',
+ "align": "right",
},
{
"label": _("Total"),
"fieldname": "total",
"fieldtype": "data",
"width": 100,
- "align": 'right',
- }
+ "align": "right",
+ },
]
return columns
+
def get_data(filters):
- daily_summary_emails = frappe.get_all('Daily Work Summary',
- fields=["name"],
- filters=[["creation","Between", filters.range]])
- daily_summary_emails = [d.get('name') for d in daily_summary_emails]
- replies = frappe.get_all('Communication',
- fields=['content', 'text_content', 'sender'],
- filters=[['reference_doctype','=', 'Daily Work Summary'],
- ['reference_name', 'in', daily_summary_emails],
- ['communication_type', '=', 'Communication'],
- ['sent_or_received', '=', 'Received']],
- order_by='creation asc')
+ daily_summary_emails = frappe.get_all(
+ "Daily Work Summary", fields=["name"], filters=[["creation", "Between", filters.range]]
+ )
+ daily_summary_emails = [d.get("name") for d in daily_summary_emails]
+ replies = frappe.get_all(
+ "Communication",
+ fields=["content", "text_content", "sender"],
+ filters=[
+ ["reference_doctype", "=", "Daily Work Summary"],
+ ["reference_name", "in", daily_summary_emails],
+ ["communication_type", "=", "Communication"],
+ ["sent_or_received", "=", "Received"],
+ ],
+ order_by="creation asc",
+ )
data = []
total = len(daily_summary_emails)
for user in get_user_emails_from_group(filters.group):
- user_name = frappe.get_value('User', user, 'full_name')
+ user_name = frappe.get_value("User", user, "full_name")
count = len([d for d in replies if d.sender == user])
data.append([user_name, count, total])
return data
diff --git a/erpnext/hr/report/employee_advance_summary/employee_advance_summary.py b/erpnext/hr/report/employee_advance_summary/employee_advance_summary.py
index 62b83f26a61..29532f7680a 100644
--- a/erpnext/hr/report/employee_advance_summary/employee_advance_summary.py
+++ b/erpnext/hr/report/employee_advance_summary/employee_advance_summary.py
@@ -7,7 +7,8 @@ from frappe import _, msgprint
def execute(filters=None):
- if not filters: filters = {}
+ if not filters:
+ filters = {}
advances_list = get_advances(filters)
columns = get_columns()
@@ -18,8 +19,16 @@ def execute(filters=None):
data = []
for advance in advances_list:
- row = [advance.name, advance.employee, advance.company, advance.posting_date,
- advance.advance_amount, advance.paid_amount, advance.claimed_amount, advance.status]
+ row = [
+ advance.name,
+ advance.employee,
+ advance.company,
+ advance.posting_date,
+ advance.advance_amount,
+ advance.paid_amount,
+ advance.claimed_amount,
+ advance.status,
+ ]
data.append(row)
return columns, data
@@ -32,54 +41,40 @@ def get_columns():
"fieldname": "title",
"fieldtype": "Link",
"options": "Employee Advance",
- "width": 120
+ "width": 120,
},
{
"label": _("Employee"),
"fieldname": "employee",
"fieldtype": "Link",
"options": "Employee",
- "width": 120
+ "width": 120,
},
{
"label": _("Company"),
"fieldname": "company",
"fieldtype": "Link",
"options": "Company",
- "width": 120
- },
- {
- "label": _("Posting Date"),
- "fieldname": "posting_date",
- "fieldtype": "Date",
- "width": 120
+ "width": 120,
},
+ {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
{
"label": _("Advance Amount"),
"fieldname": "advance_amount",
"fieldtype": "Currency",
- "width": 120
- },
- {
- "label": _("Paid Amount"),
- "fieldname": "paid_amount",
- "fieldtype": "Currency",
- "width": 120
+ "width": 120,
},
+ {"label": _("Paid Amount"), "fieldname": "paid_amount", "fieldtype": "Currency", "width": 120},
{
"label": _("Claimed Amount"),
"fieldname": "claimed_amount",
"fieldtype": "Currency",
- "width": 120
+ "width": 120,
},
- {
- "label": _("Status"),
- "fieldname": "status",
- "fieldtype": "Data",
- "width": 120
- }
+ {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 120},
]
+
def get_conditions(filters):
conditions = ""
@@ -96,10 +91,15 @@ def get_conditions(filters):
return conditions
+
def get_advances(filters):
conditions = get_conditions(filters)
- return frappe.db.sql("""select name, employee, paid_amount, status, advance_amount, claimed_amount, company,
+ return frappe.db.sql(
+ """select name, employee, paid_amount, status, advance_amount, claimed_amount, company,
posting_date, purpose
from `tabEmployee Advance`
- where docstatus<2 %s order by posting_date, name desc""" %
- conditions, filters, as_dict=1)
+ where docstatus<2 %s order by posting_date, name desc"""
+ % conditions,
+ filters,
+ as_dict=1,
+ )
diff --git a/erpnext/hr/report/employee_analytics/employee_analytics.py b/erpnext/hr/report/employee_analytics/employee_analytics.py
index 3a75276cb07..12be156ab9b 100644
--- a/erpnext/hr/report/employee_analytics/employee_analytics.py
+++ b/erpnext/hr/report/employee_analytics/employee_analytics.py
@@ -7,10 +7,11 @@ from frappe import _
def execute(filters=None):
- if not filters: filters = {}
+ if not filters:
+ filters = {}
if not filters["company"]:
- frappe.throw(_('{0} is mandatory').format(_('Company')))
+ frappe.throw(_("{0} is mandatory").format(_("Company")))
columns = get_columns()
employees = get_employees(filters)
@@ -20,28 +21,41 @@ def execute(filters=None):
for department in parameters_result:
parameters.append(department)
- chart = get_chart_data(parameters,employees, filters)
+ chart = get_chart_data(parameters, employees, filters)
return columns, employees, None, chart
+
def get_columns():
return [
- _("Employee") + ":Link/Employee:120", _("Name") + ":Data:200", _("Date of Birth")+ ":Date:100",
- _("Branch") + ":Link/Branch:120", _("Department") + ":Link/Department:120",
- _("Designation") + ":Link/Designation:120", _("Gender") + "::100", _("Company") + ":Link/Company:120"
+ _("Employee") + ":Link/Employee:120",
+ _("Name") + ":Data:200",
+ _("Date of Birth") + ":Date:100",
+ _("Branch") + ":Link/Branch:120",
+ _("Department") + ":Link/Department:120",
+ _("Designation") + ":Link/Designation:120",
+ _("Gender") + "::100",
+ _("Company") + ":Link/Company:120",
]
-def get_conditions(filters):
- conditions = " and "+filters.get("parameter").lower().replace(" ","_")+" IS NOT NULL "
- if filters.get("company"): conditions += " and company = '%s'" % \
- filters["company"].replace("'", "\\'")
+def get_conditions(filters):
+ conditions = " and " + filters.get("parameter").lower().replace(" ", "_") + " IS NOT NULL "
+
+ if filters.get("company"):
+ conditions += " and company = '%s'" % filters["company"].replace("'", "\\'")
return conditions
+
def get_employees(filters):
conditions = get_conditions(filters)
- return frappe.db.sql("""select name, employee_name, date_of_birth,
+ return frappe.db.sql(
+ """select name, employee_name, date_of_birth,
branch, department, designation,
- gender, company from `tabEmployee` where status = 'Active' %s""" % conditions, as_list=1)
+ gender, company from `tabEmployee` where status = 'Active' %s"""
+ % conditions,
+ as_list=1,
+ )
+
def get_parameters(filters):
if filters.get("parameter") == "Grade":
@@ -49,36 +63,37 @@ def get_parameters(filters):
else:
parameter = filters.get("parameter")
- return frappe.db.sql("""select name from `tab"""+ parameter +"""` """, as_list=1)
+ return frappe.db.sql("""select name from `tab""" + parameter + """` """, as_list=1)
-def get_chart_data(parameters,employees, filters):
+
+def get_chart_data(parameters, employees, filters):
if not parameters:
parameters = []
datasets = []
- parameter_field_name = filters.get("parameter").lower().replace(" ","_")
+ parameter_field_name = filters.get("parameter").lower().replace(" ", "_")
label = []
for parameter in parameters:
if parameter:
- total_employee = frappe.db.sql("""select count(*) from
- `tabEmployee` where """+
- parameter_field_name + """ = %s and company = %s""" ,( parameter[0], filters.get("company")), as_list=1)
+ total_employee = frappe.db.sql(
+ """select count(*) from
+ `tabEmployee` where """
+ + parameter_field_name
+ + """ = %s and company = %s""",
+ (parameter[0], filters.get("company")),
+ as_list=1,
+ )
if total_employee[0][0]:
label.append(parameter)
datasets.append(total_employee[0][0])
- values = [ value for value in datasets if value !=0]
+ values = [value for value in datasets if value != 0]
- total_employee = frappe.db.count('Employee', {'status':'Active'})
+ total_employee = frappe.db.count("Employee", {"status": "Active"})
others = total_employee - sum(values)
label.append(["Not Set"])
values.append(others)
- chart = {
- "data": {
- 'labels': label,
- 'datasets': [{'name': 'Employees','values': values}]
- }
- }
+ chart = {"data": {"labels": label, "datasets": [{"name": "Employees", "values": values}]}}
chart["type"] = "donut"
return chart
diff --git a/erpnext/hr/report/employee_birthday/employee_birthday.py b/erpnext/hr/report/employee_birthday/employee_birthday.py
index cec5a48c199..a6a13d8a4d7 100644
--- a/erpnext/hr/report/employee_birthday/employee_birthday.py
+++ b/erpnext/hr/report/employee_birthday/employee_birthday.py
@@ -7,34 +7,59 @@ from frappe import _
def execute(filters=None):
- if not filters: filters = {}
+ if not filters:
+ filters = {}
columns = get_columns()
data = get_employees(filters)
return columns, data
+
def get_columns():
return [
- _("Employee") + ":Link/Employee:120", _("Name") + ":Data:200", _("Date of Birth")+ ":Date:100",
- _("Branch") + ":Link/Branch:120", _("Department") + ":Link/Department:120",
- _("Designation") + ":Link/Designation:120", _("Gender") + "::60", _("Company") + ":Link/Company:120"
+ _("Employee") + ":Link/Employee:120",
+ _("Name") + ":Data:200",
+ _("Date of Birth") + ":Date:100",
+ _("Branch") + ":Link/Branch:120",
+ _("Department") + ":Link/Department:120",
+ _("Designation") + ":Link/Designation:120",
+ _("Gender") + "::60",
+ _("Company") + ":Link/Company:120",
]
+
def get_employees(filters):
conditions = get_conditions(filters)
- return frappe.db.sql("""select name, employee_name, date_of_birth,
+ return frappe.db.sql(
+ """select name, employee_name, date_of_birth,
branch, department, designation,
- gender, company from tabEmployee where status = 'Active' %s""" % conditions, as_list=1)
+ gender, company from tabEmployee where status = 'Active' %s"""
+ % conditions,
+ as_list=1,
+ )
+
def get_conditions(filters):
conditions = ""
if filters.get("month"):
- month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov",
- "Dec"].index(filters["month"]) + 1
+ month = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ].index(filters["month"]) + 1
conditions += " and month(date_of_birth) = '%s'" % month
- if filters.get("company"): conditions += " and company = '%s'" % \
- filters["company"].replace("'", "\\'")
+ if filters.get("company"):
+ conditions += " and company = '%s'" % filters["company"].replace("'", "\\'")
return conditions
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
index b375b18b079..ca352f197dc 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -3,18 +3,22 @@
from itertools import groupby
+from typing import Dict, List, Optional, Tuple
import frappe
from frappe import _
-from frappe.utils import add_days
+from frappe.utils import add_days, getdate
+from erpnext.hr.doctype.leave_allocation.leave_allocation import get_previous_allocation
from erpnext.hr.doctype.leave_application.leave_application import (
get_leave_balance_on,
get_leaves_for_period,
)
+Filters = frappe._dict
-def execute(filters=None):
+
+def execute(filters: Optional[Filters] = None) -> Tuple:
if filters.to_date <= filters.from_date:
frappe.throw(_('"From Date" can not be greater than or equal to "To Date"'))
@@ -23,146 +27,177 @@ def execute(filters=None):
charts = get_chart_data(data)
return columns, data, None, charts
-def get_columns():
- columns = [{
- 'label': _('Leave Type'),
- 'fieldtype': 'Link',
- 'fieldname': 'leave_type',
- 'width': 200,
- 'options': 'Leave Type'
- }, {
- 'label': _('Employee'),
- 'fieldtype': 'Link',
- 'fieldname': 'employee',
- 'width': 100,
- 'options': 'Employee'
- }, {
- 'label': _('Employee Name'),
- 'fieldtype': 'Dynamic Link',
- 'fieldname': 'employee_name',
- 'width': 100,
- 'options': 'employee'
- }, {
- 'label': _('Opening Balance'),
- 'fieldtype': 'float',
- 'fieldname': 'opening_balance',
- 'width': 130,
- }, {
- 'label': _('Leave Allocated'),
- 'fieldtype': 'float',
- 'fieldname': 'leaves_allocated',
- 'width': 130,
- }, {
- 'label': _('Leave Taken'),
- 'fieldtype': 'float',
- 'fieldname': 'leaves_taken',
- 'width': 130,
- }, {
- 'label': _('Leave Expired'),
- 'fieldtype': 'float',
- 'fieldname': 'leaves_expired',
- 'width': 130,
- }, {
- 'label': _('Closing Balance'),
- 'fieldtype': 'float',
- 'fieldname': 'closing_balance',
- 'width': 130,
- }]
- return columns
+def get_columns() -> List[Dict]:
+ return [
+ {
+ "label": _("Leave Type"),
+ "fieldtype": "Link",
+ "fieldname": "leave_type",
+ "width": 200,
+ "options": "Leave Type",
+ },
+ {
+ "label": _("Employee"),
+ "fieldtype": "Link",
+ "fieldname": "employee",
+ "width": 100,
+ "options": "Employee",
+ },
+ {
+ "label": _("Employee Name"),
+ "fieldtype": "Dynamic Link",
+ "fieldname": "employee_name",
+ "width": 100,
+ "options": "employee",
+ },
+ {
+ "label": _("Opening Balance"),
+ "fieldtype": "float",
+ "fieldname": "opening_balance",
+ "width": 150,
+ },
+ {
+ "label": _("New Leave(s) Allocated"),
+ "fieldtype": "float",
+ "fieldname": "leaves_allocated",
+ "width": 200,
+ },
+ {
+ "label": _("Leave(s) Taken"),
+ "fieldtype": "float",
+ "fieldname": "leaves_taken",
+ "width": 150,
+ },
+ {
+ "label": _("Leave(s) Expired"),
+ "fieldtype": "float",
+ "fieldname": "leaves_expired",
+ "width": 150,
+ },
+ {
+ "label": _("Closing Balance"),
+ "fieldtype": "float",
+ "fieldname": "closing_balance",
+ "width": 150,
+ },
+ ]
-def get_data(filters):
- leave_types = frappe.db.get_list('Leave Type', pluck='name', order_by='name')
+
+def get_data(filters: Filters) -> List:
+ leave_types = frappe.db.get_list("Leave Type", pluck="name", order_by="name")
conditions = get_conditions(filters)
user = frappe.session.user
- department_approver_map = get_department_leave_approver_map(filters.get('department'))
+ department_approver_map = get_department_leave_approver_map(filters.get("department"))
- active_employees = frappe.get_list('Employee',
+ active_employees = frappe.get_list(
+ "Employee",
filters=conditions,
- fields=['name', 'employee_name', 'department', 'user_id', 'leave_approver'])
+ fields=["name", "employee_name", "department", "user_id", "leave_approver"],
+ )
data = []
for leave_type in leave_types:
if len(active_employees) > 1:
- data.append({
- 'leave_type': leave_type
- })
+ data.append({"leave_type": leave_type})
else:
- row = frappe._dict({
- 'leave_type': leave_type
- })
+ row = frappe._dict({"leave_type": leave_type})
for employee in active_employees:
- leave_approvers = department_approver_map.get(employee.department_name, []).append(employee.leave_approver)
+ leave_approvers = department_approver_map.get(employee.department_name, []).append(
+ employee.leave_approver
+ )
- if (leave_approvers and len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) \
- or ("HR Manager" in frappe.get_roles(user)):
+ if (
+ (leave_approvers and len(leave_approvers) and user in leave_approvers)
+ or (user in ["Administrator", employee.user_id])
+ or ("HR Manager" in frappe.get_roles(user))
+ ):
if len(active_employees) > 1:
row = frappe._dict()
- row.employee = employee.name,
+ row.employee = employee.name
row.employee_name = employee.employee_name
- leaves_taken = get_leaves_for_period(employee.name, leave_type,
- filters.from_date, filters.to_date) * -1
+ leaves_taken = (
+ get_leaves_for_period(employee.name, leave_type, filters.from_date, filters.to_date) * -1
+ )
- new_allocation, expired_leaves = get_allocated_and_expired_leaves(filters.from_date, filters.to_date, employee.name, leave_type)
-
-
- opening = get_leave_balance_on(employee.name, leave_type, add_days(filters.from_date, -1)) #allocation boundary condition
+ new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves(
+ filters.from_date, filters.to_date, employee.name, leave_type
+ )
+ opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves)
row.leaves_allocated = new_allocation
- row.leaves_expired = expired_leaves - leaves_taken if expired_leaves - leaves_taken > 0 else 0
+ row.leaves_expired = expired_leaves
row.opening_balance = opening
row.leaves_taken = leaves_taken
# not be shown on the basis of days left it create in user mind for carry_forward leave
- row.closing_balance = (new_allocation + opening - (row.leaves_expired + leaves_taken))
+ row.closing_balance = new_allocation + opening - (row.leaves_expired + leaves_taken)
row.indent = 1
data.append(row)
return data
-def get_conditions(filters):
- conditions={
- 'status': 'Active',
+
+def get_opening_balance(
+ employee: str, leave_type: str, filters: Filters, carry_forwarded_leaves: float
+) -> float:
+ # allocation boundary condition
+ # opening balance is the closing leave balance 1 day before the filter start date
+ opening_balance_date = add_days(filters.from_date, -1)
+ allocation = get_previous_allocation(filters.from_date, leave_type, employee)
+
+ if (
+ allocation
+ and allocation.get("to_date")
+ and opening_balance_date
+ and getdate(allocation.get("to_date")) == getdate(opening_balance_date)
+ ):
+ # if opening balance date is same as the previous allocation's expiry
+ # then opening balance should only consider carry forwarded leaves
+ opening_balance = carry_forwarded_leaves
+ else:
+ # else directly get leave balance on the previous day
+ opening_balance = get_leave_balance_on(employee, leave_type, opening_balance_date)
+
+ return opening_balance
+
+
+def get_conditions(filters: Filters) -> Dict:
+ conditions = {
+ "status": "Active",
}
- if filters.get('employee'):
- conditions['name'] = filters.get('employee')
+ if filters.get("employee"):
+ conditions["name"] = filters.get("employee")
- if filters.get('company'):
- conditions['company'] = filters.get('company')
+ if filters.get("company"):
+ conditions["company"] = filters.get("company")
- if filters.get('department'):
- conditions['department'] = filters.get('department')
+ if filters.get("department"):
+ conditions["department"] = filters.get("department")
return conditions
-def get_department_leave_approver_map(department=None):
+def get_department_leave_approver_map(department: Optional[str] = None):
# get current department and all its child
- department_list = frappe.get_list('Department',
- filters={
- 'disabled': 0
- },
- or_filters={
- 'name': department,
- 'parent_department': department
- },
- fields=['name'],
- pluck='name'
- )
+ department_list = frappe.get_list(
+ "Department",
+ filters={"disabled": 0},
+ or_filters={"name": department, "parent_department": department},
+ pluck="name",
+ )
# retrieve approvers list from current department and from its subsequent child departments
- approver_list = frappe.get_all('Department Approver',
- filters={
- 'parentfield': 'leave_approvers',
- 'parent': ('in', department_list)
- },
- fields=['parent', 'approver'],
- as_list=1
- )
+ approver_list = frappe.get_all(
+ "Department Approver",
+ filters={"parentfield": "leave_approvers", "parent": ("in", department_list)},
+ fields=["parent", "approver"],
+ as_list=True,
+ )
approvers = {}
@@ -171,73 +206,100 @@ def get_department_leave_approver_map(department=None):
return approvers
-def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
-
- from frappe.utils import getdate
+def get_allocated_and_expired_leaves(
+ from_date: str, to_date: str, employee: str, leave_type: str
+) -> Tuple[float, float, float]:
new_allocation = 0
expired_leaves = 0
+ carry_forwarded_leaves = 0
- records= frappe.db.sql("""
- SELECT
- employee, leave_type, from_date, to_date, leaves, transaction_name,
- transaction_type, is_carry_forward, is_expired
- FROM `tabLeave Ledger Entry`
- WHERE employee=%(employee)s AND leave_type=%(leave_type)s
- AND docstatus=1
- AND transaction_type = 'Leave Allocation'
- AND (from_date between %(from_date)s AND %(to_date)s
- OR to_date between %(from_date)s AND %(to_date)s
- OR (from_date < %(from_date)s AND to_date > %(to_date)s))
- """, {
- "from_date": from_date,
- "to_date": to_date,
- "employee": employee,
- "leave_type": leave_type
- }, as_dict=1)
+ records = get_leave_ledger_entries(from_date, to_date, employee, leave_type)
for record in records:
+ # new allocation records with `is_expired=1` are created when leave expires
+ # these new records should not be considered, else it leads to negative leave balance
+ if record.is_expired:
+ continue
+
if record.to_date < getdate(to_date):
+ # leave allocations ending before to_date, reduce leaves taken within that period
+ # since they are already used, they won't expire
expired_leaves += record.leaves
+ expired_leaves += get_leaves_for_period(employee, leave_type, record.from_date, record.to_date)
if record.from_date >= getdate(from_date):
- new_allocation += record.leaves
+ if record.is_carry_forward:
+ carry_forwarded_leaves += record.leaves
+ else:
+ new_allocation += record.leaves
- return new_allocation, expired_leaves
+ return new_allocation, expired_leaves, carry_forwarded_leaves
-def get_chart_data(data):
+
+def get_leave_ledger_entries(
+ from_date: str, to_date: str, employee: str, leave_type: str
+) -> List[Dict]:
+ ledger = frappe.qb.DocType("Leave Ledger Entry")
+ records = (
+ frappe.qb.from_(ledger)
+ .select(
+ ledger.employee,
+ ledger.leave_type,
+ ledger.from_date,
+ ledger.to_date,
+ ledger.leaves,
+ ledger.transaction_name,
+ ledger.transaction_type,
+ ledger.is_carry_forward,
+ ledger.is_expired,
+ )
+ .where(
+ (ledger.docstatus == 1)
+ & (ledger.transaction_type == "Leave Allocation")
+ & (ledger.employee == employee)
+ & (ledger.leave_type == leave_type)
+ & (
+ (ledger.from_date[from_date:to_date])
+ | (ledger.to_date[from_date:to_date])
+ | ((ledger.from_date < from_date) & (ledger.to_date > to_date))
+ )
+ )
+ ).run(as_dict=True)
+
+ return records
+
+
+def get_chart_data(data: List) -> Dict:
labels = []
datasets = []
employee_data = data
- if data and data[0].get('employee_name'):
+ if data and data[0].get("employee_name"):
get_dataset_for_chart(employee_data, datasets, labels)
chart = {
- 'data': {
- 'labels': labels,
- 'datasets': datasets
- },
- 'type': 'bar',
- 'colors': ['#456789', '#EE8888', '#7E77BF']
+ "data": {"labels": labels, "datasets": datasets},
+ "type": "bar",
+ "colors": ["#456789", "#EE8888", "#7E77BF"],
}
return chart
-def get_dataset_for_chart(employee_data, datasets, labels):
- leaves = []
- employee_data = sorted(employee_data, key=lambda k: k['employee_name'])
- for key, group in groupby(employee_data, lambda x: x['employee_name']):
+def get_dataset_for_chart(employee_data: List, datasets: List, labels: List) -> List:
+ leaves = []
+ employee_data = sorted(employee_data, key=lambda k: k["employee_name"])
+
+ for key, group in groupby(employee_data, lambda x: x["employee_name"]):
for grp in group:
if grp.closing_balance:
- leaves.append(frappe._dict({
- 'leave_type': grp.leave_type,
- 'closing_balance': grp.closing_balance
- }))
+ leaves.append(
+ frappe._dict({"leave_type": grp.leave_type, "closing_balance": grp.closing_balance})
+ )
if leaves:
labels.append(key)
for leave in leaves:
- datasets.append({'name': leave.leave_type, 'values': [leave.closing_balance]})
+ datasets.append({"name": leave.leave_type, "values": [leave.closing_balance]})
diff --git a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
new file mode 100644
index 00000000000..dc0f4d2c944
--- /dev/null
+++ b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
@@ -0,0 +1,209 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+
+import unittest
+
+import frappe
+from frappe.utils import add_days, add_months, flt, get_year_ending, get_year_start, getdate
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
+from erpnext.hr.doctype.leave_application.test_leave_application import (
+ get_first_sunday,
+ make_allocation_record,
+)
+from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
+from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
+from erpnext.hr.report.employee_leave_balance.employee_leave_balance import execute
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
+ make_holiday_list,
+ make_leave_application,
+)
+
+test_records = frappe.get_test_records("Leave Type")
+
+
+class TestEmployeeLeaveBalance(unittest.TestCase):
+ def setUp(self):
+ for dt in [
+ "Leave Application",
+ "Leave Allocation",
+ "Salary Slip",
+ "Leave Ledger Entry",
+ "Leave Type",
+ ]:
+ frappe.db.delete(dt)
+
+ frappe.set_user("Administrator")
+
+ self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company")
+
+ self.date = getdate()
+ self.year_start = getdate(get_year_start(self.date))
+ self.mid_year = add_months(self.year_start, 6)
+ self.year_end = getdate(get_year_ending(self.date))
+
+ self.holiday_list = make_holiday_list(
+ "_Test Emp Balance Holiday List", self.year_start, self.year_end
+ )
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ @set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
+ def test_employee_leave_balance(self):
+ frappe.get_doc(test_records[0]).insert()
+
+ # 5 leaves
+ allocation1 = make_allocation_record(
+ employee=self.employee_id,
+ from_date=add_days(self.year_start, -11),
+ to_date=add_days(self.year_start, -1),
+ leaves=5,
+ )
+ # 30 leaves
+ allocation2 = make_allocation_record(
+ employee=self.employee_id, from_date=self.year_start, to_date=self.year_end
+ )
+ # expires 5 leaves
+ process_expired_allocation()
+
+ # 4 days leave
+ first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
+ leave_application = make_leave_application(
+ self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), "_Test Leave Type"
+ )
+ leave_application.reload()
+
+ filters = frappe._dict(
+ {
+ "from_date": allocation1.from_date,
+ "to_date": allocation2.to_date,
+ "employee": self.employee_id,
+ }
+ )
+
+ report = execute(filters)
+
+ expected_data = [
+ {
+ "leave_type": "_Test Leave Type",
+ "employee": self.employee_id,
+ "employee_name": "test_emp_leave_balance@example.com",
+ "leaves_allocated": flt(allocation1.new_leaves_allocated + allocation2.new_leaves_allocated),
+ "leaves_expired": flt(allocation1.new_leaves_allocated),
+ "opening_balance": flt(0),
+ "leaves_taken": flt(leave_application.total_leave_days),
+ "closing_balance": flt(allocation2.new_leaves_allocated - leave_application.total_leave_days),
+ "indent": 1,
+ }
+ ]
+
+ self.assertEqual(report[1], expected_data)
+
+ @set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
+ def test_opening_balance_on_alloc_boundary_dates(self):
+ frappe.get_doc(test_records[0]).insert()
+
+ # 30 leaves allocated
+ allocation1 = make_allocation_record(
+ employee=self.employee_id, from_date=self.year_start, to_date=self.year_end
+ )
+ # 4 days leave application in the first allocation
+ first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
+ leave_application = make_leave_application(
+ self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), "_Test Leave Type"
+ )
+ leave_application.reload()
+
+ # Case 1: opening balance for first alloc boundary
+ filters = frappe._dict(
+ {"from_date": self.year_start, "to_date": self.year_end, "employee": self.employee_id}
+ )
+ report = execute(filters)
+ self.assertEqual(report[1][0].opening_balance, 0)
+
+ # Case 2: opening balance after leave application date
+ filters = frappe._dict(
+ {
+ "from_date": add_days(leave_application.to_date, 1),
+ "to_date": self.year_end,
+ "employee": self.employee_id,
+ }
+ )
+ report = execute(filters)
+ self.assertEqual(
+ report[1][0].opening_balance,
+ (allocation1.new_leaves_allocated - leave_application.total_leave_days),
+ )
+
+ # Case 3: leave balance shows actual balance and not consumption balance as per remaining days near alloc end date
+ # eg: 3 days left for alloc to end, leave balance should still be 26 and not 3
+ filters = frappe._dict(
+ {
+ "from_date": add_days(self.year_end, -3),
+ "to_date": self.year_end,
+ "employee": self.employee_id,
+ }
+ )
+ report = execute(filters)
+ self.assertEqual(
+ report[1][0].opening_balance,
+ (allocation1.new_leaves_allocated - leave_application.total_leave_days),
+ )
+
+ @set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
+ def test_opening_balance_considers_carry_forwarded_leaves(self):
+ leave_type = create_leave_type(leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1)
+ leave_type.insert()
+
+ # 30 leaves allocated for first half of the year
+ allocation1 = make_allocation_record(
+ employee=self.employee_id,
+ from_date=self.year_start,
+ to_date=self.mid_year,
+ leave_type=leave_type.name,
+ )
+ # 4 days leave application in the first allocation
+ first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
+ leave_application = make_leave_application(
+ self.employee_id, first_sunday, add_days(first_sunday, 3), leave_type.name
+ )
+ leave_application.reload()
+ # 30 leaves allocated for second half of the year + carry forward leaves (26) from the previous allocation
+ allocation2 = make_allocation_record(
+ employee=self.employee_id,
+ from_date=add_days(self.mid_year, 1),
+ to_date=self.year_end,
+ carry_forward=True,
+ leave_type=leave_type.name,
+ )
+
+ # Case 1: carry forwarded leaves considered in opening balance for second alloc
+ filters = frappe._dict(
+ {
+ "from_date": add_days(self.mid_year, 1),
+ "to_date": self.year_end,
+ "employee": self.employee_id,
+ }
+ )
+ report = execute(filters)
+ # available leaves from old alloc
+ opening_balance = allocation1.new_leaves_allocated - leave_application.total_leave_days
+ self.assertEqual(report[1][0].opening_balance, opening_balance)
+
+ # Case 2: opening balance one day after alloc boundary = carry forwarded leaves + new leaves alloc
+ filters = frappe._dict(
+ {
+ "from_date": add_days(self.mid_year, 2),
+ "to_date": self.year_end,
+ "employee": self.employee_id,
+ }
+ )
+ report = execute(filters)
+ # available leaves from old alloc
+ opening_balance = allocation2.new_leaves_allocated + (
+ allocation1.new_leaves_allocated - leave_application.total_leave_days
+ )
+ self.assertEqual(report[1][0].opening_balance, opening_balance)
diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
index 936184a9c0d..f0087eb1b9e 100644
--- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
+++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
@@ -19,11 +19,12 @@ def execute(filters=None):
return columns, data
+
def get_columns(leave_types):
columns = [
_("Employee") + ":Link.Employee:150",
_("Employee Name") + "::200",
- _("Department") +"::150"
+ _("Department") + "::150",
]
for leave_type in leave_types:
@@ -31,6 +32,7 @@ def get_columns(leave_types):
return columns
+
def get_conditions(filters):
conditions = {
"status": "Active",
@@ -43,15 +45,18 @@ def get_conditions(filters):
return conditions
+
def get_data(filters, leave_types):
user = frappe.session.user
conditions = get_conditions(filters)
- active_employees = frappe.get_all("Employee",
+ active_employees = frappe.get_all(
+ "Employee",
filters=conditions,
- fields=["name", "employee_name", "department", "user_id", "leave_approver"])
+ fields=["name", "employee_name", "department", "user_id", "leave_approver"],
+ )
- department_approver_map = get_department_leave_approver_map(filters.get('department'))
+ department_approver_map = get_department_leave_approver_map(filters.get("department"))
data = []
for employee in active_employees:
@@ -59,14 +64,18 @@ def get_data(filters, leave_types):
if employee.leave_approver:
leave_approvers.append(employee.leave_approver)
- if (len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) or ("HR Manager" in frappe.get_roles(user)):
+ if (
+ (len(leave_approvers) and user in leave_approvers)
+ or (user in ["Administrator", employee.user_id])
+ or ("HR Manager" in frappe.get_roles(user))
+ ):
row = [employee.name, employee.employee_name, employee.department]
available_leave = get_leave_details(employee.name, filters.date)
for leave_type in leave_types:
remaining = 0
if leave_type in available_leave["leave_allocation"]:
# opening balance
- remaining = available_leave["leave_allocation"][leave_type]['remaining_leaves']
+ remaining = available_leave["leave_allocation"][leave_type]["remaining_leaves"]
row += [remaining]
diff --git a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py
new file mode 100644
index 00000000000..34b665fa9f0
--- /dev/null
+++ b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py
@@ -0,0 +1,148 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+
+import unittest
+
+import frappe
+from frappe.utils import add_days, flt, get_year_ending, get_year_start, getdate
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
+from erpnext.hr.doctype.leave_application.test_leave_application import (
+ get_first_sunday,
+ make_allocation_record,
+)
+from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
+from erpnext.hr.report.employee_leave_balance_summary.employee_leave_balance_summary import execute
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
+ make_holiday_list,
+ make_leave_application,
+)
+
+test_records = frappe.get_test_records("Leave Type")
+
+
+class TestEmployeeLeaveBalance(unittest.TestCase):
+ def setUp(self):
+ for dt in [
+ "Leave Application",
+ "Leave Allocation",
+ "Salary Slip",
+ "Leave Ledger Entry",
+ "Leave Type",
+ ]:
+ frappe.db.delete(dt)
+
+ frappe.set_user("Administrator")
+
+ self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company")
+ self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company")
+
+ self.date = getdate()
+ self.year_start = getdate(get_year_start(self.date))
+ self.year_end = getdate(get_year_ending(self.date))
+
+ self.holiday_list = make_holiday_list(
+ "_Test Emp Balance Holiday List", self.year_start, self.year_end
+ )
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ @set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
+ def test_employee_leave_balance_summary(self):
+ frappe.get_doc(test_records[0]).insert()
+
+ # 5 leaves
+ allocation1 = make_allocation_record(
+ employee=self.employee_id,
+ from_date=add_days(self.year_start, -11),
+ to_date=add_days(self.year_start, -1),
+ leaves=5,
+ )
+ # 30 leaves
+ allocation2 = make_allocation_record(
+ employee=self.employee_id, from_date=self.year_start, to_date=self.year_end
+ )
+
+ # 2 days leave within the first allocation
+ leave_application1 = make_leave_application(
+ self.employee_id,
+ add_days(self.year_start, -11),
+ add_days(self.year_start, -10),
+ "_Test Leave Type",
+ )
+ leave_application1.reload()
+
+ # expires 3 leaves
+ process_expired_allocation()
+
+ # 4 days leave within the second allocation
+ first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
+ leave_application2 = make_leave_application(
+ self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), "_Test Leave Type"
+ )
+ leave_application2.reload()
+
+ filters = frappe._dict(
+ {
+ "date": add_days(leave_application2.to_date, 1),
+ "company": "_Test Company",
+ "employee": self.employee_id,
+ }
+ )
+
+ report = execute(filters)
+
+ expected_data = [
+ [
+ self.employee_id,
+ "test_emp_leave_balance@example.com",
+ frappe.db.get_value("Employee", self.employee_id, "department"),
+ flt(
+ allocation1.new_leaves_allocated # allocated = 5
+ + allocation2.new_leaves_allocated # allocated = 30
+ - leave_application1.total_leave_days # leaves taken in the 1st alloc = 2
+ - (
+ allocation1.new_leaves_allocated - leave_application1.total_leave_days
+ ) # leaves expired from 1st alloc = 3
+ - leave_application2.total_leave_days # leaves taken in the 2nd alloc = 4
+ ),
+ ]
+ ]
+
+ self.assertEqual(report[1], expected_data)
+
+ @set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
+ def test_get_leave_balance_near_alloc_expiry(self):
+ frappe.get_doc(test_records[0]).insert()
+
+ # 30 leaves allocated
+ allocation = make_allocation_record(
+ employee=self.employee_id, from_date=self.year_start, to_date=self.year_end
+ )
+ # 4 days leave application in the first allocation
+ first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start)
+ leave_application = make_leave_application(
+ self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), "_Test Leave Type"
+ )
+ leave_application.reload()
+
+ # Leave balance should show actual balance, and not "consumption balance as per remaining days", near alloc end date
+ # eg: 3 days left for alloc to end, leave balance should still be 26 and not 3
+ filters = frappe._dict(
+ {"date": add_days(self.year_end, -3), "company": "_Test Company", "employee": self.employee_id}
+ )
+ report = execute(filters)
+
+ expected_data = [
+ [
+ self.employee_id,
+ "test_emp_leave_balance@example.com",
+ frappe.db.get_value("Employee", self.employee_id, "department"),
+ flt(allocation.new_leaves_allocated - leave_application.total_leave_days),
+ ]
+ ]
+
+ self.assertEqual(report[1], expected_data)
diff --git a/erpnext/hr/report/employees_working_on_a_holiday/employees_working_on_a_holiday.py b/erpnext/hr/report/employees_working_on_a_holiday/employees_working_on_a_holiday.py
index 00a4a7c29f5..f13fabf06e6 100644
--- a/erpnext/hr/report/employees_working_on_a_holiday/employees_working_on_a_holiday.py
+++ b/erpnext/hr/report/employees_working_on_a_holiday/employees_working_on_a_holiday.py
@@ -21,16 +21,21 @@ def get_columns():
_("Name") + ":Data:200",
_("Date") + ":Date:100",
_("Status") + ":Data:70",
- _("Holiday") + ":Data:200"
+ _("Holiday") + ":Data:200",
]
+
def get_employees(filters):
- holiday_filter = [["holiday_date", ">=", filters.from_date], ["holiday_date", "<=", filters.to_date]]
+ holiday_filter = [
+ ["holiday_date", ">=", filters.from_date],
+ ["holiday_date", "<=", filters.to_date],
+ ]
if filters.holiday_list:
holiday_filter.append(["parent", "=", filters.holiday_list])
- holidays = frappe.get_all("Holiday", fields=["holiday_date", "description"],
- filters=holiday_filter)
+ holidays = frappe.get_all(
+ "Holiday", fields=["holiday_date", "description"], filters=holiday_filter
+ )
holiday_names = {}
holidays_list = []
@@ -39,18 +44,23 @@ def get_employees(filters):
holidays_list.append(holiday.holiday_date)
holiday_names[holiday.holiday_date] = holiday.description
- if(holidays_list):
+ if holidays_list:
cond = " attendance_date in %(holidays_list)s"
if filters.holiday_list:
- cond += """ and (employee in (select employee from tabEmployee where holiday_list = %(holidays)s))"""
+ cond += (
+ """ and (employee in (select employee from tabEmployee where holiday_list = %(holidays)s))"""
+ )
- employee_list = frappe.db.sql("""select
+ employee_list = frappe.db.sql(
+ """select
employee, employee_name, attendance_date, status
from tabAttendance
- where %s"""% cond.format(', '.join(["%s"] * len(holidays_list))),
- {'holidays_list':holidays_list,
- 'holidays':filters.holiday_list}, as_list=True)
+ where %s"""
+ % cond.format(", ".join(["%s"] * len(holidays_list))),
+ {"holidays_list": holidays_list, "holidays": filters.holiday_list},
+ as_list=True,
+ )
for employee_data in employee_list:
employee_data.append(holiday_names[employee_data[2]])
diff --git a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
index 4e043379404..c6f5bf05891 100644
--- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
+++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
@@ -15,21 +15,15 @@ status_map = {
"Weekly Off": "WO",
"On Leave": "L",
"Present": "P",
- "Work From Home": "WFH"
- }
+ "Work From Home": "WFH",
+}
+
+day_abbr = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
-day_abbr = [
- "Mon",
- "Tue",
- "Wed",
- "Thu",
- "Fri",
- "Sat",
- "Sun"
-]
def execute(filters=None):
- if not filters: filters = {}
+ if not filters:
+ filters = {}
if filters.hide_year_field == 1:
filters.year = 2020
@@ -44,25 +38,29 @@ def execute(filters=None):
emp_map, group_by_parameters = get_employee_details(filters.group_by, filters.company)
holiday_list = []
for parameter in group_by_parameters:
- h_list = [emp_map[parameter][d]["holiday_list"] for d in emp_map[parameter] if emp_map[parameter][d]["holiday_list"]]
+ h_list = [
+ emp_map[parameter][d]["holiday_list"]
+ for d in emp_map[parameter]
+ if emp_map[parameter][d]["holiday_list"]
+ ]
holiday_list += h_list
else:
emp_map = get_employee_details(filters.group_by, filters.company)
holiday_list = [emp_map[d]["holiday_list"] for d in emp_map if emp_map[d]["holiday_list"]]
-
- default_holiday_list = frappe.get_cached_value('Company', filters.get("company"), "default_holiday_list")
+ default_holiday_list = frappe.get_cached_value(
+ "Company", filters.get("company"), "default_holiday_list"
+ )
holiday_list.append(default_holiday_list)
holiday_list = list(set(holiday_list))
holiday_map = get_holiday(holiday_list, filters["month"])
data = []
- leave_list = None
+ leave_types = None
if filters.summarized_view:
- leave_types = frappe.db.sql("""select name from `tabLeave Type`""", as_list=True)
- leave_list = [d[0] + ":Float:120" for d in leave_types]
- columns.extend(leave_list)
+ leave_types = frappe.get_all("Leave Type", pluck="name")
+ columns.extend([leave_type + ":Float:120" for leave_type in leave_types])
columns.extend([_("Total Late Entries") + ":Float:120", _("Total Early Exits") + ":Float:120"])
if filters.group_by:
@@ -70,20 +68,39 @@ def execute(filters=None):
for parameter in group_by_parameters:
emp_map_set = set([key for key in emp_map[parameter].keys()])
att_map_set = set([key for key in att_map.keys()])
- if (att_map_set & emp_map_set):
- parameter_row = [""+ parameter + ""] + ['' for day in range(filters["total_days_in_month"] + 2)]
+ if att_map_set & emp_map_set:
+ parameter_row = ["" + parameter + ""] + [
+ "" for day in range(filters["total_days_in_month"] + 2)
+ ]
data.append(parameter_row)
- record, emp_att_data = add_data(emp_map[parameter], att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list)
+ record, emp_att_data = add_data(
+ emp_map[parameter],
+ att_map,
+ filters,
+ holiday_map,
+ conditions,
+ default_holiday_list,
+ leave_types=leave_types,
+ )
emp_att_map.update(emp_att_data)
data += record
else:
- record, emp_att_map = add_data(emp_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list)
+ record, emp_att_map = add_data(
+ emp_map,
+ att_map,
+ filters,
+ holiday_map,
+ conditions,
+ default_holiday_list,
+ leave_types=leave_types,
+ )
data += record
chart_data = get_chart_data(emp_att_map, days)
return columns, data, None, chart_data
+
def get_chart_data(emp_att_map, days):
labels = []
datasets = [
@@ -92,12 +109,10 @@ def get_chart_data(emp_att_map, days):
{"name": "Leave", "values": []},
]
for idx, day in enumerate(days, start=0):
- p = day.replace("::65", "")
labels.append(day.replace("::65", ""))
total_absent_on_day = 0
total_leave_on_day = 0
total_present_on_day = 0
- total_holiday = 0
for emp in emp_att_map.keys():
if emp_att_map[emp][idx]:
if emp_att_map[emp][idx] == "A":
@@ -110,25 +125,20 @@ def get_chart_data(emp_att_map, days):
if emp_att_map[emp][idx] == "L":
total_leave_on_day += 1
-
datasets[0]["values"].append(total_absent_on_day)
datasets[1]["values"].append(total_present_on_day)
datasets[2]["values"].append(total_leave_on_day)
-
- chart = {
- "data": {
- 'labels': labels,
- 'datasets': datasets
- }
- }
+ chart = {"data": {"labels": labels, "datasets": datasets}}
chart["type"] = "line"
return chart
-def add_data(employee_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=None):
+def add_data(
+ employee_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_types=None
+):
record = []
emp_att_map = {}
for emp in employee_map:
@@ -141,7 +151,7 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, default_ho
row += [" "]
row += [emp, emp_det.employee_name]
- total_p = total_a = total_l = total_h = total_um= 0.0
+ total_p = total_a = total_l = total_h = total_um = 0.0
emp_status_map = []
for day in range(filters["total_days_in_month"]):
status = None
@@ -152,7 +162,7 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, default_ho
if emp_holiday_list in holiday_map:
for idx, ele in enumerate(holiday_map[emp_holiday_list]):
- if day+1 == holiday_map[emp_holiday_list][idx][0]:
+ if day + 1 == holiday_map[emp_holiday_list][idx][0]:
if holiday_map[emp_holiday_list][idx][1]:
status = "Weekly Off"
else:
@@ -162,7 +172,7 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, default_ho
abbr = status_map.get(status, "")
emp_status_map.append(abbr)
- if filters.summarized_view:
+ if filters.summarized_view:
if status == "Present" or status == "Work From Home":
total_p += 1
elif status == "Absent":
@@ -189,12 +199,21 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, default_ho
filters.update({"employee": emp})
if filters.summarized_view:
- leave_details = frappe.db.sql("""select leave_type, status, count(*) as count from `tabAttendance`\
- where leave_type is not NULL %s group by leave_type, status""" % conditions, filters, as_dict=1)
+ leave_details = frappe.db.sql(
+ """select leave_type, status, count(*) as count from `tabAttendance`\
+ where leave_type is not NULL %s group by leave_type, status"""
+ % conditions,
+ filters,
+ as_dict=1,
+ )
- time_default_counts = frappe.db.sql("""select (select count(*) from `tabAttendance` where \
+ time_default_counts = frappe.db.sql(
+ """select (select count(*) from `tabAttendance` where \
late_entry = 1 %s) as late_entry_count, (select count(*) from tabAttendance where \
- early_exit = 1 %s) as early_exit_count""" % (conditions, conditions), filters)
+ early_exit = 1 %s) as early_exit_count"""
+ % (conditions, conditions),
+ filters,
+ )
leaves = {}
for d in leave_details:
@@ -205,44 +224,54 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, default_ho
else:
leaves[d.leave_type] = d.count
- for d in leave_list:
+ for d in leave_types:
if d in leaves:
row.append(leaves[d])
else:
row.append("0.0")
- row.extend([time_default_counts[0][0],time_default_counts[0][1]])
+ row.extend([time_default_counts[0][0], time_default_counts[0][1]])
emp_att_map[emp] = emp_status_map
record.append(row)
return record, emp_att_map
+
def get_columns(filters):
columns = []
if filters.group_by:
- columns = [_(filters.group_by)+ ":Link/Branch:120"]
+ columns = [_(filters.group_by) + ":Link/Branch:120"]
- columns += [
- _("Employee") + ":Link/Employee:120", _("Employee Name") + ":Data/:120"
- ]
+ columns += [_("Employee") + ":Link/Employee:120", _("Employee Name") + ":Data/:120"]
days = []
for day in range(filters["total_days_in_month"]):
- date = str(filters.year) + "-" + str(filters.month)+ "-" + str(day+1)
+ date = str(filters.year) + "-" + str(filters.month) + "-" + str(day + 1)
day_name = day_abbr[getdate(date).weekday()]
- days.append(cstr(day+1)+ " " +day_name +"::65")
+ days.append(cstr(day + 1) + " " + day_name + "::65")
if not filters.summarized_view:
columns += days
if filters.summarized_view:
- columns += [_("Total Present") + ":Float:120", _("Total Leaves") + ":Float:120", _("Total Absent") + ":Float:120", _("Total Holidays") + ":Float:120", _("Unmarked Days")+ ":Float:120"]
+ columns += [
+ _("Total Present") + ":Float:120",
+ _("Total Leaves") + ":Float:120",
+ _("Total Absent") + ":Float:120",
+ _("Total Holidays") + ":Float:120",
+ _("Unmarked Days") + ":Float:120",
+ ]
return columns, days
+
def get_attendance_list(conditions, filters):
- attendance_list = frappe.db.sql("""select employee, day(attendance_date) as day_of_month,
- status from tabAttendance where docstatus = 1 %s order by employee, attendance_date""" %
- conditions, filters, as_dict=1)
+ attendance_list = frappe.db.sql(
+ """select employee, day(attendance_date) as day_of_month,
+ status from tabAttendance where docstatus = 1 %s order by employee, attendance_date"""
+ % conditions,
+ filters,
+ as_dict=1,
+ )
if not attendance_list:
msgprint(_("No attendance record found"), alert=True, indicator="orange")
@@ -254,6 +283,7 @@ def get_attendance_list(conditions, filters):
return att_map
+
def get_conditions(filters):
if not (filters.get("month") and filters.get("year")):
msgprint(_("Please select month and year"), raise_exception=1)
@@ -262,29 +292,35 @@ def get_conditions(filters):
conditions = " and month(attendance_date) = %(month)s and year(attendance_date) = %(year)s"
- if filters.get("company"): conditions += " and company = %(company)s"
- if filters.get("employee"): conditions += " and employee = %(employee)s"
+ if filters.get("company"):
+ conditions += " and company = %(company)s"
+ if filters.get("employee"):
+ conditions += " and employee = %(employee)s"
return conditions, filters
+
def get_employee_details(group_by, company):
emp_map = {}
query = """select name, employee_name, designation, department, branch, company,
- holiday_list from `tabEmployee` where company = %s """ % frappe.db.escape(company)
+ holiday_list from `tabEmployee` where company = %s """ % frappe.db.escape(
+ company
+ )
if group_by:
group_by = group_by.lower()
query += " order by " + group_by + " ASC"
- employee_details = frappe.db.sql(query , as_dict=1)
+ employee_details = frappe.db.sql(query, as_dict=1)
group_by_parameters = []
if group_by:
- group_by_parameters = list(set(detail.get(group_by, "") for detail in employee_details if detail.get(group_by, "")))
+ group_by_parameters = list(
+ set(detail.get(group_by, "") for detail in employee_details if detail.get(group_by, ""))
+ )
for parameter in group_by_parameters:
- emp_map[parameter] = {}
-
+ emp_map[parameter] = {}
for d in employee_details:
if group_by and len(group_by_parameters):
@@ -299,18 +335,28 @@ def get_employee_details(group_by, company):
else:
return emp_map, group_by_parameters
+
def get_holiday(holiday_list, month):
holiday_map = frappe._dict()
for d in holiday_list:
if d:
- holiday_map.setdefault(d, frappe.db.sql('''select day(holiday_date), weekly_off from `tabHoliday`
- where parent=%s and month(holiday_date)=%s''', (d, month)))
+ holiday_map.setdefault(
+ d,
+ frappe.db.sql(
+ """select day(holiday_date), weekly_off from `tabHoliday`
+ where parent=%s and month(holiday_date)=%s""",
+ (d, month),
+ ),
+ )
return holiday_map
+
@frappe.whitelist()
def get_attendance_years():
- year_list = frappe.db.sql_list("""select distinct YEAR(attendance_date) from tabAttendance ORDER BY YEAR(attendance_date) DESC""")
+ year_list = frappe.db.sql_list(
+ """select distinct YEAR(attendance_date) from tabAttendance ORDER BY YEAR(attendance_date) DESC"""
+ )
if not year_list:
year_list = [getdate().year]
diff --git a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py
new file mode 100644
index 00000000000..91da08eee50
--- /dev/null
+++ b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py
@@ -0,0 +1,46 @@
+import frappe
+from dateutil.relativedelta import relativedelta
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import now_datetime
+
+from erpnext.hr.doctype.attendance.attendance import mark_attendance
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.report.monthly_attendance_sheet.monthly_attendance_sheet import execute
+
+
+class TestMonthlyAttendanceSheet(FrappeTestCase):
+ def setUp(self):
+ self.employee = make_employee("test_employee@example.com")
+ frappe.db.delete("Attendance", {"employee": self.employee})
+
+ def test_monthly_attendance_sheet_report(self):
+ now = now_datetime()
+ previous_month = now.month - 1
+ previous_month_first = now.replace(day=1).replace(month=previous_month).date()
+
+ company = frappe.db.get_value("Employee", self.employee, "company")
+
+ # mark different attendance status on first 3 days of previous month
+ mark_attendance(self.employee, previous_month_first, "Absent")
+ mark_attendance(self.employee, previous_month_first + relativedelta(days=1), "Present")
+ mark_attendance(self.employee, previous_month_first + relativedelta(days=2), "On Leave")
+
+ filters = frappe._dict(
+ {
+ "month": previous_month,
+ "year": now.year,
+ "company": company,
+ }
+ )
+ report = execute(filters=filters)
+ employees = report[1][0]
+ datasets = report[3]["data"]["datasets"]
+ absent = datasets[0]["values"]
+ present = datasets[1]["values"]
+ leaves = datasets[2]["values"]
+
+ # ensure correct attendance is reflect on the report
+ self.assertIn(self.employee, employees)
+ self.assertEqual(absent[0], 1)
+ self.assertEqual(present[1], 1)
+ self.assertEqual(leaves[2], 1)
diff --git a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py
index 6383a9bbac9..b6caf400dd0 100644
--- a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py
+++ b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py
@@ -8,7 +8,8 @@ from frappe import _
def execute(filters=None):
- if not filters: filters = {}
+ if not filters:
+ filters = {}
filters = frappe._dict(filters)
columns = get_columns()
@@ -25,67 +26,53 @@ def get_columns():
"fieldtype": "Link",
"fieldname": "staffing_plan",
"options": "Staffing Plan",
- "width": 150
+ "width": 150,
},
{
"label": _("Job Opening"),
"fieldtype": "Link",
"fieldname": "job_opening",
"options": "Job Opening",
- "width": 105
+ "width": 105,
},
{
"label": _("Job Applicant"),
"fieldtype": "Link",
"fieldname": "job_applicant",
"options": "Job Applicant",
- "width": 150
- },
- {
- "label": _("Applicant name"),
- "fieldtype": "data",
- "fieldname": "applicant_name",
- "width": 130
+ "width": 150,
},
+ {"label": _("Applicant name"), "fieldtype": "data", "fieldname": "applicant_name", "width": 130},
{
"label": _("Application Status"),
"fieldtype": "Data",
"fieldname": "application_status",
- "width": 150
+ "width": 150,
},
{
"label": _("Job Offer"),
"fieldtype": "Link",
"fieldname": "job_offer",
"options": "job Offer",
- "width": 150
- },
- {
- "label": _("Designation"),
- "fieldtype": "Data",
- "fieldname": "designation",
- "width": 100
- },
- {
- "label": _("Offer Date"),
- "fieldtype": "date",
- "fieldname": "offer_date",
- "width": 100
+ "width": 150,
},
+ {"label": _("Designation"), "fieldtype": "Data", "fieldname": "designation", "width": 100},
+ {"label": _("Offer Date"), "fieldtype": "date", "fieldname": "offer_date", "width": 100},
{
"label": _("Job Offer status"),
"fieldtype": "Data",
"fieldname": "job_offer_status",
- "width": 150
- }
+ "width": 150,
+ },
]
+
def get_data(filters):
data = []
staffing_plan_details = get_staffing_plan(filters)
- staffing_plan_list = list(set([details["name"] for details in staffing_plan_details]))
- sp_jo_map , jo_list = get_job_opening(staffing_plan_list)
- jo_ja_map , ja_list = get_job_applicant(jo_list)
+ staffing_plan_list = list(set([details["name"] for details in staffing_plan_details]))
+ sp_jo_map, jo_list = get_job_opening(staffing_plan_list)
+ jo_ja_map, ja_list = get_job_applicant(jo_list)
ja_joff_map = get_job_offer(ja_list)
for sp in sp_jo_map.keys():
@@ -100,37 +87,40 @@ def get_parent_row(sp_jo_map, sp, jo_ja_map, ja_joff_map):
if sp in sp_jo_map.keys():
for jo in sp_jo_map[sp]:
row = {
- "staffing_plan" : sp,
- "job_opening" : jo["name"],
+ "staffing_plan": sp,
+ "job_opening": jo["name"],
}
data.append(row)
- child_row = get_child_row( jo["name"], jo_ja_map, ja_joff_map)
+ child_row = get_child_row(jo["name"], jo_ja_map, ja_joff_map)
data += child_row
return data
+
def get_child_row(jo, jo_ja_map, ja_joff_map):
data = []
if jo in jo_ja_map.keys():
for ja in jo_ja_map[jo]:
row = {
- "indent":1,
+ "indent": 1,
"job_applicant": ja.name,
"applicant_name": ja.applicant_name,
"application_status": ja.status,
}
if ja.name in ja_joff_map.keys():
- jo_detail =ja_joff_map[ja.name][0]
+ jo_detail = ja_joff_map[ja.name][0]
row["job_offer"] = jo_detail.name
row["job_offer_status"] = jo_detail.status
- row["offer_date"]= jo_detail.offer_date.strftime("%d-%m-%Y")
+ row["offer_date"] = jo_detail.offer_date.strftime("%d-%m-%Y")
row["designation"] = jo_detail.designation
data.append(row)
return data
+
def get_staffing_plan(filters):
- staffing_plan = frappe.db.sql("""
+ staffing_plan = frappe.db.sql(
+ """
select
sp.name, sp.department, spd.designation, spd.vacancies, spd.current_count, spd.parent, sp.to_date
from
@@ -139,13 +129,20 @@ def get_staffing_plan(filters):
spd.parent = sp.name
And
sp.to_date > '{0}'
- """.format(filters.on_date), as_dict = 1)
+ """.format(
+ filters.on_date
+ ),
+ as_dict=1,
+ )
return staffing_plan
+
def get_job_opening(sp_list):
- job_openings = frappe.get_all("Job Opening", filters = [["staffing_plan", "IN", sp_list]], fields =["name", "staffing_plan"])
+ job_openings = frappe.get_all(
+ "Job Opening", filters=[["staffing_plan", "IN", sp_list]], fields=["name", "staffing_plan"]
+ )
sp_jo_map = {}
jo_list = []
@@ -160,12 +157,17 @@ def get_job_opening(sp_list):
return sp_jo_map, jo_list
+
def get_job_applicant(jo_list):
jo_ja_map = {}
- ja_list =[]
+ ja_list = []
- applicants = frappe.get_all("Job Applicant", filters = [["job_title", "IN", jo_list]], fields =["name", "job_title","applicant_name", 'status'])
+ applicants = frappe.get_all(
+ "Job Applicant",
+ filters=[["job_title", "IN", jo_list]],
+ fields=["name", "job_title", "applicant_name", "status"],
+ )
for applicant in applicants:
if applicant.job_title not in jo_ja_map.keys():
@@ -175,12 +177,17 @@ def get_job_applicant(jo_list):
ja_list.append(applicant.name)
- return jo_ja_map , ja_list
+ return jo_ja_map, ja_list
+
def get_job_offer(ja_list):
ja_joff_map = {}
- offers = frappe.get_all("Job Offer", filters = [["job_applicant", "IN", ja_list]], fields =["name", "job_applicant", "status", 'offer_date', 'designation'])
+ offers = frappe.get_all(
+ "Job Offer",
+ filters=[["job_applicant", "IN", ja_list]],
+ fields=["name", "job_applicant", "status", "offer_date", "designation"],
+ )
for offer in offers:
if offer.job_applicant not in ja_joff_map.keys():
diff --git a/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py b/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py
index 8672e68cf4b..da6dace72b5 100644
--- a/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py
+++ b/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py
@@ -17,12 +17,14 @@ from erpnext.hr.report.vehicle_expenses.vehicle_expenses import execute
class TestVehicleExpenses(unittest.TestCase):
@classmethod
def setUpClass(self):
- frappe.db.sql('delete from `tabVehicle Log`')
+ frappe.db.sql("delete from `tabVehicle Log`")
- employee_id = frappe.db.sql('''select name from `tabEmployee` where name="testdriver@example.com"''')
+ employee_id = frappe.db.sql(
+ '''select name from `tabEmployee` where name="testdriver@example.com"'''
+ )
self.employee_id = employee_id[0][0] if employee_id else None
if not self.employee_id:
- self.employee_id = make_employee('testdriver@example.com', company='_Test Company')
+ self.employee_id = make_employee("testdriver@example.com", company="_Test Company")
self.license_plate = get_vehicle(self.employee_id)
@@ -31,36 +33,35 @@ class TestVehicleExpenses(unittest.TestCase):
expense_claim = make_expense_claim(vehicle_log.name)
# Based on Fiscal Year
- filters = {
- 'filter_based_on': 'Fiscal Year',
- 'fiscal_year': get_fiscal_year(getdate())[0]
- }
+ filters = {"filter_based_on": "Fiscal Year", "fiscal_year": get_fiscal_year(getdate())[0]}
report = execute(filters)
- expected_data = [{
- 'vehicle': self.license_plate,
- 'make': 'Maruti',
- 'model': 'PCM',
- 'location': 'Mumbai',
- 'log_name': vehicle_log.name,
- 'odometer': 5010,
- 'date': getdate(),
- 'fuel_qty': 50.0,
- 'fuel_price': 500.0,
- 'fuel_expense': 25000.0,
- 'service_expense': 2000.0,
- 'employee': self.employee_id
- }]
+ expected_data = [
+ {
+ "vehicle": self.license_plate,
+ "make": "Maruti",
+ "model": "PCM",
+ "location": "Mumbai",
+ "log_name": vehicle_log.name,
+ "odometer": 5010,
+ "date": getdate(),
+ "fuel_qty": 50.0,
+ "fuel_price": 500.0,
+ "fuel_expense": 25000.0,
+ "service_expense": 2000.0,
+ "employee": self.employee_id,
+ }
+ ]
self.assertEqual(report[1], expected_data)
# Based on Date Range
fiscal_year = get_fiscal_year(getdate(), as_dict=True)
filters = {
- 'filter_based_on': 'Date Range',
- 'from_date': fiscal_year.year_start_date,
- 'to_date': fiscal_year.year_end_date
+ "filter_based_on": "Date Range",
+ "from_date": fiscal_year.year_start_date,
+ "to_date": fiscal_year.year_end_date,
}
report = execute(filters)
@@ -68,9 +69,9 @@ class TestVehicleExpenses(unittest.TestCase):
# clean up
vehicle_log.cancel()
- frappe.delete_doc('Expense Claim', expense_claim.name)
- frappe.delete_doc('Vehicle Log', vehicle_log.name)
+ frappe.delete_doc("Expense Claim", expense_claim.name)
+ frappe.delete_doc("Vehicle Log", vehicle_log.name)
def tearDown(self):
- frappe.delete_doc('Vehicle', self.license_plate, force=1)
- frappe.delete_doc('Employee', self.employee_id, force=1)
+ frappe.delete_doc("Vehicle", self.license_plate, force=1)
+ frappe.delete_doc("Employee", self.employee_id, force=1)
diff --git a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py
index 17d1e9d46a0..fc5510ddad8 100644
--- a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py
+++ b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py
@@ -18,83 +18,44 @@ def execute(filters=None):
return columns, data, None, chart
+
def get_columns():
return [
{
- 'fieldname': 'vehicle',
- 'fieldtype': 'Link',
- 'label': _('Vehicle'),
- 'options': 'Vehicle',
- 'width': 150
+ "fieldname": "vehicle",
+ "fieldtype": "Link",
+ "label": _("Vehicle"),
+ "options": "Vehicle",
+ "width": 150,
+ },
+ {"fieldname": "make", "fieldtype": "Data", "label": _("Make"), "width": 100},
+ {"fieldname": "model", "fieldtype": "Data", "label": _("Model"), "width": 80},
+ {"fieldname": "location", "fieldtype": "Data", "label": _("Location"), "width": 100},
+ {
+ "fieldname": "log_name",
+ "fieldtype": "Link",
+ "label": _("Vehicle Log"),
+ "options": "Vehicle Log",
+ "width": 100,
+ },
+ {"fieldname": "odometer", "fieldtype": "Int", "label": _("Odometer Value"), "width": 120},
+ {"fieldname": "date", "fieldtype": "Date", "label": _("Date"), "width": 100},
+ {"fieldname": "fuel_qty", "fieldtype": "Float", "label": _("Fuel Qty"), "width": 80},
+ {"fieldname": "fuel_price", "fieldtype": "Float", "label": _("Fuel Price"), "width": 100},
+ {"fieldname": "fuel_expense", "fieldtype": "Currency", "label": _("Fuel Expense"), "width": 150},
+ {
+ "fieldname": "service_expense",
+ "fieldtype": "Currency",
+ "label": _("Service Expense"),
+ "width": 150,
},
{
- 'fieldname': 'make',
- 'fieldtype': 'Data',
- 'label': _('Make'),
- 'width': 100
+ "fieldname": "employee",
+ "fieldtype": "Link",
+ "label": _("Employee"),
+ "options": "Employee",
+ "width": 150,
},
- {
- 'fieldname': 'model',
- 'fieldtype': 'Data',
- 'label': _('Model'),
- 'width': 80
- },
- {
- 'fieldname': 'location',
- 'fieldtype': 'Data',
- 'label': _('Location'),
- 'width': 100
- },
- {
- 'fieldname': 'log_name',
- 'fieldtype': 'Link',
- 'label': _('Vehicle Log'),
- 'options': 'Vehicle Log',
- 'width': 100
- },
- {
- 'fieldname': 'odometer',
- 'fieldtype': 'Int',
- 'label': _('Odometer Value'),
- 'width': 120
- },
- {
- 'fieldname': 'date',
- 'fieldtype': 'Date',
- 'label': _('Date'),
- 'width': 100
- },
- {
- 'fieldname': 'fuel_qty',
- 'fieldtype': 'Float',
- 'label': _('Fuel Qty'),
- 'width': 80
- },
- {
- 'fieldname': 'fuel_price',
- 'fieldtype': 'Float',
- 'label': _('Fuel Price'),
- 'width': 100
- },
- {
- 'fieldname': 'fuel_expense',
- 'fieldtype': 'Currency',
- 'label': _('Fuel Expense'),
- 'width': 150
- },
- {
- 'fieldname': 'service_expense',
- 'fieldtype': 'Currency',
- 'label': _('Service Expense'),
- 'width': 150
- },
- {
- 'fieldname': 'employee',
- 'fieldtype': 'Link',
- 'label': _('Employee'),
- 'options': 'Employee',
- 'width': 150
- }
]
@@ -102,7 +63,8 @@ def get_vehicle_log_data(filters):
start_date, end_date = get_period_dates(filters)
conditions, values = get_conditions(filters)
- data = frappe.db.sql("""
+ data = frappe.db.sql(
+ """
SELECT
vhcl.license_plate as vehicle, vhcl.make, vhcl.model,
vhcl.location, log.name as log_name, log.odometer,
@@ -116,58 +78,70 @@ def get_vehicle_log_data(filters):
and log.docstatus = 1
and date between %(start_date)s and %(end_date)s
{0}
- ORDER BY date""".format(conditions), values, as_dict=1)
+ ORDER BY date""".format(
+ conditions
+ ),
+ values,
+ as_dict=1,
+ )
for row in data:
- row['service_expense'] = get_service_expense(row.log_name)
+ row["service_expense"] = get_service_expense(row.log_name)
return data
def get_conditions(filters):
- conditions = ''
+ conditions = ""
start_date, end_date = get_period_dates(filters)
- values = {
- 'start_date': start_date,
- 'end_date': end_date
- }
+ values = {"start_date": start_date, "end_date": end_date}
if filters.employee:
- conditions += ' and log.employee = %(employee)s'
- values['employee'] = filters.employee
+ conditions += " and log.employee = %(employee)s"
+ values["employee"] = filters.employee
if filters.vehicle:
- conditions += ' and vhcl.license_plate = %(vehicle)s'
- values['vehicle'] = filters.vehicle
+ conditions += " and vhcl.license_plate = %(vehicle)s"
+ values["vehicle"] = filters.vehicle
return conditions, values
def get_period_dates(filters):
- if filters.filter_based_on == 'Fiscal Year' and filters.fiscal_year:
- fy = frappe.db.get_value('Fiscal Year', filters.fiscal_year,
- ['year_start_date', 'year_end_date'], as_dict=True)
+ if filters.filter_based_on == "Fiscal Year" and filters.fiscal_year:
+ fy = frappe.db.get_value(
+ "Fiscal Year", filters.fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
+ )
return fy.year_start_date, fy.year_end_date
else:
return filters.from_date, filters.to_date
def get_service_expense(logname):
- expense_amount = frappe.db.sql("""
+ expense_amount = frappe.db.sql(
+ """
SELECT sum(expense_amount)
FROM
`tabVehicle Log` log, `tabVehicle Service` service
WHERE
service.parent=log.name and log.name=%s
- """, logname)
+ """,
+ logname,
+ )
return flt(expense_amount[0][0]) if expense_amount else 0.0
def get_chart_data(data, filters):
- period_list = get_period_list(filters.fiscal_year, filters.fiscal_year,
- filters.from_date, filters.to_date, filters.filter_based_on, 'Monthly')
+ period_list = get_period_list(
+ filters.fiscal_year,
+ filters.fiscal_year,
+ filters.from_date,
+ filters.to_date,
+ filters.filter_based_on,
+ "Monthly",
+ )
fuel_data, service_data = [], []
@@ -184,29 +158,20 @@ def get_chart_data(data, filters):
service_data.append([period.key, total_service_exp])
labels = [period.label for period in period_list]
- fuel_exp_data= [row[1] for row in fuel_data]
- service_exp_data= [row[1] for row in service_data]
+ fuel_exp_data = [row[1] for row in fuel_data]
+ service_exp_data = [row[1] for row in service_data]
datasets = []
if fuel_exp_data:
- datasets.append({
- 'name': _('Fuel Expenses'),
- 'values': fuel_exp_data
- })
+ datasets.append({"name": _("Fuel Expenses"), "values": fuel_exp_data})
if service_exp_data:
- datasets.append({
- 'name': _('Service Expenses'),
- 'values': service_exp_data
- })
+ datasets.append({"name": _("Service Expenses"), "values": service_exp_data})
chart = {
- 'data': {
- 'labels': labels,
- 'datasets': datasets
- },
- 'type': 'line',
- 'fieldtype': 'Currency'
+ "data": {"labels": labels, "datasets": datasets},
+ "type": "line",
+ "fieldtype": "Currency",
}
return chart
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index 46bcadcf536..f1c1608cdd0 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -26,20 +26,22 @@ from erpnext.hr.doctype.employee.employee import (
)
-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
- '''
+ """
+ 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):
validate_active_employee(self.employee)
# remove the task if linked before submitting the form
if self.amended_from:
for activity in self.activities:
- activity.task = ''
+ activity.task = ""
def on_submit(self):
# create the project for the given employee onboarding
@@ -49,13 +51,17 @@ class EmployeeBoardingController(Document):
else:
project_name += self.employee
- project = frappe.get_doc({
+ 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,
+ "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)
+ "company": self.company,
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True)
self.db_set("project", project.name)
self.db_set("boarding_status", "Pending")
@@ -68,20 +74,23 @@ class EmployeeBoardingController(Document):
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)
+ 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('''
+ user_list = frappe.db.sql_list(
+ """
SELECT
DISTINCT(has_role.parent)
FROM
@@ -92,7 +101,9 @@ class EmployeeBoardingController(Document):
has_role.parenttype = 'User'
AND user.enabled = 1
AND has_role.role = %s
- ''', activity.role)
+ """,
+ activity.role,
+ )
users = unique(users + user_list)
if "Administrator" in users:
@@ -105,11 +116,11 @@ class EmployeeBoardingController(Document):
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": [user],
+ "doctype": task.doctype,
+ "name": task.name,
+ "description": task.description or task.subject,
+ "notify": self.notify_users_by_email,
}
assign_to.add(args)
@@ -118,41 +129,56 @@ class EmployeeBoardingController(Document):
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', '')
+ 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"],
+ 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")
+ order_by="idx",
+ )
+
@frappe.whitelist()
def get_boarding_status(project):
- status = 'Pending'
+ status = "Pending"
if project:
- doc = frappe.get_doc('Project', project)
+ doc = frappe.get_doc("Project", project)
if flt(doc.percent_complete) > 0.0 and flt(doc.percent_complete) < 100.0:
- status = 'In Process'
+ status = "In Process"
elif flt(doc.percent_complete) == 100.0:
- status = 'Completed'
+ status = "Completed"
return status
+
def set_employee_name(doc):
if doc.employee and not doc.employee_name:
doc.employee_name = frappe.db.get_value("Employee", doc.employee, "employee_name")
+
def update_employee_work_history(employee, details, date=None, cancel=False):
if not employee.internal_work_history and not cancel:
- employee.append("internal_work_history", {
- "branch": employee.branch,
- "designation": employee.designation,
- "department": employee.department,
- "from_date": employee.date_of_joining
- })
+ employee.append(
+ "internal_work_history",
+ {
+ "branch": employee.branch,
+ "designation": employee.designation,
+ "department": employee.department,
+ "from_date": employee.date_of_joining,
+ },
+ )
internal_work_history = {}
for item in details:
@@ -163,7 +189,7 @@ def update_employee_work_history(employee, details, date=None, cancel=False):
new_data = item.new if not cancel else item.current
if fieldtype == "Date" and new_data:
new_data = getdate(new_data)
- elif fieldtype =="Datetime" and new_data:
+ elif fieldtype == "Datetime" and new_data:
new_data = get_datetime(new_data)
setattr(employee, item.fieldname, new_data)
if item.fieldname in ["department", "designation", "branch"]:
@@ -178,6 +204,7 @@ def update_employee_work_history(employee, details, date=None, cancel=False):
return employee
+
def delete_employee_work_history(details, employee, date):
filters = {}
for d in details:
@@ -201,12 +228,25 @@ def delete_employee_work_history(details, employee, date):
def get_employee_fields_label():
fields = []
for df in frappe.get_meta("Employee").get("fields"):
- if df.fieldname in ["salutation", "user_id", "employee_number", "employment_type",
- "holiday_list", "branch", "department", "designation", "grade",
- "notice_number_of_days", "reports_to", "leave_policy", "company_email"]:
- fields.append({"value": df.fieldname, "label": df.label})
+ if df.fieldname in [
+ "salutation",
+ "user_id",
+ "employee_number",
+ "employment_type",
+ "holiday_list",
+ "branch",
+ "department",
+ "designation",
+ "grade",
+ "notice_number_of_days",
+ "reports_to",
+ "leave_policy",
+ "company_email",
+ ]:
+ fields.append({"value": df.fieldname, "label": df.label})
return fields
+
@frappe.whitelist()
def get_employee_field_property(employee, fieldname):
if employee and fieldname:
@@ -217,17 +257,15 @@ def get_employee_field_property(employee, fieldname):
value = formatdate(value)
elif field.fieldtype == "Datetime":
value = format_datetime(value)
- return {
- "value" : value,
- "datatype" : field.fieldtype,
- "label" : field.label,
- "options" : options
- }
+ return {"value": value, "datatype": field.fieldtype, "label": field.label, "options": options}
else:
return False
+
def validate_dates(doc, from_date, to_date):
- date_of_joining, relieving_date = frappe.db.get_value("Employee", doc.employee, ["date_of_joining", "relieving_date"])
+ date_of_joining, relieving_date = frappe.db.get_value(
+ "Employee", doc.employee, ["date_of_joining", "relieving_date"]
+ )
if getdate(from_date) > getdate(to_date):
frappe.throw(_("To date can not be less than from date"))
elif getdate(from_date) > getdate(nowdate()):
@@ -237,7 +275,8 @@ def validate_dates(doc, from_date, to_date):
elif relieving_date and getdate(to_date) > getdate(relieving_date):
frappe.throw(_("To date can not greater than employee's relieving date"))
-def validate_overlap(doc, from_date, to_date, company = None):
+
+def validate_overlap(doc, from_date, to_date, company=None):
query = """
select name
from `tab{0}`
@@ -247,15 +286,19 @@ def validate_overlap(doc, from_date, to_date, company = None):
if not doc.name:
# hack! if name is null, it could cause problems with !=
- doc.name = "New "+doc.doctype
+ doc.name = "New " + doc.doctype
- overlap_doc = frappe.db.sql(query.format(doc.doctype),{
+ overlap_doc = frappe.db.sql(
+ query.format(doc.doctype),
+ {
"employee": doc.get("employee"),
"from_date": from_date,
"to_date": to_date,
"name": doc.name,
- "company": company
- }, as_dict = 1)
+ "company": company,
+ },
+ as_dict=1,
+ )
if overlap_doc:
if doc.get("employee"):
@@ -264,6 +307,7 @@ def validate_overlap(doc, from_date, to_date, company = None):
exists_for = company
throw_overlap_error(doc, exists_for, overlap_doc[0].name, from_date, to_date)
+
def get_doc_condition(doctype):
if doctype == "Compensatory Leave Request":
return "and employee = %(employee)s and docstatus < 2 \
@@ -275,23 +319,36 @@ def get_doc_condition(doctype):
or to_date between %(from_date)s and %(to_date)s \
or (from_date < %(from_date)s and to_date > %(to_date)s))"
+
def throw_overlap_error(doc, exists_for, overlap_doc, from_date, to_date):
- msg = _("A {0} exists between {1} and {2} (").format(doc.doctype,
- formatdate(from_date), formatdate(to_date)) \
- + """ {1}""".format(doc.doctype, overlap_doc) \
+ msg = (
+ _("A {0} exists between {1} and {2} (").format(
+ doc.doctype, formatdate(from_date), formatdate(to_date)
+ )
+ + """ {1}""".format(doc.doctype, overlap_doc)
+ _(") for {0}").format(exists_for)
+ )
frappe.throw(msg)
+
def validate_duplicate_exemption_for_payroll_period(doctype, docname, payroll_period, employee):
- existing_record = frappe.db.exists(doctype, {
- "payroll_period": payroll_period,
- "employee": employee,
- 'docstatus': ['<', 2],
- 'name': ['!=', docname]
- })
+ existing_record = frappe.db.exists(
+ doctype,
+ {
+ "payroll_period": payroll_period,
+ "employee": employee,
+ "docstatus": ["<", 2],
+ "name": ["!=", docname],
+ },
+ )
if existing_record:
- frappe.throw(_("{0} already exists for employee {1} and period {2}")
- .format(doctype, employee, payroll_period), DuplicateDeclarationError)
+ frappe.throw(
+ _("{0} already exists for employee {1} and period {2}").format(
+ doctype, employee, payroll_period
+ ),
+ DuplicateDeclarationError,
+ )
+
def validate_tax_declaration(declarations):
subcategories = []
@@ -300,61 +357,79 @@ def validate_tax_declaration(declarations):
frappe.throw(_("More than one selection for {0} not allowed").format(d.exemption_sub_category))
subcategories.append(d.exemption_sub_category)
+
def get_total_exemption_amount(declarations):
exemptions = frappe._dict()
for d in declarations:
exemptions.setdefault(d.exemption_category, frappe._dict())
category_max_amount = exemptions.get(d.exemption_category).max_amount
if not category_max_amount:
- category_max_amount = frappe.db.get_value("Employee Tax Exemption Category", d.exemption_category, "max_amount")
+ category_max_amount = frappe.db.get_value(
+ "Employee Tax Exemption Category", d.exemption_category, "max_amount"
+ )
exemptions.get(d.exemption_category).max_amount = category_max_amount
- sub_category_exemption_amount = d.max_amount \
- if (d.max_amount and flt(d.amount) > flt(d.max_amount)) else d.amount
+ sub_category_exemption_amount = (
+ d.max_amount if (d.max_amount and flt(d.amount) > flt(d.max_amount)) else d.amount
+ )
exemptions.get(d.exemption_category).setdefault("total_exemption_amount", 0.0)
exemptions.get(d.exemption_category).total_exemption_amount += flt(sub_category_exemption_amount)
- if category_max_amount and exemptions.get(d.exemption_category).total_exemption_amount > category_max_amount:
+ if (
+ category_max_amount
+ and exemptions.get(d.exemption_category).total_exemption_amount > category_max_amount
+ ):
exemptions.get(d.exemption_category).total_exemption_amount = category_max_amount
total_exemption_amount = sum([flt(d.total_exemption_amount) for d in exemptions.values()])
return total_exemption_amount
+
@frappe.whitelist()
def get_leave_period(from_date, to_date, company):
- leave_period = frappe.db.sql("""
+ leave_period = frappe.db.sql(
+ """
select name, from_date, to_date
from `tabLeave Period`
where company=%(company)s and is_active=1
and (from_date between %(from_date)s and %(to_date)s
or to_date between %(from_date)s and %(to_date)s
or (from_date < %(from_date)s and to_date > %(to_date)s))
- """, {
- "from_date": from_date,
- "to_date": to_date,
- "company": company
- }, as_dict=1)
+ """,
+ {"from_date": from_date, "to_date": to_date, "company": company},
+ as_dict=1,
+ )
if leave_period:
return leave_period
+
def generate_leave_encashment():
- ''' Generates a draft leave encashment on allocation expiry '''
+ """Generates a draft leave encashment on allocation expiry"""
from erpnext.hr.doctype.leave_encashment.leave_encashment import create_leave_encashment
- if frappe.db.get_single_value('HR Settings', 'auto_leave_encashment'):
- leave_type = frappe.get_all('Leave Type', filters={'allow_encashment': 1}, fields=['name'])
- leave_type=[l['name'] for l in leave_type]
+ if frappe.db.get_single_value("HR Settings", "auto_leave_encashment"):
+ leave_type = frappe.get_all("Leave Type", filters={"allow_encashment": 1}, fields=["name"])
+ leave_type = [l["name"] for l in leave_type]
- leave_allocation = frappe.get_all("Leave Allocation", filters={
- 'to_date': add_days(today(), -1),
- 'leave_type': ('in', leave_type)
- }, fields=['employee', 'leave_period', 'leave_type', 'to_date', 'total_leaves_allocated', 'new_leaves_allocated'])
+ leave_allocation = frappe.get_all(
+ "Leave Allocation",
+ filters={"to_date": add_days(today(), -1), "leave_type": ("in", leave_type)},
+ fields=[
+ "employee",
+ "leave_period",
+ "leave_type",
+ "to_date",
+ "total_leaves_allocated",
+ "new_leaves_allocated",
+ ],
+ )
create_leave_encashment(leave_allocation=leave_allocation)
+
def allocate_earned_leaves(ignore_duplicates=False):
- '''Allocate earned leaves to Employees'''
+ """Allocate earned leaves to Employees"""
e_leave_types = get_earned_leaves()
today = getdate()
@@ -367,37 +442,63 @@ def allocate_earned_leaves(ignore_duplicates=False):
if not allocation.leave_policy_assignment and not allocation.leave_policy:
continue
- leave_policy = allocation.leave_policy if allocation.leave_policy else frappe.db.get_value(
- "Leave Policy Assignment", allocation.leave_policy_assignment, ["leave_policy"])
+ leave_policy = (
+ allocation.leave_policy
+ if allocation.leave_policy
+ else frappe.db.get_value(
+ "Leave Policy Assignment", allocation.leave_policy_assignment, ["leave_policy"]
+ )
+ )
- annual_allocation = frappe.db.get_value("Leave Policy Detail", filters={
- 'parent': leave_policy,
- 'leave_type': e_leave_type.name
- }, fieldname=['annual_allocation'])
+ annual_allocation = frappe.db.get_value(
+ "Leave Policy Detail",
+ filters={"parent": leave_policy, "leave_type": e_leave_type.name},
+ fieldname=["annual_allocation"],
+ )
- from_date=allocation.from_date
+ from_date = allocation.from_date
if e_leave_type.based_on_date_of_joining:
- from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
+ from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
- if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining):
- update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates)
+ if check_effective_date(
+ from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining
+ ):
+ update_previous_leave_allocation(
+ allocation, annual_allocation, e_leave_type, ignore_duplicates
+ )
-def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates=False):
- earned_leaves = get_monthly_earned_leave(annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding)
- allocation = frappe.get_doc('Leave Allocation', allocation.name)
- new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
+def update_previous_leave_allocation(
+ allocation, annual_allocation, e_leave_type, ignore_duplicates=False
+):
+ earned_leaves = get_monthly_earned_leave(
+ annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding
+ )
- if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0:
- new_allocation = e_leave_type.max_leaves_allowed
+ allocation = frappe.get_doc("Leave Allocation", allocation.name)
+ new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
- if new_allocation != allocation.total_leaves_allocated:
- today_date = today()
+ if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0:
+ new_allocation = e_leave_type.max_leaves_allowed
- if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation):
- allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
- create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
+ if new_allocation != allocation.total_leaves_allocated:
+ today_date = today()
+
+ if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation):
+ allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
+ create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
+
+ if e_leave_type.based_on_date_of_joining:
+ text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format(
+ frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
+ )
+ else:
+ text = _("allocated {0} leave(s) via scheduler on {1}").format(
+ frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
+ )
+
+ allocation.add_comment(comment_type="Info", text=text)
def get_monthly_earned_leave(annual_leaves, frequency, rounding):
@@ -425,8 +526,9 @@ def is_earned_leave_already_allocated(allocation, annual_allocation):
date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment)
- leaves_for_passed_months = assignment.get_leaves_for_passed_months(allocation.leave_type,
- annual_allocation, leave_type_details, date_of_joining)
+ leaves_for_passed_months = assignment.get_leaves_for_passed_months(
+ allocation.leave_type, annual_allocation, leave_type_details, date_of_joining
+ )
# exclude carry-forwarded leaves while checking for leave allocation for passed months
num_allocations = allocation.total_leaves_allocated
@@ -439,26 +541,39 @@ def is_earned_leave_already_allocated(allocation, annual_allocation):
def get_leave_allocations(date, leave_type):
- return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy
+ return frappe.db.sql(
+ """select name, employee, from_date, to_date, leave_policy_assignment, leave_policy
from `tabLeave Allocation`
where
%s between from_date and to_date and docstatus=1
and leave_type=%s""",
- (date, leave_type), as_dict=1)
+ (date, leave_type),
+ as_dict=1,
+ )
def get_earned_leaves():
- return frappe.get_all("Leave Type",
- fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding", "based_on_date_of_joining"],
- filters={'is_earned_leave' : 1})
+ return frappe.get_all(
+ "Leave Type",
+ fields=[
+ "name",
+ "max_leaves_allowed",
+ "earned_leave_frequency",
+ "rounding",
+ "based_on_date_of_joining",
+ ],
+ filters={"is_earned_leave": 1},
+ )
+
def create_additional_leave_ledger_entry(allocation, leaves, date):
- ''' Create leave ledger entry for leave types '''
+ """Create leave ledger entry for leave types"""
allocation.new_leaves_allocated = leaves
allocation.from_date = date
allocation.unused_leaves = 0
allocation.create_leave_ledger_entry()
+
def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining):
import calendar
@@ -467,10 +582,12 @@ def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining
from_date = get_datetime(from_date)
to_date = get_datetime(to_date)
rd = relativedelta.relativedelta(to_date, from_date)
- #last day of month
- last_day = calendar.monthrange(to_date.year, to_date.month)[1]
+ # last day of month
+ last_day = calendar.monthrange(to_date.year, to_date.month)[1]
- if (from_date.day == to_date.day and based_on_date_of_joining) or (not based_on_date_of_joining and to_date.day == last_day):
+ if (from_date.day == to_date.day and based_on_date_of_joining) or (
+ not based_on_date_of_joining and to_date.day == last_day
+ ):
if frequency == "Monthly":
return True
elif frequency == "Quarterly" and rd.months % 3:
@@ -487,16 +604,21 @@ def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining
def get_salary_assignment(employee, date):
- assignment = frappe.db.sql("""
+ assignment = frappe.db.sql(
+ """
select * from `tabSalary Structure Assignment`
where employee=%(employee)s
and docstatus = 1
- and %(on_date)s >= from_date order by from_date desc limit 1""", {
- 'employee': employee,
- 'on_date': date,
- }, as_dict=1)
+ and %(on_date)s >= from_date order by from_date desc limit 1""",
+ {
+ "employee": employee,
+ "on_date": date,
+ },
+ as_dict=1,
+ )
return assignment[0] if assignment else None
+
def get_sal_slip_total_benefit_given(employee, payroll_period, component=False):
total_given_benefit_amount = 0
query = """
@@ -514,17 +636,22 @@ def get_sal_slip_total_benefit_given(employee, payroll_period, component=False):
if component:
query += "and sd.salary_component = %(component)s"
- sum_of_given_benefit = frappe.db.sql(query, {
- 'employee': employee,
- 'start_date': payroll_period.start_date,
- 'end_date': payroll_period.end_date,
- 'component': component
- }, as_dict=True)
+ sum_of_given_benefit = frappe.db.sql(
+ query,
+ {
+ "employee": employee,
+ "start_date": payroll_period.start_date,
+ "end_date": payroll_period.end_date,
+ "component": component,
+ },
+ as_dict=True,
+ )
if sum_of_given_benefit and flt(sum_of_given_benefit[0].total_amount) > 0:
total_given_benefit_amount = sum_of_given_benefit[0].total_amount
return total_given_benefit_amount
+
def get_holiday_dates_for_employee(employee, start_date, end_date):
"""return a list of holiday dates for the given employee between start_date and end_date"""
# return only date
@@ -533,50 +660,48 @@ def get_holiday_dates_for_employee(employee, start_date, end_date):
return [cstr(h.holiday_date) for h in holidays]
-def get_holidays_for_employee(employee, start_date, end_date, raise_exception=True, only_non_weekly=False):
+def get_holidays_for_employee(
+ employee, start_date, end_date, raise_exception=True, only_non_weekly=False
+):
"""Get Holidays for a given employee
- `employee` (str)
- `start_date` (str or datetime)
- `end_date` (str or datetime)
- `raise_exception` (bool)
- `only_non_weekly` (bool)
+ `employee` (str)
+ `start_date` (str or datetime)
+ `end_date` (str or datetime)
+ `raise_exception` (bool)
+ `only_non_weekly` (bool)
- return: list of dicts with `holiday_date` and `description`
+ return: list of dicts with `holiday_date` and `description`
"""
holiday_list = get_holiday_list_for_employee(employee, raise_exception=raise_exception)
if not holiday_list:
return []
- filters = {
- 'parent': holiday_list,
- 'holiday_date': ('between', [start_date, end_date])
- }
+ filters = {"parent": holiday_list, "holiday_date": ("between", [start_date, end_date])}
if only_non_weekly:
- filters['weekly_off'] = False
+ filters["weekly_off"] = False
- holidays = frappe.get_all(
- 'Holiday',
- fields=['description', 'holiday_date'],
- filters=filters
- )
+ holidays = frappe.get_all("Holiday", fields=["description", "holiday_date"], filters=filters)
return holidays
+
@erpnext.allow_regional
def calculate_annual_eligible_hra_exemption(doc):
# Don't delete this method, used for localization
# Indian HRA Exemption Calculation
return {}
+
@erpnext.allow_regional
def calculate_hra_exemption_for_period(doc):
# Don't delete this method, used for localization
# Indian HRA Exemption Calculation
return {}
+
def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, component=False):
total_claimed_amount = 0
query = """
@@ -591,24 +716,29 @@ def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, co
if component:
query += "and earning_component = %(component)s"
- sum_of_claimed_amount = frappe.db.sql(query, {
- 'employee': employee,
- 'start_date': payroll_period.start_date,
- 'end_date': payroll_period.end_date,
- 'component': component
- }, as_dict=True)
+ sum_of_claimed_amount = frappe.db.sql(
+ query,
+ {
+ "employee": employee,
+ "start_date": payroll_period.start_date,
+ "end_date": payroll_period.end_date,
+ "component": component,
+ },
+ as_dict=True,
+ )
if sum_of_claimed_amount and flt(sum_of_claimed_amount[0].total_amount) > 0:
total_claimed_amount = sum_of_claimed_amount[0].total_amount
return total_claimed_amount
+
def share_doc_with_approver(doc, user):
# if approver does not have permissions, share
if not frappe.has_permission(doc=doc, ptype="submit", user=user):
- frappe.share.add(doc.doctype, doc.name, user, submit=1,
- flags={"ignore_share_permission": True})
+ frappe.share.add(doc.doctype, doc.name, user, submit=1, flags={"ignore_share_permission": True})
- frappe.msgprint(_("Shared with the user {0} with {1} access").format(
- user, frappe.bold("submit"), alert=True))
+ frappe.msgprint(
+ _("Shared with the user {0} with {1} access").format(user, frappe.bold("submit"), alert=True)
+ )
# remove shared doc if approver changes
doc_before_save = doc.get_doc_before_save()
@@ -616,14 +746,19 @@ def share_doc_with_approver(doc, user):
approvers = {
"Leave Application": "leave_approver",
"Expense Claim": "expense_approver",
- "Shift Request": "approver"
+ "Shift Request": "approver",
}
approver = approvers.get(doc.doctype)
if doc_before_save.get(approver) != doc.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)
+ frappe.throw(
+ _("Transactions cannot be created for an Inactive Employee {0}.").format(
+ get_link_to_form("Employee", employee)
+ ),
+ InactiveEmployeeStatusError,
+ )
diff --git a/erpnext/hr/web_form/job_application/job_application.py b/erpnext/hr/web_form/job_application/job_application.py
index 19b550feea7..02e3e933330 100644
--- a/erpnext/hr/web_form/job_application/job_application.py
+++ b/erpnext/hr/web_form/job_application/job_application.py
@@ -1,5 +1,3 @@
-
-
def get_context(context):
# do your magic here
pass
diff --git a/erpnext/hub_node/__init__.py b/erpnext/hub_node/__init__.py
index 2bfbfd1088e..4ff2e2c9dd0 100644
--- a/erpnext/hub_node/__init__.py
+++ b/erpnext/hub_node/__init__.py
@@ -7,12 +7,13 @@ import frappe
@frappe.whitelist()
def enable_hub():
- hub_settings = frappe.get_doc('Marketplace Settings')
+ hub_settings = frappe.get_doc("Marketplace Settings")
hub_settings.register()
frappe.db.commit()
return hub_settings
+
@frappe.whitelist()
def sync():
- hub_settings = frappe.get_doc('Marketplace Settings')
+ hub_settings = frappe.get_doc("Marketplace Settings")
hub_settings.sync()
diff --git a/erpnext/hub_node/api.py b/erpnext/hub_node/api.py
index 1bf7b8c3b52..410c508de06 100644
--- a/erpnext/hub_node/api.py
+++ b/erpnext/hub_node/api.py
@@ -1,4 +1,3 @@
-
import json
import frappe
@@ -14,24 +13,24 @@ current_user = frappe.session.user
def register_marketplace(company, company_description):
validate_registerer()
- settings = frappe.get_single('Marketplace Settings')
+ settings = frappe.get_single("Marketplace Settings")
message = settings.register_seller(company, company_description)
- if message.get('hub_seller_name'):
+ if message.get("hub_seller_name"):
settings.registered = 1
- settings.hub_seller_name = message.get('hub_seller_name')
+ settings.hub_seller_name = message.get("hub_seller_name")
settings.save()
settings.add_hub_user(frappe.session.user)
- return { 'ok': 1 }
+ return {"ok": 1}
@frappe.whitelist()
def register_users(user_list):
user_list = json.loads(user_list)
- settings = frappe.get_single('Marketplace Settings')
+ settings = frappe.get_single("Marketplace Settings")
for user in user_list:
settings.add_hub_user(user)
@@ -40,14 +39,16 @@ def register_users(user_list):
def validate_registerer():
- if current_user == 'Administrator':
- frappe.throw(_('Please login as another user to register on Marketplace'))
+ if current_user == "Administrator":
+ frappe.throw(_("Please login as another user to register on Marketplace"))
- valid_roles = ['System Manager', 'Item Manager']
+ valid_roles = ["System Manager", "Item Manager"]
if not frappe.utils.is_subset(valid_roles, frappe.get_roles()):
- frappe.throw(_('Only users with {0} role can register on Marketplace').format(', '.join(valid_roles)),
- frappe.PermissionError)
+ frappe.throw(
+ _("Only users with {0} role can register on Marketplace").format(", ".join(valid_roles)),
+ frappe.PermissionError,
+ )
@frappe.whitelist()
@@ -57,9 +58,7 @@ def call_hub_method(method, params=None):
if isinstance(params, string_types):
params = json.loads(params)
- params.update({
- 'cmd': 'hub.hub.api.' + method
- })
+ params.update({"cmd": "hub.hub.api." + method})
response = connection.post_request(params)
return response
@@ -67,81 +66,81 @@ def call_hub_method(method, params=None):
def map_fields(items):
field_mappings = get_field_mappings()
- table_fields = [d.fieldname for d in frappe.get_meta('Item').get_table_fields()]
+ table_fields = [d.fieldname for d in frappe.get_meta("Item").get_table_fields()]
- hub_seller_name = frappe.db.get_value('Marketplace Settings', 'Marketplace Settings', 'hub_seller_name')
+ hub_seller_name = frappe.db.get_value(
+ "Marketplace Settings", "Marketplace Settings", "hub_seller_name"
+ )
for item in items:
for fieldname in table_fields:
item.pop(fieldname, None)
for mapping in field_mappings:
- local_fieldname = mapping.get('local_fieldname')
- remote_fieldname = mapping.get('remote_fieldname')
+ local_fieldname = mapping.get("local_fieldname")
+ remote_fieldname = mapping.get("remote_fieldname")
value = item.get(local_fieldname)
item.pop(local_fieldname, None)
item[remote_fieldname] = value
- item['doctype'] = 'Hub Item'
- item['hub_seller'] = hub_seller_name
- item.pop('attachments', None)
+ item["doctype"] = "Hub Item"
+ item["hub_seller"] = hub_seller_name
+ item.pop("attachments", None)
return items
@frappe.whitelist()
-def get_valid_items(search_value=''):
+def get_valid_items(search_value=""):
items = frappe.get_list(
- 'Item',
+ "Item",
fields=["*"],
- filters={
- 'disabled': 0,
- 'item_name': ['like', '%' + search_value + '%'],
- 'publish_in_hub': 0
- },
- order_by="modified desc"
+ filters={"disabled": 0, "item_name": ["like", "%" + search_value + "%"], "publish_in_hub": 0},
+ order_by="modified desc",
)
valid_items = filter(lambda x: x.image and x.description, items)
def prepare_item(item):
item.source_type = "local"
- item.attachments = get_attachments('Item', item.item_code)
+ item.attachments = get_attachments("Item", item.item_code)
return item
valid_items = map(prepare_item, valid_items)
return valid_items
+
@frappe.whitelist()
def update_item(ref_doc, data):
data = json.loads(data)
- data.update(dict(doctype='Hub Item', name=ref_doc))
+ data.update(dict(doctype="Hub Item", name=ref_doc))
try:
connection = get_hub_connection()
connection.update(data)
except Exception as e:
- frappe.log_error(message=e, title='Hub Sync Error')
+ frappe.log_error(message=e, title="Hub Sync Error")
+
@frappe.whitelist()
def publish_selected_items(items_to_publish):
items_to_publish = json.loads(items_to_publish)
items_to_update = []
if not len(items_to_publish):
- frappe.throw(_('No items to publish'))
+ frappe.throw(_("No items to publish"))
for item in items_to_publish:
- item_code = item.get('item_code')
- frappe.db.set_value('Item', item_code, 'publish_in_hub', 1)
+ item_code = item.get("item_code")
+ frappe.db.set_value("Item", item_code, "publish_in_hub", 1)
hub_dict = {
- 'doctype': 'Hub Tracked Item',
- 'item_code': item_code,
- 'published': 1,
- 'hub_category': item.get('hub_category'),
- 'image_list': item.get('image_list')
+ "doctype": "Hub Tracked Item",
+ "item_code": item_code,
+ "published": 1,
+ "hub_category": item.get("hub_category"),
+ "image_list": item.get("image_list"),
}
frappe.get_doc(hub_dict).insert(ignore_if_duplicate=True)
@@ -157,65 +156,67 @@ def publish_selected_items(items_to_publish):
item_sync_postprocess()
except Exception as e:
- frappe.log_error(message=e, title='Hub Sync Error')
+ frappe.log_error(message=e, title="Hub Sync Error")
+
@frappe.whitelist()
def unpublish_item(item_code, hub_item_name):
- ''' Remove item listing from the marketplace '''
+ """Remove item listing from the marketplace"""
- response = call_hub_method('unpublish_item', {
- 'hub_item_name': hub_item_name
- })
+ response = call_hub_method("unpublish_item", {"hub_item_name": hub_item_name})
if response:
- frappe.db.set_value('Item', item_code, 'publish_in_hub', 0)
- frappe.delete_doc('Hub Tracked Item', item_code)
+ frappe.db.set_value("Item", item_code, "publish_in_hub", 0)
+ frappe.delete_doc("Hub Tracked Item", item_code)
else:
- frappe.throw(_('Unable to update remote activity'))
+ frappe.throw(_("Unable to update remote activity"))
+
@frappe.whitelist()
def get_unregistered_users():
- settings = frappe.get_single('Marketplace Settings')
- registered_users = [user.user for user in settings.users] + ['Administrator', 'Guest']
- all_users = [user.name for user in frappe.db.get_all('User', filters={'enabled': 1})]
+ settings = frappe.get_single("Marketplace Settings")
+ registered_users = [user.user for user in settings.users] + ["Administrator", "Guest"]
+ all_users = [user.name for user in frappe.db.get_all("User", filters={"enabled": 1})]
unregistered_users = [user for user in all_users if user not in registered_users]
return unregistered_users
def item_sync_preprocess(intended_item_publish_count):
- response = call_hub_method('pre_items_publish', {
- 'intended_item_publish_count': intended_item_publish_count
- })
+ response = call_hub_method(
+ "pre_items_publish", {"intended_item_publish_count": intended_item_publish_count}
+ )
if response:
frappe.db.set_value("Marketplace Settings", "Marketplace Settings", "sync_in_progress", 1)
return response
else:
- frappe.throw(_('Unable to update remote activity'))
+ frappe.throw(_("Unable to update remote activity"))
def item_sync_postprocess():
- response = call_hub_method('post_items_publish', {})
+ response = call_hub_method("post_items_publish", {})
if response:
- frappe.db.set_value('Marketplace Settings', 'Marketplace Settings', 'last_sync_datetime', frappe.utils.now())
+ frappe.db.set_value(
+ "Marketplace Settings", "Marketplace Settings", "last_sync_datetime", frappe.utils.now()
+ )
else:
- frappe.throw(_('Unable to update remote activity'))
+ frappe.throw(_("Unable to update remote activity"))
- frappe.db.set_value('Marketplace Settings', 'Marketplace Settings', 'sync_in_progress', 0)
+ frappe.db.set_value("Marketplace Settings", "Marketplace Settings", "sync_in_progress", 0)
def convert_relative_image_urls_to_absolute(items):
from six.moves.urllib.parse import urljoin
for item in items:
- file_path = item['image']
+ file_path = item["image"]
- if file_path.startswith('/files/'):
- item['image'] = urljoin(frappe.utils.get_url(), file_path)
+ if file_path.startswith("/files/"):
+ item["image"] = urljoin(frappe.utils.get_url(), file_path)
def get_hub_connection():
- settings = frappe.get_single('Marketplace Settings')
+ settings = frappe.get_single("Marketplace Settings")
marketplace_url = settings.marketplace_url
hub_user = settings.get_hub_user(frappe.session.user)
diff --git a/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.py b/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.py
index af2ff37797e..e8b68cbc0e7 100644
--- a/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.py
+++ b/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.py
@@ -10,82 +10,82 @@ from frappe.utils import cint
class MarketplaceSettings(Document):
-
def register_seller(self, company, company_description):
- country, currency, company_logo = frappe.db.get_value('Company', company,
- ['country', 'default_currency', 'company_logo'])
+ country, currency, company_logo = frappe.db.get_value(
+ "Company", company, ["country", "default_currency", "company_logo"]
+ )
company_details = {
- 'company': company,
- 'country': country,
- 'currency': currency,
- 'company_description': company_description,
- 'company_logo': company_logo,
- 'site_name': frappe.utils.get_url()
+ "company": company,
+ "country": country,
+ "currency": currency,
+ "company_description": company_description,
+ "company_logo": company_logo,
+ "site_name": frappe.utils.get_url(),
}
hub_connection = self.get_connection()
- response = hub_connection.post_request({
- 'cmd': 'hub.hub.api.add_hub_seller',
- 'company_details': json.dumps(company_details)
- })
+ response = hub_connection.post_request(
+ {"cmd": "hub.hub.api.add_hub_seller", "company_details": json.dumps(company_details)}
+ )
return response
-
def add_hub_user(self, user_email):
- '''Create a Hub User and User record on hub server
+ """Create a Hub User and User record on hub server
and if successfull append it to Hub User table
- '''
+ """
if not self.registered:
return
hub_connection = self.get_connection()
- first_name, last_name = frappe.db.get_value('User', user_email, ['first_name', 'last_name'])
+ first_name, last_name = frappe.db.get_value("User", user_email, ["first_name", "last_name"])
- hub_user = hub_connection.post_request({
- 'cmd': 'hub.hub.api.add_hub_user',
- 'user_email': user_email,
- 'first_name': first_name,
- 'last_name': last_name,
- 'hub_seller': self.hub_seller_name
- })
+ hub_user = hub_connection.post_request(
+ {
+ "cmd": "hub.hub.api.add_hub_user",
+ "user_email": user_email,
+ "first_name": first_name,
+ "last_name": last_name,
+ "hub_seller": self.hub_seller_name,
+ }
+ )
- self.append('users', {
- 'user': hub_user.get('user_email'),
- 'hub_user_name': hub_user.get('hub_user_name'),
- 'password': hub_user.get('password')
- })
+ self.append(
+ "users",
+ {
+ "user": hub_user.get("user_email"),
+ "hub_user_name": hub_user.get("hub_user_name"),
+ "password": hub_user.get("password"),
+ },
+ )
self.save()
def get_hub_user(self, user):
- '''Return the Hub User doc from the `users` table if password is set'''
+ """Return the Hub User doc from the `users` table if password is set"""
- filtered_users = list(filter(
- lambda x: x.user == user and x.password,
- self.users
- ))
+ filtered_users = list(filter(lambda x: x.user == user and x.password, self.users))
if filtered_users:
return filtered_users[0]
-
def get_connection(self):
return FrappeClient(self.marketplace_url)
-
def unregister(self):
"""Disable the User on hubmarket.org"""
+
@frappe.whitelist()
def is_marketplace_enabled():
- if not hasattr(frappe.local, 'is_marketplace_enabled'):
- frappe.local.is_marketplace_enabled = cint(frappe.db.get_single_value('Marketplace Settings',
- 'disable_marketplace'))
+ if not hasattr(frappe.local, "is_marketplace_enabled"):
+ frappe.local.is_marketplace_enabled = cint(
+ frappe.db.get_single_value("Marketplace Settings", "disable_marketplace")
+ )
return frappe.local.is_marketplace_enabled
diff --git a/erpnext/hub_node/legacy.py b/erpnext/hub_node/legacy.py
index b19167bfefe..8d95c0706c2 100644
--- a/erpnext/hub_node/legacy.py
+++ b/erpnext/hub_node/legacy.py
@@ -1,4 +1,3 @@
-
import json
import frappe
@@ -11,9 +10,10 @@ from frappe.utils.nestedset import get_root_of
def get_list(doctype, start, limit, fields, filters, order_by):
pass
+
def get_hub_connection():
- if frappe.db.exists('Data Migration Connector', 'Hub Connector'):
- hub_connector = frappe.get_doc('Data Migration Connector', 'Hub Connector')
+ if frappe.db.exists("Data Migration Connector", "Hub Connector"):
+ hub_connector = frappe.get_doc("Data Migration Connector", "Hub Connector")
hub_connection = hub_connector.get_connection()
return hub_connection.connection
@@ -21,10 +21,11 @@ def get_hub_connection():
hub_connection = FrappeClient(frappe.conf.hub_url)
return hub_connection
+
def make_opportunity(buyer_name, email_id):
buyer_name = "HUB-" + buyer_name
- if not frappe.db.exists('Lead', {'email_id': email_id}):
+ if not frappe.db.exists("Lead", {"email_id": email_id}):
lead = frappe.new_doc("Lead")
lead.lead_name = buyer_name
lead.email_id = email_id
@@ -32,9 +33,10 @@ def make_opportunity(buyer_name, email_id):
o = frappe.new_doc("Opportunity")
o.opportunity_from = "Lead"
- o.lead = frappe.get_all("Lead", filters={"email_id": email_id}, fields = ["name"])[0]["name"]
+ o.lead = frappe.get_all("Lead", filters={"email_id": email_id}, fields=["name"])[0]["name"]
o.save(ignore_permissions=True)
+
@frappe.whitelist()
def make_rfq_and_send_opportunity(item, supplier):
supplier = make_supplier(supplier)
@@ -43,105 +45,110 @@ def make_rfq_and_send_opportunity(item, supplier):
rfq = make_rfq(item, supplier, contact)
status = send_opportunity(contact)
- return {
- 'rfq': rfq,
- 'hub_document_created': status
- }
+ return {"rfq": rfq, "hub_document_created": status}
+
def make_supplier(supplier):
# make supplier if not already exists
supplier = frappe._dict(json.loads(supplier))
- if not frappe.db.exists('Supplier', {'supplier_name': supplier.supplier_name}):
- supplier_doc = frappe.get_doc({
- 'doctype': 'Supplier',
- 'supplier_name': supplier.supplier_name,
- 'supplier_group': supplier.supplier_group,
- 'supplier_email': supplier.supplier_email
- }).insert()
+ if not frappe.db.exists("Supplier", {"supplier_name": supplier.supplier_name}):
+ supplier_doc = frappe.get_doc(
+ {
+ "doctype": "Supplier",
+ "supplier_name": supplier.supplier_name,
+ "supplier_group": supplier.supplier_group,
+ "supplier_email": supplier.supplier_email,
+ }
+ ).insert()
else:
- supplier_doc = frappe.get_doc('Supplier', supplier.supplier_name)
+ supplier_doc = frappe.get_doc("Supplier", supplier.supplier_name)
return supplier_doc
+
def make_contact(supplier):
- contact_name = get_default_contact('Supplier', supplier.supplier_name)
+ contact_name = get_default_contact("Supplier", supplier.supplier_name)
# make contact if not already exists
if not contact_name:
- contact = frappe.get_doc({
- 'doctype': 'Contact',
- 'first_name': supplier.supplier_name,
- 'is_primary_contact': 1,
- 'links': [
- {'link_doctype': 'Supplier', 'link_name': supplier.supplier_name}
- ]
- })
+ contact = frappe.get_doc(
+ {
+ "doctype": "Contact",
+ "first_name": supplier.supplier_name,
+ "is_primary_contact": 1,
+ "links": [{"link_doctype": "Supplier", "link_name": supplier.supplier_name}],
+ }
+ )
contact.add_email(supplier.supplier_email, is_primary=True)
contact.insert()
else:
- contact = frappe.get_doc('Contact', contact_name)
+ contact = frappe.get_doc("Contact", contact_name)
return contact
+
def make_item(item):
# make item if not already exists
item = frappe._dict(json.loads(item))
- if not frappe.db.exists('Item', {'item_code': item.item_code}):
- item_doc = frappe.get_doc({
- 'doctype': 'Item',
- 'item_code': item.item_code,
- 'item_group': item.item_group,
- 'is_item_from_hub': 1
- }).insert()
+ if not frappe.db.exists("Item", {"item_code": item.item_code}):
+ item_doc = frappe.get_doc(
+ {
+ "doctype": "Item",
+ "item_code": item.item_code,
+ "item_group": item.item_group,
+ "is_item_from_hub": 1,
+ }
+ ).insert()
else:
- item_doc = frappe.get_doc('Item', item.item_code)
+ item_doc = frappe.get_doc("Item", item.item_code)
return item_doc
+
def make_rfq(item, supplier, contact):
# make rfq
- rfq = frappe.get_doc({
- 'doctype': 'Request for Quotation',
- 'transaction_date': nowdate(),
- 'status': 'Draft',
- 'company': frappe.db.get_single_value('Marketplace Settings', 'company'),
- 'message_for_supplier': 'Please supply the specified items at the best possible rates',
- 'suppliers': [
- { 'supplier': supplier.name, 'contact': contact.name }
- ],
- 'items': [
- {
- 'item_code': item.item_code,
- 'qty': 1,
- 'schedule_date': nowdate(),
- 'warehouse': item.default_warehouse or get_root_of("Warehouse"),
- 'description': item.description,
- 'uom': item.stock_uom
- }
- ]
- }).insert()
+ rfq = frappe.get_doc(
+ {
+ "doctype": "Request for Quotation",
+ "transaction_date": nowdate(),
+ "status": "Draft",
+ "company": frappe.db.get_single_value("Marketplace Settings", "company"),
+ "message_for_supplier": "Please supply the specified items at the best possible rates",
+ "suppliers": [{"supplier": supplier.name, "contact": contact.name}],
+ "items": [
+ {
+ "item_code": item.item_code,
+ "qty": 1,
+ "schedule_date": nowdate(),
+ "warehouse": item.default_warehouse or get_root_of("Warehouse"),
+ "description": item.description,
+ "uom": item.stock_uom,
+ }
+ ],
+ }
+ ).insert()
rfq.save()
rfq.submit()
return rfq
+
def send_opportunity(contact):
# Make Hub Message on Hub with lead data
doc = {
- 'doctype': 'Lead',
- 'lead_name': frappe.db.get_single_value('Marketplace Settings', 'company'),
- 'email_id': frappe.db.get_single_value('Marketplace Settings', 'user')
+ "doctype": "Lead",
+ "lead_name": frappe.db.get_single_value("Marketplace Settings", "company"),
+ "email_id": frappe.db.get_single_value("Marketplace Settings", "user"),
}
- args = frappe._dict(dict(
- doctype='Hub Message',
- reference_doctype='Lead',
- data=json.dumps(doc),
- user=contact.email_id
- ))
+ args = frappe._dict(
+ dict(
+ doctype="Hub Message", reference_doctype="Lead", data=json.dumps(doc), user=contact.email_id
+ )
+ )
connection = get_hub_connection()
- response = connection.insert('Hub Message', args)
+ response = connection.insert("Hub Message", args)
return response.ok
diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py
index 9512c8fa195..03eed801fcb 100644
--- a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py
+++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py
@@ -13,10 +13,19 @@ from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applic
@frappe.whitelist()
@cache_source
-def get_data(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None,
- to_date = None, timespan = None, time_interval = None, heatmap_year = None):
+def get_data(
+ chart_name=None,
+ chart=None,
+ no_cache=None,
+ filters=None,
+ from_date=None,
+ to_date=None,
+ timespan=None,
+ time_interval=None,
+ heatmap_year=None,
+):
if chart_name:
- chart = frappe.get_doc('Dashboard Chart', chart_name)
+ chart = frappe.get_doc("Dashboard Chart", chart_name)
else:
chart = frappe._dict(frappe.parse_json(chart))
@@ -30,28 +39,44 @@ def get_data(chart_name = None, chart = None, no_cache = None, filters = None, f
labels = []
values = []
- if filters.get('company'):
+ if filters.get("company"):
conditions = "AND company = %(company)s"
loan_security_details = get_loan_security_details()
- unpledges = frappe._dict(frappe.db.sql("""
+ unpledges = frappe._dict(
+ frappe.db.sql(
+ """
SELECT u.loan_security, sum(u.qty) as qty
FROM `tabLoan Security Unpledge` up, `tabUnpledge` u
WHERE u.parent = up.name
AND up.status = 'Approved'
{conditions}
GROUP BY u.loan_security
- """.format(conditions=conditions), filters, as_list=1))
+ """.format(
+ conditions=conditions
+ ),
+ filters,
+ as_list=1,
+ )
+ )
- pledges = frappe._dict(frappe.db.sql("""
+ pledges = frappe._dict(
+ frappe.db.sql(
+ """
SELECT p.loan_security, sum(p.qty) as qty
FROM `tabLoan Security Pledge` lp, `tabPledge`p
WHERE p.parent = lp.name
AND lp.status = 'Pledged'
{conditions}
GROUP BY p.loan_security
- """.format(conditions=conditions), filters, as_list=1))
+ """.format(
+ conditions=conditions
+ ),
+ filters,
+ as_list=1,
+ )
+ )
for security, qty in iteritems(pledges):
current_pledges.setdefault(security, qty)
@@ -61,19 +86,15 @@ def get_data(chart_name = None, chart = None, no_cache = None, filters = None, f
count = 0
for security, qty in iteritems(sorted_pledges):
- values.append(qty * loan_security_details.get(security, {}).get('latest_price', 0))
+ values.append(qty * loan_security_details.get(security, {}).get("latest_price", 0))
labels.append(security)
- count +=1
+ count += 1
## Just need top 10 securities
if count == 10:
break
return {
- 'labels': labels,
- 'datasets': [{
- 'name': 'Top 10 Securities',
- 'chartType': 'bar',
- 'values': values
- }]
+ "labels": labels,
+ "datasets": [{"name": "Top 10 Securities", "chartType": "bar", "values": values}],
}
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index f3914d51286..03ec4014eec 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -21,7 +21,7 @@ from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpled
class Loan(AccountsController):
def validate(self):
- if self.applicant_type == 'Employee' and self.repay_from_salary:
+ if self.applicant_type == "Employee" and self.repay_from_salary:
validate_employee_currency_with_company_currency(self.applicant, self.company)
self.set_loan_amount()
self.validate_loan_amount()
@@ -31,27 +31,40 @@ class Loan(AccountsController):
self.validate_repay_from_salary()
if self.is_term_loan:
- validate_repayment_method(self.repayment_method, self.loan_amount, self.monthly_repayment_amount,
- self.repayment_periods, self.is_term_loan)
+ validate_repayment_method(
+ self.repayment_method,
+ self.loan_amount,
+ self.monthly_repayment_amount,
+ self.repayment_periods,
+ self.is_term_loan,
+ )
self.make_repayment_schedule()
self.set_repayment_period()
self.calculate_totals()
def validate_accounts(self):
- for fieldname in ['payment_account', 'loan_account', 'interest_income_account', 'penalty_income_account']:
- company = frappe.get_value("Account", self.get(fieldname), 'company')
+ for fieldname in [
+ "payment_account",
+ "loan_account",
+ "interest_income_account",
+ "penalty_income_account",
+ ]:
+ company = frappe.get_value("Account", self.get(fieldname), "company")
if company != self.company:
- frappe.throw(_("Account {0} does not belongs to company {1}").format(frappe.bold(self.get(fieldname)),
- frappe.bold(self.company)))
+ frappe.throw(
+ _("Account {0} does not belongs to company {1}").format(
+ frappe.bold(self.get(fieldname)), frappe.bold(self.company)
+ )
+ )
def on_submit(self):
self.link_loan_security_pledge()
def on_cancel(self):
self.unlink_loan_security_pledge()
- self.ignore_linked_doctypes = ['GL Entry']
+ self.ignore_linked_doctypes = ["GL Entry"]
def set_missing_fields(self):
if not self.company:
@@ -64,15 +77,25 @@ class Loan(AccountsController):
self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest")
if self.repayment_method == "Repay Over Number of Periods":
- self.monthly_repayment_amount = get_monthly_repayment_amount(self.loan_amount, self.rate_of_interest, self.repayment_periods)
+ self.monthly_repayment_amount = get_monthly_repayment_amount(
+ self.loan_amount, self.rate_of_interest, self.repayment_periods
+ )
def check_sanctioned_amount_limit(self):
- sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
+ sanctioned_amount_limit = get_sanctioned_amount_limit(
+ self.applicant_type, self.applicant, self.company
+ )
if sanctioned_amount_limit:
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
- if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit):
- frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))
+ if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(
+ sanctioned_amount_limit
+ ):
+ frappe.throw(
+ _("Sanctioned Amount limit crossed for {0} {1}").format(
+ self.applicant_type, frappe.bold(self.applicant)
+ )
+ )
def validate_repay_from_salary(self):
if not self.is_term_loan and self.repay_from_salary:
@@ -85,8 +108,8 @@ class Loan(AccountsController):
self.repayment_schedule = []
payment_date = self.repayment_start_date
balance_amount = self.loan_amount
- while(balance_amount > 0):
- interest_amount = flt(balance_amount * flt(self.rate_of_interest) / (12*100))
+ while balance_amount > 0:
+ interest_amount = flt(balance_amount * flt(self.rate_of_interest) / (12 * 100))
principal_amount = self.monthly_repayment_amount - interest_amount
balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount)
if balance_amount < 0:
@@ -94,13 +117,16 @@ class Loan(AccountsController):
balance_amount = 0.0
total_payment = principal_amount + interest_amount
- self.append("repayment_schedule", {
- "payment_date": payment_date,
- "principal_amount": principal_amount,
- "interest_amount": interest_amount,
- "total_payment": total_payment,
- "balance_loan_amount": balance_amount
- })
+ self.append(
+ "repayment_schedule",
+ {
+ "payment_date": payment_date,
+ "principal_amount": principal_amount,
+ "interest_amount": interest_amount,
+ "total_payment": total_payment,
+ "balance_loan_amount": balance_amount,
+ },
+ )
next_payment_date = add_single_month(payment_date)
payment_date = next_payment_date
@@ -118,14 +144,13 @@ class Loan(AccountsController):
if self.is_term_loan:
for data in self.repayment_schedule:
self.total_payment += data.total_payment
- self.total_interest_payable +=data.interest_amount
+ self.total_interest_payable += data.interest_amount
else:
self.total_payment = self.loan_amount
def set_loan_amount(self):
if self.loan_application and not self.loan_amount:
- self.loan_amount = frappe.db.get_value('Loan Application', self.loan_application, 'loan_amount')
-
+ self.loan_amount = frappe.db.get_value("Loan Application", self.loan_application, "loan_amount")
def validate_loan_amount(self):
if self.maximum_loan_amount and self.loan_amount > self.maximum_loan_amount:
@@ -137,30 +162,36 @@ class Loan(AccountsController):
def link_loan_security_pledge(self):
if self.is_secured_loan and self.loan_application:
- maximum_loan_value = frappe.db.get_value('Loan Security Pledge',
- {
- 'loan_application': self.loan_application,
- 'status': 'Requested'
- },
- 'sum(maximum_loan_value)'
+ maximum_loan_value = frappe.db.get_value(
+ "Loan Security Pledge",
+ {"loan_application": self.loan_application, "status": "Requested"},
+ "sum(maximum_loan_value)",
)
if maximum_loan_value:
- frappe.db.sql("""
+ frappe.db.sql(
+ """
UPDATE `tabLoan Security Pledge`
SET loan = %s, pledge_time = %s, status = 'Pledged'
WHERE status = 'Requested' and loan_application = %s
- """, (self.name, now_datetime(), self.loan_application))
+ """,
+ (self.name, now_datetime(), self.loan_application),
+ )
- self.db_set('maximum_loan_amount', maximum_loan_value)
+ self.db_set("maximum_loan_amount", maximum_loan_value)
def unlink_loan_security_pledge(self):
- pledges = frappe.get_all('Loan Security Pledge', fields=['name'], filters={'loan': self.name})
+ pledges = frappe.get_all("Loan Security Pledge", fields=["name"], filters={"loan": self.name})
pledge_list = [d.name for d in pledges]
if pledge_list:
- frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET
+ frappe.db.sql(
+ """UPDATE `tabLoan Security Pledge` SET
loan = '', status = 'Unpledged'
- where name in (%s) """ % (', '.join(['%s']*len(pledge_list))), tuple(pledge_list)) #nosec
+ where name in (%s) """
+ % (", ".join(["%s"] * len(pledge_list))),
+ tuple(pledge_list),
+ ) # nosec
+
def update_total_amount_paid(doc):
total_amount_paid = 0
@@ -169,24 +200,51 @@ def update_total_amount_paid(doc):
total_amount_paid += data.total_payment
frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid)
+
def get_total_loan_amount(applicant_type, applicant, company):
pending_amount = 0
- loan_details = frappe.db.get_all("Loan",
- filters={"applicant_type": applicant_type, "company": company, "applicant": applicant, "docstatus": 1,
- "status": ("!=", "Closed")},
- fields=["status", "total_payment", "disbursed_amount", "total_interest_payable", "total_principal_paid",
- "written_off_amount"])
+ loan_details = frappe.db.get_all(
+ "Loan",
+ filters={
+ "applicant_type": applicant_type,
+ "company": company,
+ "applicant": applicant,
+ "docstatus": 1,
+ "status": ("!=", "Closed"),
+ },
+ fields=[
+ "status",
+ "total_payment",
+ "disbursed_amount",
+ "total_interest_payable",
+ "total_principal_paid",
+ "written_off_amount",
+ ],
+ )
- interest_amount = flt(frappe.db.get_value("Loan Interest Accrual", {"applicant_type": applicant_type,
- "company": company, "applicant": applicant, "docstatus": 1}, "sum(interest_amount - paid_interest_amount)"))
+ interest_amount = flt(
+ frappe.db.get_value(
+ "Loan Interest Accrual",
+ {"applicant_type": applicant_type, "company": company, "applicant": applicant, "docstatus": 1},
+ "sum(interest_amount - paid_interest_amount)",
+ )
+ )
for loan in loan_details:
if loan.status in ("Disbursed", "Loan Closure Requested"):
- pending_amount += flt(loan.total_payment) - flt(loan.total_interest_payable) \
- - flt(loan.total_principal_paid) - flt(loan.written_off_amount)
+ pending_amount += (
+ flt(loan.total_payment)
+ - flt(loan.total_interest_payable)
+ - flt(loan.total_principal_paid)
+ - flt(loan.written_off_amount)
+ )
elif loan.status == "Partially Disbursed":
- pending_amount += flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \
- - flt(loan.total_principal_paid) - flt(loan.written_off_amount)
+ pending_amount += (
+ flt(loan.disbursed_amount)
+ - flt(loan.total_interest_payable)
+ - flt(loan.total_principal_paid)
+ - flt(loan.written_off_amount)
+ )
elif loan.status == "Sanctioned":
pending_amount += flt(loan.total_payment)
@@ -194,12 +252,18 @@ def get_total_loan_amount(applicant_type, applicant, company):
return pending_amount
-def get_sanctioned_amount_limit(applicant_type, applicant, company):
- return frappe.db.get_value('Sanctioned Loan Amount',
- {'applicant_type': applicant_type, 'company': company, 'applicant': applicant},
- 'sanctioned_amount_limit')
-def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_amount, repayment_periods, is_term_loan):
+def get_sanctioned_amount_limit(applicant_type, applicant, company):
+ return frappe.db.get_value(
+ "Sanctioned Loan Amount",
+ {"applicant_type": applicant_type, "company": company, "applicant": applicant},
+ "sanctioned_amount_limit",
+ )
+
+
+def validate_repayment_method(
+ repayment_method, loan_amount, monthly_repayment_amount, repayment_periods, is_term_loan
+):
if is_term_loan and not repayment_method:
frappe.throw(_("Repayment Method is mandatory for term loans"))
@@ -213,27 +277,34 @@ def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_a
if monthly_repayment_amount > loan_amount:
frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount"))
+
def get_monthly_repayment_amount(loan_amount, rate_of_interest, repayment_periods):
if rate_of_interest:
- monthly_interest_rate = flt(rate_of_interest) / (12 *100)
- monthly_repayment_amount = math.ceil((loan_amount * monthly_interest_rate *
- (1 + monthly_interest_rate)**repayment_periods) \
- / ((1 + monthly_interest_rate)**repayment_periods - 1))
+ monthly_interest_rate = flt(rate_of_interest) / (12 * 100)
+ monthly_repayment_amount = math.ceil(
+ (loan_amount * monthly_interest_rate * (1 + monthly_interest_rate) ** repayment_periods)
+ / ((1 + monthly_interest_rate) ** repayment_periods - 1)
+ )
else:
monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods)
return monthly_repayment_amount
+
@frappe.whitelist()
def request_loan_closure(loan, posting_date=None):
if not posting_date:
posting_date = getdate()
amounts = calculate_amounts(loan, posting_date)
- pending_amount = amounts['pending_principal_amount'] + amounts['unaccrued_interest'] + \
- amounts['interest_amount'] + amounts['penalty_amount']
+ pending_amount = (
+ amounts["pending_principal_amount"]
+ + amounts["unaccrued_interest"]
+ + amounts["interest_amount"]
+ + amounts["penalty_amount"]
+ )
- loan_type = frappe.get_value('Loan', loan, 'loan_type')
- write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount')
+ loan_type = frappe.get_value("Loan", loan, "loan_type")
+ write_off_limit = frappe.get_value("Loan Type", loan_type, "write_off_amount")
if pending_amount and abs(pending_amount) < write_off_limit:
# Auto create loan write off and update status as loan closure requested
@@ -242,7 +313,8 @@ def request_loan_closure(loan, posting_date=None):
elif pending_amount > 0:
frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))
- frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
+ frappe.db.set_value("Loan", loan, "status", "Loan Closure Requested")
+
@frappe.whitelist()
def get_loan_application(loan_application):
@@ -250,10 +322,12 @@ def get_loan_application(loan_application):
if loan:
return loan.as_dict()
+
def close_loan(loan, total_amount_paid):
frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid)
frappe.db.set_value("Loan", loan, "status", "Closed")
+
@frappe.whitelist()
def make_loan_disbursement(loan, company, applicant_type, applicant, pending_amount=0, as_dict=0):
disbursement_entry = frappe.new_doc("Loan Disbursement")
@@ -270,6 +344,7 @@ def make_loan_disbursement(loan, company, applicant_type, applicant, pending_amo
else:
return disbursement_entry
+
@frappe.whitelist()
def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as_dict=0):
repayment_entry = frappe.new_doc("Loan Repayment")
@@ -285,27 +360,28 @@ def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as
else:
return repayment_entry
+
@frappe.whitelist()
def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict=0):
if not company:
- company = frappe.get_value('Loan', loan, 'company')
+ company = frappe.get_value("Loan", loan, "company")
if not posting_date:
posting_date = getdate()
amounts = calculate_amounts(loan, posting_date)
- pending_amount = amounts['pending_principal_amount']
+ pending_amount = amounts["pending_principal_amount"]
if amount and (amount > pending_amount):
- frappe.throw(_('Write Off amount cannot be greater than pending loan amount'))
+ frappe.throw(_("Write Off amount cannot be greater than pending loan amount"))
if not amount:
amount = pending_amount
# get default write off account from company master
- write_off_account = frappe.get_value('Company', company, 'write_off_account')
+ write_off_account = frappe.get_value("Company", company, "write_off_account")
- write_off = frappe.new_doc('Loan Write Off')
+ write_off = frappe.new_doc("Loan Write Off")
write_off.loan = loan
write_off.posting_date = posting_date
write_off.write_off_account = write_off_account
@@ -317,26 +393,35 @@ def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict
else:
return write_off
+
@frappe.whitelist()
-def unpledge_security(loan=None, loan_security_pledge=None, security_map=None, as_dict=0, save=0, submit=0, approve=0):
+def unpledge_security(
+ loan=None, loan_security_pledge=None, security_map=None, as_dict=0, save=0, submit=0, approve=0
+):
# if no security_map is passed it will be considered as full unpledge
if security_map and isinstance(security_map, string_types):
security_map = json.loads(security_map)
if loan:
pledge_qty_map = security_map or get_pledged_security_qty(loan)
- loan_doc = frappe.get_doc('Loan', loan)
- unpledge_request = create_loan_security_unpledge(pledge_qty_map, loan_doc.name, loan_doc.company,
- loan_doc.applicant_type, loan_doc.applicant)
+ loan_doc = frappe.get_doc("Loan", loan)
+ unpledge_request = create_loan_security_unpledge(
+ pledge_qty_map, loan_doc.name, loan_doc.company, loan_doc.applicant_type, loan_doc.applicant
+ )
# will unpledge qty based on loan security pledge
elif loan_security_pledge:
security_map = {}
- pledge_doc = frappe.get_doc('Loan Security Pledge', loan_security_pledge)
+ pledge_doc = frappe.get_doc("Loan Security Pledge", loan_security_pledge)
for security in pledge_doc.securities:
security_map.setdefault(security.loan_security, security.qty)
- unpledge_request = create_loan_security_unpledge(security_map, pledge_doc.loan,
- pledge_doc.company, pledge_doc.applicant_type, pledge_doc.applicant)
+ unpledge_request = create_loan_security_unpledge(
+ security_map,
+ pledge_doc.loan,
+ pledge_doc.company,
+ pledge_doc.applicant_type,
+ pledge_doc.applicant,
+ )
if save:
unpledge_request.save()
@@ -346,16 +431,17 @@ def unpledge_security(loan=None, loan_security_pledge=None, security_map=None, a
if approve:
if unpledge_request.docstatus == 1:
- unpledge_request.status = 'Approved'
+ unpledge_request.status = "Approved"
unpledge_request.save()
else:
- frappe.throw(_('Only submittted unpledge requests can be approved'))
+ frappe.throw(_("Only submittted unpledge requests can be approved"))
if as_dict:
return unpledge_request
else:
return unpledge_request
+
def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, applicant):
unpledge_request = frappe.new_doc("Loan Security Unpledge")
unpledge_request.applicant_type = applicant_type
@@ -365,17 +451,16 @@ def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, a
for security, qty in unpledge_map.items():
if qty:
- unpledge_request.append('securities', {
- "loan_security": security,
- "qty": qty
- })
+ unpledge_request.append("securities", {"loan_security": security, "qty": qty})
return unpledge_request
+
def validate_employee_currency_with_company_currency(applicant, company):
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import (
get_employee_currency,
)
+
if not applicant:
frappe.throw(_("Please select Applicant"))
if not company:
@@ -383,18 +468,20 @@ def validate_employee_currency_with_company_currency(applicant, company):
employee_currency = get_employee_currency(applicant)
company_currency = erpnext.get_company_currency(company)
if employee_currency != company_currency:
- frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}")
- .format(applicant, employee_currency))
+ frappe.throw(
+ _(
+ "Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}"
+ ).format(applicant, employee_currency)
+ )
+
@frappe.whitelist()
def get_shortfall_applicants():
- loans = frappe.get_all('Loan Security Shortfall', {'status': 'Pending'}, pluck='loan')
- applicants = set(frappe.get_all('Loan', {'name': ('in', loans)}, pluck='name'))
+ loans = frappe.get_all("Loan Security Shortfall", {"status": "Pending"}, pluck="loan")
+ applicants = set(frappe.get_all("Loan", {"name": ("in", loans)}, pluck="name"))
+
+ return {"value": len(applicants), "fieldtype": "Int"}
- return {
- "value": len(applicants),
- "fieldtype": "Int"
- }
def add_single_month(date):
if getdate(date) == get_last_day(date):
@@ -402,29 +489,46 @@ def add_single_month(date):
else:
return add_months(date, 1)
+
@frappe.whitelist()
def make_refund_jv(loan, amount=0, reference_number=None, reference_date=None, submit=0):
- loan_details = frappe.db.get_value('Loan', loan, ['applicant_type', 'applicant',
- 'loan_account', 'payment_account', 'posting_date', 'company', 'name',
- 'total_payment', 'total_principal_paid'], as_dict=1)
+ loan_details = frappe.db.get_value(
+ "Loan",
+ loan,
+ [
+ "applicant_type",
+ "applicant",
+ "loan_account",
+ "payment_account",
+ "posting_date",
+ "company",
+ "name",
+ "total_payment",
+ "total_principal_paid",
+ ],
+ as_dict=1,
+ )
- loan_details.doctype = 'Loan'
+ loan_details.doctype = "Loan"
loan_details[loan_details.applicant_type.lower()] = loan_details.applicant
if not amount:
amount = flt(loan_details.total_principal_paid - loan_details.total_payment)
if amount < 0:
- frappe.throw(_('No excess amount pending for refund'))
+ frappe.throw(_("No excess amount pending for refund"))
- refund_jv = get_payment_entry(loan_details, {
- "party_type": loan_details.applicant_type,
- "party_account": loan_details.loan_account,
- "amount_field_party": 'debit_in_account_currency',
- "amount_field_bank": 'credit_in_account_currency',
- "amount": amount,
- "bank_account": loan_details.payment_account
- })
+ refund_jv = get_payment_entry(
+ loan_details,
+ {
+ "party_type": loan_details.applicant_type,
+ "party_account": loan_details.loan_account,
+ "amount_field_party": "debit_in_account_currency",
+ "amount_field_bank": "credit_in_account_currency",
+ "amount": amount,
+ "bank_account": loan_details.payment_account,
+ },
+ )
if reference_number:
refund_jv.cheque_no = reference_number
@@ -435,4 +539,4 @@ def make_refund_jv(loan, amount=0, reference_number=None, reference_date=None, s
if submit:
refund_jv.submit()
- return refund_jv
\ No newline at end of file
+ return refund_jv
diff --git a/erpnext/loan_management/doctype/loan/loan_dashboard.py b/erpnext/loan_management/doctype/loan/loan_dashboard.py
index 0374eda4991..971d5450eaa 100644
--- a/erpnext/loan_management/doctype/loan/loan_dashboard.py
+++ b/erpnext/loan_management/doctype/loan/loan_dashboard.py
@@ -1,18 +1,19 @@
-
-
def get_data():
return {
- 'fieldname': 'loan',
- 'non_standard_fieldnames': {
- 'Loan Disbursement': 'against_loan',
- 'Loan Repayment': 'against_loan',
+ "fieldname": "loan",
+ "non_standard_fieldnames": {
+ "Loan Disbursement": "against_loan",
+ "Loan Repayment": "against_loan",
},
- 'transactions': [
+ "transactions": [
+ {"items": ["Loan Security Pledge", "Loan Security Shortfall", "Loan Disbursement"]},
{
- 'items': ['Loan Security Pledge', 'Loan Security Shortfall', 'Loan Disbursement']
+ "items": [
+ "Loan Repayment",
+ "Loan Interest Accrual",
+ "Loan Write Off",
+ "Loan Security Unpledge",
+ ]
},
- {
- 'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Write Off', 'Loan Security Unpledge']
- }
- ]
+ ],
}
diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py
index 5ebb2e1bdce..e2b0870c322 100644
--- a/erpnext/loan_management/doctype/loan/test_loan.py
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -39,31 +39,69 @@ from erpnext.selling.doctype.customer.test_customer import get_customer_dict
class TestLoan(unittest.TestCase):
def setUp(self):
create_loan_accounts()
- create_loan_type("Personal Loan", 500000, 8.4,
+ create_loan_type(
+ "Personal Loan",
+ 500000,
+ 8.4,
is_term_loan=1,
- mode_of_payment='Cash',
- disbursement_account='Disbursement Account - _TC',
- payment_account='Payment Account - _TC',
- loan_account='Loan Account - _TC',
- interest_income_account='Interest Income Account - _TC',
- penalty_income_account='Penalty Income Account - _TC')
+ mode_of_payment="Cash",
+ disbursement_account="Disbursement Account - _TC",
+ payment_account="Payment Account - _TC",
+ loan_account="Loan Account - _TC",
+ interest_income_account="Interest Income Account - _TC",
+ penalty_income_account="Penalty Income Account - _TC",
+ )
- create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Disbursement Account - _TC',
- 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
+ create_loan_type(
+ "Stock Loan",
+ 2000000,
+ 13.5,
+ 25,
+ 1,
+ 5,
+ "Cash",
+ "Disbursement Account - _TC",
+ "Payment Account - _TC",
+ "Loan Account - _TC",
+ "Interest Income Account - _TC",
+ "Penalty Income Account - _TC",
+ )
- create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
- 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
+ create_loan_type(
+ "Demand Loan",
+ 2000000,
+ 13.5,
+ 25,
+ 0,
+ 5,
+ "Cash",
+ "Disbursement Account - _TC",
+ "Payment Account - _TC",
+ "Loan Account - _TC",
+ "Interest Income Account - _TC",
+ "Penalty Income Account - _TC",
+ )
create_loan_security_type()
create_loan_security()
- create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
- create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
+ create_loan_security_price(
+ "Test Security 1", 500, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24))
+ )
+ create_loan_security_price(
+ "Test Security 2", 250, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24))
+ )
self.applicant1 = make_employee("robert_loan@loan.com")
- make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR', company="_Test Company")
+ make_salary_structure(
+ "Test Salary Structure Loan",
+ "Monthly",
+ employee=self.applicant1,
+ currency="INR",
+ company="_Test Company",
+ )
if not frappe.db.exists("Customer", "_Test Loan Customer"):
- frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)
+ frappe.get_doc(get_customer_dict("_Test Loan Customer")).insert(ignore_permissions=True)
if not frappe.db.exists("Customer", "_Test Loan Customer 1"):
frappe.get_doc(get_customer_dict("_Test Loan Customer 1")).insert(ignore_permissions=True)
@@ -74,7 +112,7 @@ class TestLoan(unittest.TestCase):
create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20)
def test_loan(self):
- loan = frappe.get_doc("Loan", {"applicant":self.applicant1})
+ loan = frappe.get_doc("Loan", {"applicant": self.applicant1})
self.assertEqual(loan.monthly_repayment_amount, 15052)
self.assertEqual(flt(loan.total_interest_payable, 0), 21034)
self.assertEqual(flt(loan.total_payment, 0), 301034)
@@ -83,7 +121,11 @@ class TestLoan(unittest.TestCase):
self.assertEqual(len(schedule), 20)
- for idx, principal_amount, interest_amount, balance_loan_amount in [[3, 13369, 1683, 227080], [19, 14941, 105, 0], [17, 14740, 312, 29785]]:
+ for idx, principal_amount, interest_amount, balance_loan_amount in [
+ [3, 13369, 1683, 227080],
+ [19, 14941, 105, 0],
+ [17, 14740, 312, 29785],
+ ]:
self.assertEqual(flt(schedule[idx].principal_amount, 0), principal_amount)
self.assertEqual(flt(schedule[idx].interest_amount, 0), interest_amount)
self.assertEqual(flt(schedule[idx].balance_loan_amount, 0), balance_loan_amount)
@@ -98,30 +140,35 @@ class TestLoan(unittest.TestCase):
def test_loan_with_security(self):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00,
- }]
+ pledge = [
+ {
+ "loan_security": "Test Security 1",
+ "qty": 4000.00,
+ }
+ ]
- loan_application = create_loan_application('_Test Company', self.applicant2,
- 'Stock Loan', pledge, "Repay Over Number of Periods", 12)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant2, "Stock Loan", pledge, "Repay Over Number of Periods", 12
+ )
create_pledge(loan_application)
- loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods",
- 12, loan_application)
+ loan = create_loan_with_security(
+ self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application
+ )
self.assertEqual(loan.loan_amount, 1000000)
def test_loan_disbursement(self):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00
- }]
+ pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
- loan_application = create_loan_application('_Test Company', self.applicant2, 'Stock Loan', pledge, "Repay Over Number of Periods", 12)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant2, "Stock Loan", pledge, "Repay Over Number of Periods", 12
+ )
create_pledge(loan_application)
- loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application)
+ loan = create_loan_with_security(
+ self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application
+ )
self.assertEqual(loan.loan_amount, 1000000)
loan.submit()
@@ -130,14 +177,16 @@ class TestLoan(unittest.TestCase):
loan_disbursement_entry2 = make_loan_disbursement_entry(loan.name, 500000)
loan = frappe.get_doc("Loan", loan.name)
- gl_entries1 = frappe.db.get_all("GL Entry",
+ gl_entries1 = frappe.db.get_all(
+ "GL Entry",
fields=["name"],
- filters = {'voucher_type': 'Loan Disbursement', 'voucher_no': loan_disbursement_entry1.name}
+ filters={"voucher_type": "Loan Disbursement", "voucher_no": loan_disbursement_entry1.name},
)
- gl_entries2 = frappe.db.get_all("GL Entry",
+ gl_entries2 = frappe.db.get_all(
+ "GL Entry",
fields=["name"],
- filters = {'voucher_type': 'Loan Disbursement', 'voucher_no': loan_disbursement_entry2.name}
+ filters={"voucher_type": "Loan Disbursement", "voucher_no": loan_disbursement_entry2.name},
)
self.assertEqual(loan.status, "Disbursed")
@@ -151,73 +200,93 @@ class TestLoan(unittest.TestCase):
frappe.db.sql("DELETE FROM `tabLoan Application` where applicant = '_Test Loan Customer 1'")
frappe.db.sql("DELETE FROM `tabLoan Security Pledge` where applicant = '_Test Loan Customer 1'")
- if not frappe.db.get_value("Sanctioned Loan Amount", filters={"applicant_type": "Customer",
- "applicant": "_Test Loan Customer 1", "company": "_Test Company"}):
- frappe.get_doc({
- "doctype": "Sanctioned Loan Amount",
+ if not frappe.db.get_value(
+ "Sanctioned Loan Amount",
+ filters={
"applicant_type": "Customer",
"applicant": "_Test Loan Customer 1",
- "sanctioned_amount_limit": 1500000,
- "company": "_Test Company"
- }).insert(ignore_permissions=True)
+ "company": "_Test Company",
+ },
+ ):
+ frappe.get_doc(
+ {
+ "doctype": "Sanctioned Loan Amount",
+ "applicant_type": "Customer",
+ "applicant": "_Test Loan Customer 1",
+ "sanctioned_amount_limit": 1500000,
+ "company": "_Test Company",
+ }
+ ).insert(ignore_permissions=True)
# Make First Loan
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00
- }]
+ pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
- loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant3, "Demand Loan", pledge
+ )
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant3, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan = create_demand_loan(
+ self.applicant3, "Demand Loan", loan_application, posting_date="2019-10-01"
+ )
loan.submit()
# Make second loan greater than the sanctioned amount
- loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge,
- do_not_save=True)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant3, "Demand Loan", pledge, do_not_save=True
+ )
self.assertRaises(frappe.ValidationError, loan_application.save)
def test_regular_loan_repayment(self):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00
- }]
+ pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
- loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant2, "Demand Loan", pledge
+ )
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan = create_demand_loan(
+ self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01"
+ )
loan.submit()
self.assertEqual(loan.loan_amount, 1000000)
- first_date = '2019-10-01'
- last_date = '2019-10-30'
+ first_date = "2019-10-01"
+ last_date = "2019-10-30"
no_of_days = date_diff(last_date, first_date) + 1
- accrued_interest_amount = flt((loan.loan_amount * loan.rate_of_interest * no_of_days)
- / (days_in_year(get_datetime(first_date).year) * 100), 2)
+ accrued_interest_amount = flt(
+ (loan.loan_amount * loan.rate_of_interest * no_of_days)
+ / (days_in_year(get_datetime(first_date).year) * 100),
+ 2,
+ )
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
- process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
+ process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), 111119)
+ repayment_entry = create_repayment_entry(
+ loan.name, self.applicant2, add_days(last_date, 10), 111119
+ )
repayment_entry.save()
repayment_entry.submit()
penalty_amount = (accrued_interest_amount * 5 * 25) / 100
- self.assertEqual(flt(repayment_entry.penalty_amount,0), flt(penalty_amount, 0))
+ self.assertEqual(flt(repayment_entry.penalty_amount, 0), flt(penalty_amount, 0))
- amounts = frappe.db.get_all('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount'])
+ amounts = frappe.db.get_all(
+ "Loan Interest Accrual", {"loan": loan.name}, ["paid_interest_amount"]
+ )
loan.load_from_db()
- total_interest_paid = amounts[0]['paid_interest_amount'] + amounts[1]['paid_interest_amount']
- self.assertEqual(amounts[1]['paid_interest_amount'], repayment_entry.interest_payable)
- self.assertEqual(flt(loan.total_principal_paid, 0), flt(repayment_entry.amount_paid -
- penalty_amount - total_interest_paid, 0))
+ total_interest_paid = amounts[0]["paid_interest_amount"] + amounts[1]["paid_interest_amount"]
+ self.assertEqual(amounts[1]["paid_interest_amount"], repayment_entry.interest_payable)
+ self.assertEqual(
+ flt(loan.total_principal_paid, 0),
+ flt(repayment_entry.amount_paid - penalty_amount - total_interest_paid, 0),
+ )
# Check Repayment Entry cancel
repayment_entry.load_from_db()
@@ -228,21 +297,22 @@ class TestLoan(unittest.TestCase):
self.assertEqual(loan.total_principal_paid, 0)
def test_loan_closure(self):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00
- }]
+ pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
- loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant2, "Demand Loan", pledge
+ )
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan = create_demand_loan(
+ self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01"
+ )
loan.submit()
self.assertEqual(loan.loan_amount, 1000000)
- first_date = '2019-10-01'
- last_date = '2019-10-30'
+ first_date = "2019-10-01"
+ last_date = "2019-10-30"
no_of_days = date_diff(last_date, first_date) + 1
@@ -251,20 +321,27 @@ class TestLoan(unittest.TestCase):
# 5 days as well though in grace period
no_of_days += 5
- accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
- / (days_in_year(get_datetime(first_date).year) * 100)
+ accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / (
+ days_in_year(get_datetime(first_date).year) * 100
+ )
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
- process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
+ process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
- flt(loan.loan_amount + accrued_interest_amount))
+ repayment_entry = create_repayment_entry(
+ loan.name,
+ self.applicant2,
+ add_days(last_date, 5),
+ flt(loan.loan_amount + accrued_interest_amount),
+ )
repayment_entry.submit()
- amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)'])
+ amount = frappe.db.get_value(
+ "Loan Interest Accrual", {"loan": loan.name}, ["sum(paid_interest_amount)"]
+ )
- self.assertEqual(flt(amount, 0),flt(accrued_interest_amount, 0))
+ self.assertEqual(flt(amount, 0), flt(accrued_interest_amount, 0))
self.assertEqual(flt(repayment_entry.penalty_amount, 5), 0)
request_loan_closure(loan.name)
@@ -272,78 +349,101 @@ class TestLoan(unittest.TestCase):
self.assertEqual(loan.status, "Loan Closure Requested")
def test_loan_repayment_for_term_loan(self):
- pledges = [{
- "loan_security": "Test Security 2",
- "qty": 4000.00
- },
- {
- "loan_security": "Test Security 1",
- "qty": 2000.00
- }]
+ pledges = [
+ {"loan_security": "Test Security 2", "qty": 4000.00},
+ {"loan_security": "Test Security 1", "qty": 2000.00},
+ ]
- loan_application = create_loan_application('_Test Company', self.applicant2, 'Stock Loan', pledges,
- "Repay Over Number of Periods", 12)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant2, "Stock Loan", pledges, "Repay Over Number of Periods", 12
+ )
create_pledge(loan_application)
- loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application,
- posting_date=add_months(nowdate(), -1))
+ loan = create_loan_with_security(
+ self.applicant2,
+ "Stock Loan",
+ "Repay Over Number of Periods",
+ 12,
+ loan_application,
+ posting_date=add_months(nowdate(), -1),
+ )
loan.submit()
- make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1))
+ make_loan_disbursement_entry(
+ loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1)
+ )
process_loan_interest_accrual_for_term_loans(posting_date=nowdate())
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(nowdate(), 5), 89768.75)
+ repayment_entry = create_repayment_entry(
+ loan.name, self.applicant2, add_days(nowdate(), 5), 89768.75
+ )
repayment_entry.submit()
- amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
- 'paid_principal_amount'])
+ amounts = frappe.db.get_value(
+ "Loan Interest Accrual", {"loan": loan.name}, ["paid_interest_amount", "paid_principal_amount"]
+ )
self.assertEqual(amounts[0], 11250.00)
self.assertEqual(amounts[1], 78303.00)
def test_repayment_schedule_update(self):
- loan = create_loan(self.applicant2, "Personal Loan", 200000, "Repay Over Number of Periods", 4,
- applicant_type='Customer', repayment_start_date='2021-04-30', posting_date='2021-04-01')
+ loan = create_loan(
+ self.applicant2,
+ "Personal Loan",
+ 200000,
+ "Repay Over Number of Periods",
+ 4,
+ applicant_type="Customer",
+ repayment_start_date="2021-04-30",
+ posting_date="2021-04-01",
+ )
loan.submit()
- make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date='2021-04-01')
+ make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date="2021-04-01")
- process_loan_interest_accrual_for_term_loans(posting_date='2021-05-01')
- process_loan_interest_accrual_for_term_loans(posting_date='2021-06-01')
+ process_loan_interest_accrual_for_term_loans(posting_date="2021-05-01")
+ process_loan_interest_accrual_for_term_loans(posting_date="2021-06-01")
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2021-06-05', 120000)
+ repayment_entry = create_repayment_entry(loan.name, self.applicant2, "2021-06-05", 120000)
repayment_entry.submit()
loan.load_from_db()
- self.assertEqual(flt(loan.get('repayment_schedule')[3].principal_amount, 2), 41369.83)
- self.assertEqual(flt(loan.get('repayment_schedule')[3].interest_amount, 2), 289.59)
- self.assertEqual(flt(loan.get('repayment_schedule')[3].total_payment, 2), 41659.41)
- self.assertEqual(flt(loan.get('repayment_schedule')[3].balance_loan_amount, 2), 0)
+ self.assertEqual(flt(loan.get("repayment_schedule")[3].principal_amount, 2), 41369.83)
+ self.assertEqual(flt(loan.get("repayment_schedule")[3].interest_amount, 2), 289.59)
+ self.assertEqual(flt(loan.get("repayment_schedule")[3].total_payment, 2), 41659.41)
+ self.assertEqual(flt(loan.get("repayment_schedule")[3].balance_loan_amount, 2), 0)
def test_security_shortfall(self):
- pledges = [{
- "loan_security": "Test Security 2",
- "qty": 8000.00,
- "haircut": 50,
- }]
+ pledges = [
+ {
+ "loan_security": "Test Security 2",
+ "qty": 8000.00,
+ "haircut": 50,
+ }
+ ]
- loan_application = create_loan_application('_Test Company', self.applicant2,
- 'Stock Loan', pledges, "Repay Over Number of Periods", 12)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant2, "Stock Loan", pledges, "Repay Over Number of Periods", 12
+ )
create_pledge(loan_application)
- loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application)
+ loan = create_loan_with_security(
+ self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application
+ )
loan.submit()
make_loan_disbursement_entry(loan.name, loan.loan_amount)
- frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100
- where loan_security='Test Security 2'""")
+ frappe.db.sql(
+ """UPDATE `tabLoan Security Price` SET loan_security_price = 100
+ where loan_security='Test Security 2'"""
+ )
create_process_loan_security_shortfall()
loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name})
@@ -353,8 +453,10 @@ class TestLoan(unittest.TestCase):
self.assertEqual(loan_security_shortfall.security_value, 800000.00)
self.assertEqual(loan_security_shortfall.shortfall_amount, 600000.00)
- frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250
- where loan_security='Test Security 2'""")
+ frappe.db.sql(
+ """ UPDATE `tabLoan Security Price` SET loan_security_price = 250
+ where loan_security='Test Security 2'"""
+ )
create_process_loan_security_shortfall()
loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name})
@@ -362,33 +464,40 @@ class TestLoan(unittest.TestCase):
self.assertEqual(loan_security_shortfall.shortfall_amount, 0)
def test_loan_security_unpledge(self):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00
- }]
+ pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
- loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant2, "Demand Loan", pledge
+ )
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan = create_demand_loan(
+ self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01"
+ )
loan.submit()
self.assertEqual(loan.loan_amount, 1000000)
- first_date = '2019-10-01'
- last_date = '2019-10-30'
+ first_date = "2019-10-01"
+ last_date = "2019-10-30"
no_of_days = date_diff(last_date, first_date) + 1
no_of_days += 5
- accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
- / (days_in_year(get_datetime(first_date).year) * 100)
+ accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / (
+ days_in_year(get_datetime(first_date).year) * 100
+ )
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
- process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
+ process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount))
+ repayment_entry = create_repayment_entry(
+ loan.name,
+ self.applicant2,
+ add_days(last_date, 5),
+ flt(loan.loan_amount + accrued_interest_amount),
+ )
repayment_entry.submit()
request_loan_closure(loan.name)
@@ -397,98 +506,108 @@ class TestLoan(unittest.TestCase):
unpledge_request = unpledge_security(loan=loan.name, save=1)
unpledge_request.submit()
- unpledge_request.status = 'Approved'
+ unpledge_request.status = "Approved"
unpledge_request.save()
loan.load_from_db()
pledged_qty = get_pledged_security_qty(loan.name)
- self.assertEqual(loan.status, 'Closed')
+ self.assertEqual(loan.status, "Closed")
self.assertEqual(sum(pledged_qty.values()), 0)
amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 5))
- self.assertEqual(amounts['pending_principal_amount'], 0)
- self.assertEqual(amounts['payable_principal_amount'], 0.0)
- self.assertEqual(amounts['interest_amount'], 0)
+ self.assertEqual(amounts["pending_principal_amount"], 0)
+ self.assertEqual(amounts["payable_principal_amount"], 0.0)
+ self.assertEqual(amounts["interest_amount"], 0)
def test_partial_loan_security_unpledge(self):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 2000.00
- },
- {
- "loan_security": "Test Security 2",
- "qty": 4000.00
- }]
+ pledge = [
+ {"loan_security": "Test Security 1", "qty": 2000.00},
+ {"loan_security": "Test Security 2", "qty": 4000.00},
+ ]
- loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant2, "Demand Loan", pledge
+ )
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan = create_demand_loan(
+ self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01"
+ )
loan.submit()
self.assertEqual(loan.loan_amount, 1000000)
- first_date = '2019-10-01'
- last_date = '2019-10-30'
+ first_date = "2019-10-01"
+ last_date = "2019-10-30"
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
- process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
+ process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), 600000)
+ repayment_entry = create_repayment_entry(
+ loan.name, self.applicant2, add_days(last_date, 5), 600000
+ )
repayment_entry.submit()
- unpledge_map = {'Test Security 2': 2000}
+ unpledge_map = {"Test Security 2": 2000}
- unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1)
+ unpledge_request = unpledge_security(loan=loan.name, security_map=unpledge_map, save=1)
unpledge_request.submit()
- unpledge_request.status = 'Approved'
+ unpledge_request.status = "Approved"
unpledge_request.save()
unpledge_request.submit()
unpledge_request.load_from_db()
self.assertEqual(unpledge_request.docstatus, 1)
def test_sanctioned_loan_security_unpledge(self):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00
- }]
+ pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
- loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant2, "Demand Loan", pledge
+ )
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan = create_demand_loan(
+ self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01"
+ )
loan.submit()
self.assertEqual(loan.loan_amount, 1000000)
- unpledge_map = {'Test Security 1': 4000}
- unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1)
+ unpledge_map = {"Test Security 1": 4000}
+ unpledge_request = unpledge_security(loan=loan.name, security_map=unpledge_map, save=1)
unpledge_request.submit()
- unpledge_request.status = 'Approved'
+ unpledge_request.status = "Approved"
unpledge_request.save()
unpledge_request.submit()
def test_disbursal_check_with_shortfall(self):
- pledges = [{
- "loan_security": "Test Security 2",
- "qty": 8000.00,
- "haircut": 50,
- }]
+ pledges = [
+ {
+ "loan_security": "Test Security 2",
+ "qty": 8000.00,
+ "haircut": 50,
+ }
+ ]
- loan_application = create_loan_application('_Test Company', self.applicant2,
- 'Stock Loan', pledges, "Repay Over Number of Periods", 12)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant2, "Stock Loan", pledges, "Repay Over Number of Periods", 12
+ )
create_pledge(loan_application)
- loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application)
+ loan = create_loan_with_security(
+ self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application
+ )
loan.submit()
- #Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge
+ # Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge
make_loan_disbursement_entry(loan.name, 700000)
- frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100
- where loan_security='Test Security 2'""")
+ frappe.db.sql(
+ """UPDATE `tabLoan Security Price` SET loan_security_price = 100
+ where loan_security='Test Security 2'"""
+ )
create_process_loan_security_shortfall()
loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name})
@@ -496,422 +615,505 @@ class TestLoan(unittest.TestCase):
self.assertEqual(get_disbursal_amount(loan.name), 0)
- frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250
- where loan_security='Test Security 2'""")
+ frappe.db.sql(
+ """ UPDATE `tabLoan Security Price` SET loan_security_price = 250
+ where loan_security='Test Security 2'"""
+ )
def test_disbursal_check_without_shortfall(self):
- pledges = [{
- "loan_security": "Test Security 2",
- "qty": 8000.00,
- "haircut": 50,
- }]
+ pledges = [
+ {
+ "loan_security": "Test Security 2",
+ "qty": 8000.00,
+ "haircut": 50,
+ }
+ ]
- loan_application = create_loan_application('_Test Company', self.applicant2,
- 'Stock Loan', pledges, "Repay Over Number of Periods", 12)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant2, "Stock Loan", pledges, "Repay Over Number of Periods", 12
+ )
create_pledge(loan_application)
- loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application)
+ loan = create_loan_with_security(
+ self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application
+ )
loan.submit()
- #Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge
+ # Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge
make_loan_disbursement_entry(loan.name, 700000)
self.assertEqual(get_disbursal_amount(loan.name), 300000)
def test_pending_loan_amount_after_closure_request(self):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00
- }]
+ pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
- loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant2, "Demand Loan", pledge
+ )
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan = create_demand_loan(
+ self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01"
+ )
loan.submit()
self.assertEqual(loan.loan_amount, 1000000)
- first_date = '2019-10-01'
- last_date = '2019-10-30'
+ first_date = "2019-10-01"
+ last_date = "2019-10-30"
no_of_days = date_diff(last_date, first_date) + 1
no_of_days += 5
- accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
- / (days_in_year(get_datetime(first_date).year) * 100)
+ accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / (
+ days_in_year(get_datetime(first_date).year) * 100
+ )
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
- process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
+ process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount))
+ repayment_entry = create_repayment_entry(
+ loan.name,
+ self.applicant2,
+ add_days(last_date, 5),
+ flt(loan.loan_amount + accrued_interest_amount),
+ )
repayment_entry.submit()
- amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
- 'paid_principal_amount'])
+ amounts = frappe.db.get_value(
+ "Loan Interest Accrual", {"loan": loan.name}, ["paid_interest_amount", "paid_principal_amount"]
+ )
request_loan_closure(loan.name)
loan.load_from_db()
self.assertEqual(loan.status, "Loan Closure Requested")
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
- self.assertEqual(amounts['pending_principal_amount'], 0.0)
+ self.assertEqual(amounts["pending_principal_amount"], 0.0)
def test_partial_unaccrued_interest_payment(self):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00
- }]
+ pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
- loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant2, "Demand Loan", pledge
+ )
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan = create_demand_loan(
+ self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01"
+ )
loan.submit()
self.assertEqual(loan.loan_amount, 1000000)
- first_date = '2019-10-01'
- last_date = '2019-10-30'
+ first_date = "2019-10-01"
+ last_date = "2019-10-30"
no_of_days = date_diff(last_date, first_date) + 1
no_of_days += 5.5
# get partial unaccrued interest amount
- paid_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
- / (days_in_year(get_datetime(first_date).year) * 100)
+ paid_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / (
+ days_in_year(get_datetime(first_date).year) * 100
+ )
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
- process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
+ process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
- paid_amount)
+ repayment_entry = create_repayment_entry(
+ loan.name, self.applicant2, add_days(last_date, 5), paid_amount
+ )
repayment_entry.submit()
repayment_entry.load_from_db()
- partial_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 5) \
- / (days_in_year(get_datetime(first_date).year) * 100)
+ partial_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 5) / (
+ days_in_year(get_datetime(first_date).year) * 100
+ )
- interest_amount = flt(amounts['interest_amount'] + partial_accrued_interest_amount, 2)
+ interest_amount = flt(amounts["interest_amount"] + partial_accrued_interest_amount, 2)
self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0))
def test_penalty(self):
loan, amounts = create_loan_scenario_for_penalty(self)
# 30 days - grace period
penalty_days = 30 - 4
- penalty_applicable_amount = flt(amounts['interest_amount']/2)
+ penalty_applicable_amount = flt(amounts["interest_amount"] / 2)
penalty_amount = flt((((penalty_applicable_amount * 25) / 100) * penalty_days), 2)
- process = process_loan_interest_accrual_for_demand_loans(posting_date = '2019-11-30')
+ process = process_loan_interest_accrual_for_demand_loans(posting_date="2019-11-30")
- calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual',
- {'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount')
+ calculated_penalty_amount = frappe.db.get_value(
+ "Loan Interest Accrual",
+ {"process_loan_interest_accrual": process, "loan": loan.name},
+ "penalty_amount",
+ )
self.assertEqual(loan.loan_amount, 1000000)
self.assertEqual(calculated_penalty_amount, penalty_amount)
def test_penalty_repayment(self):
loan, dummy = create_loan_scenario_for_penalty(self)
- amounts = calculate_amounts(loan.name, '2019-11-30 00:00:00')
+ amounts = calculate_amounts(loan.name, "2019-11-30 00:00:00")
first_penalty = 10000
- second_penalty = amounts['penalty_amount'] - 10000
+ second_penalty = amounts["penalty_amount"] - 10000
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2019-11-30 00:00:00', 10000)
+ repayment_entry = create_repayment_entry(
+ loan.name, self.applicant2, "2019-11-30 00:00:00", 10000
+ )
repayment_entry.submit()
- amounts = calculate_amounts(loan.name, '2019-11-30 00:00:01')
- self.assertEqual(amounts['penalty_amount'], second_penalty)
+ amounts = calculate_amounts(loan.name, "2019-11-30 00:00:01")
+ self.assertEqual(amounts["penalty_amount"], second_penalty)
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2019-11-30 00:00:01', second_penalty)
+ repayment_entry = create_repayment_entry(
+ loan.name, self.applicant2, "2019-11-30 00:00:01", second_penalty
+ )
repayment_entry.submit()
- amounts = calculate_amounts(loan.name, '2019-11-30 00:00:02')
- self.assertEqual(amounts['penalty_amount'], 0)
+ amounts = calculate_amounts(loan.name, "2019-11-30 00:00:02")
+ self.assertEqual(amounts["penalty_amount"], 0)
def test_loan_write_off_limit(self):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00
- }]
+ pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
- loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant2, "Demand Loan", pledge
+ )
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan = create_demand_loan(
+ self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01"
+ )
loan.submit()
self.assertEqual(loan.loan_amount, 1000000)
- first_date = '2019-10-01'
- last_date = '2019-10-30'
+ first_date = "2019-10-01"
+ last_date = "2019-10-30"
no_of_days = date_diff(last_date, first_date) + 1
no_of_days += 5
- accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
- / (days_in_year(get_datetime(first_date).year) * 100)
+ accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / (
+ days_in_year(get_datetime(first_date).year) * 100
+ )
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
- process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
+ process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
# repay 50 less so that it can be automatically written off
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
- flt(loan.loan_amount + accrued_interest_amount - 50))
+ repayment_entry = create_repayment_entry(
+ loan.name,
+ self.applicant2,
+ add_days(last_date, 5),
+ flt(loan.loan_amount + accrued_interest_amount - 50),
+ )
repayment_entry.submit()
- amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)'])
+ amount = frappe.db.get_value(
+ "Loan Interest Accrual", {"loan": loan.name}, ["sum(paid_interest_amount)"]
+ )
- self.assertEqual(flt(amount, 0),flt(accrued_interest_amount, 0))
+ self.assertEqual(flt(amount, 0), flt(accrued_interest_amount, 0))
self.assertEqual(flt(repayment_entry.penalty_amount, 5), 0)
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
- self.assertEqual(flt(amounts['pending_principal_amount'], 0), 50)
+ self.assertEqual(flt(amounts["pending_principal_amount"], 0), 50)
request_loan_closure(loan.name)
loan.load_from_db()
self.assertEqual(loan.status, "Loan Closure Requested")
def test_loan_repayment_against_partially_disbursed_loan(self):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00
- }]
+ pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
- loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant2, "Demand Loan", pledge
+ )
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan = create_demand_loan(
+ self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01"
+ )
loan.submit()
- first_date = '2019-10-01'
- last_date = '2019-10-30'
+ first_date = "2019-10-01"
+ last_date = "2019-10-30"
- make_loan_disbursement_entry(loan.name, loan.loan_amount/2, disbursement_date=first_date)
+ make_loan_disbursement_entry(loan.name, loan.loan_amount / 2, disbursement_date=first_date)
loan.load_from_db()
self.assertEqual(loan.status, "Partially Disbursed")
- create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
- flt(loan.loan_amount/3))
+ create_repayment_entry(
+ loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount / 3)
+ )
def test_loan_amount_write_off(self):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00
- }]
+ pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
- loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant2, "Demand Loan", pledge
+ )
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan = create_demand_loan(
+ self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01"
+ )
loan.submit()
self.assertEqual(loan.loan_amount, 1000000)
- first_date = '2019-10-01'
- last_date = '2019-10-30'
+ first_date = "2019-10-01"
+ last_date = "2019-10-30"
no_of_days = date_diff(last_date, first_date) + 1
no_of_days += 5
- accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
- / (days_in_year(get_datetime(first_date).year) * 100)
+ accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / (
+ days_in_year(get_datetime(first_date).year) * 100
+ )
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
- process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
+ process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
# repay 100 less so that it can be automatically written off
- repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
- flt(loan.loan_amount + accrued_interest_amount - 100))
+ repayment_entry = create_repayment_entry(
+ loan.name,
+ self.applicant2,
+ add_days(last_date, 5),
+ flt(loan.loan_amount + accrued_interest_amount - 100),
+ )
repayment_entry.submit()
- amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)'])
+ amount = frappe.db.get_value(
+ "Loan Interest Accrual", {"loan": loan.name}, ["sum(paid_interest_amount)"]
+ )
- self.assertEqual(flt(amount, 0),flt(accrued_interest_amount, 0))
+ self.assertEqual(flt(amount, 0), flt(accrued_interest_amount, 0))
self.assertEqual(flt(repayment_entry.penalty_amount, 5), 0)
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
- self.assertEqual(flt(amounts['pending_principal_amount'], 0), 100)
+ self.assertEqual(flt(amounts["pending_principal_amount"], 0), 100)
- we = make_loan_write_off(loan.name, amount=amounts['pending_principal_amount'])
+ we = make_loan_write_off(loan.name, amount=amounts["pending_principal_amount"])
we.submit()
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
- self.assertEqual(flt(amounts['pending_principal_amount'], 0), 0)
+ self.assertEqual(flt(amounts["pending_principal_amount"], 0), 0)
+
def create_loan_scenario_for_penalty(doc):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00
- }]
+ pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
- loan_application = create_loan_application('_Test Company', doc.applicant2, 'Demand Loan', pledge)
+ loan_application = create_loan_application("_Test Company", doc.applicant2, "Demand Loan", pledge)
create_pledge(loan_application)
- loan = create_demand_loan(doc.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan = create_demand_loan(
+ doc.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01"
+ )
loan.submit()
- first_date = '2019-10-01'
- last_date = '2019-10-30'
+ first_date = "2019-10-01"
+ last_date = "2019-10-30"
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
- process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
+ process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 1))
- paid_amount = amounts['interest_amount']/2
+ paid_amount = amounts["interest_amount"] / 2
- repayment_entry = create_repayment_entry(loan.name, doc.applicant2, add_days(last_date, 5),
- paid_amount)
+ repayment_entry = create_repayment_entry(
+ loan.name, doc.applicant2, add_days(last_date, 5), paid_amount
+ )
repayment_entry.submit()
return loan, amounts
+
def create_loan_accounts():
if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"):
- frappe.get_doc({
- "doctype": "Account",
- "account_name": "Loans and Advances (Assets)",
- "company": "_Test Company",
- "root_type": "Asset",
- "report_type": "Balance Sheet",
- "currency": "INR",
- "parent_account": "Current Assets - _TC",
- "account_type": "Bank",
- "is_group": 1
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "Loans and Advances (Assets)",
+ "company": "_Test Company",
+ "root_type": "Asset",
+ "report_type": "Balance Sheet",
+ "currency": "INR",
+ "parent_account": "Current Assets - _TC",
+ "account_type": "Bank",
+ "is_group": 1,
+ }
+ ).insert(ignore_permissions=True)
if not frappe.db.exists("Account", "Loan Account - _TC"):
- frappe.get_doc({
- "doctype": "Account",
- "company": "_Test Company",
- "account_name": "Loan Account",
- "root_type": "Asset",
- "report_type": "Balance Sheet",
- "currency": "INR",
- "parent_account": "Loans and Advances (Assets) - _TC",
- "account_type": "Bank",
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Account",
+ "company": "_Test Company",
+ "account_name": "Loan Account",
+ "root_type": "Asset",
+ "report_type": "Balance Sheet",
+ "currency": "INR",
+ "parent_account": "Loans and Advances (Assets) - _TC",
+ "account_type": "Bank",
+ }
+ ).insert(ignore_permissions=True)
if not frappe.db.exists("Account", "Payment Account - _TC"):
- frappe.get_doc({
- "doctype": "Account",
- "company": "_Test Company",
- "account_name": "Payment Account",
- "root_type": "Asset",
- "report_type": "Balance Sheet",
- "currency": "INR",
- "parent_account": "Bank Accounts - _TC",
- "account_type": "Bank",
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Account",
+ "company": "_Test Company",
+ "account_name": "Payment Account",
+ "root_type": "Asset",
+ "report_type": "Balance Sheet",
+ "currency": "INR",
+ "parent_account": "Bank Accounts - _TC",
+ "account_type": "Bank",
+ }
+ ).insert(ignore_permissions=True)
if not frappe.db.exists("Account", "Disbursement Account - _TC"):
- frappe.get_doc({
- "doctype": "Account",
- "company": "_Test Company",
- "account_name": "Disbursement Account",
- "root_type": "Asset",
- "report_type": "Balance Sheet",
- "currency": "INR",
- "parent_account": "Bank Accounts - _TC",
- "account_type": "Bank",
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Account",
+ "company": "_Test Company",
+ "account_name": "Disbursement Account",
+ "root_type": "Asset",
+ "report_type": "Balance Sheet",
+ "currency": "INR",
+ "parent_account": "Bank Accounts - _TC",
+ "account_type": "Bank",
+ }
+ ).insert(ignore_permissions=True)
if not frappe.db.exists("Account", "Interest Income Account - _TC"):
- frappe.get_doc({
- "doctype": "Account",
- "company": "_Test Company",
- "root_type": "Income",
- "account_name": "Interest Income Account",
- "report_type": "Profit and Loss",
- "currency": "INR",
- "parent_account": "Direct Income - _TC",
- "account_type": "Income Account",
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Account",
+ "company": "_Test Company",
+ "root_type": "Income",
+ "account_name": "Interest Income Account",
+ "report_type": "Profit and Loss",
+ "currency": "INR",
+ "parent_account": "Direct Income - _TC",
+ "account_type": "Income Account",
+ }
+ ).insert(ignore_permissions=True)
if not frappe.db.exists("Account", "Penalty Income Account - _TC"):
- frappe.get_doc({
- "doctype": "Account",
- "company": "_Test Company",
- "account_name": "Penalty Income Account",
- "root_type": "Income",
- "report_type": "Profit and Loss",
- "currency": "INR",
- "parent_account": "Direct Income - _TC",
- "account_type": "Income Account",
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Account",
+ "company": "_Test Company",
+ "account_name": "Penalty Income Account",
+ "root_type": "Income",
+ "report_type": "Profit and Loss",
+ "currency": "INR",
+ "parent_account": "Direct Income - _TC",
+ "account_type": "Income Account",
+ }
+ ).insert(ignore_permissions=True)
-def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_interest_rate=None, is_term_loan=None, grace_period_in_days=None,
- mode_of_payment=None, disbursement_account=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None,
- repayment_method=None, repayment_periods=None):
+
+def create_loan_type(
+ loan_name,
+ maximum_loan_amount,
+ rate_of_interest,
+ penalty_interest_rate=None,
+ is_term_loan=None,
+ grace_period_in_days=None,
+ mode_of_payment=None,
+ disbursement_account=None,
+ payment_account=None,
+ loan_account=None,
+ interest_income_account=None,
+ penalty_income_account=None,
+ repayment_method=None,
+ repayment_periods=None,
+):
if not frappe.db.exists("Loan Type", loan_name):
- loan_type = frappe.get_doc({
- "doctype": "Loan Type",
- "company": "_Test Company",
- "loan_name": loan_name,
- "is_term_loan": is_term_loan,
- "maximum_loan_amount": maximum_loan_amount,
- "rate_of_interest": rate_of_interest,
- "penalty_interest_rate": penalty_interest_rate,
- "grace_period_in_days": grace_period_in_days,
- "mode_of_payment": mode_of_payment,
- "disbursement_account": disbursement_account,
- "payment_account": payment_account,
- "loan_account": loan_account,
- "interest_income_account": interest_income_account,
- "penalty_income_account": penalty_income_account,
- "repayment_method": repayment_method,
- "repayment_periods": repayment_periods,
- "write_off_amount": 100
- }).insert()
+ loan_type = frappe.get_doc(
+ {
+ "doctype": "Loan Type",
+ "company": "_Test Company",
+ "loan_name": loan_name,
+ "is_term_loan": is_term_loan,
+ "maximum_loan_amount": maximum_loan_amount,
+ "rate_of_interest": rate_of_interest,
+ "penalty_interest_rate": penalty_interest_rate,
+ "grace_period_in_days": grace_period_in_days,
+ "mode_of_payment": mode_of_payment,
+ "disbursement_account": disbursement_account,
+ "payment_account": payment_account,
+ "loan_account": loan_account,
+ "interest_income_account": interest_income_account,
+ "penalty_income_account": penalty_income_account,
+ "repayment_method": repayment_method,
+ "repayment_periods": repayment_periods,
+ "write_off_amount": 100,
+ }
+ ).insert()
loan_type.submit()
+
def create_loan_security_type():
if not frappe.db.exists("Loan Security Type", "Stock"):
- frappe.get_doc({
- "doctype": "Loan Security Type",
- "loan_security_type": "Stock",
- "unit_of_measure": "Nos",
- "haircut": 50.00,
- "loan_to_value_ratio": 50
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Loan Security Type",
+ "loan_security_type": "Stock",
+ "unit_of_measure": "Nos",
+ "haircut": 50.00,
+ "loan_to_value_ratio": 50,
+ }
+ ).insert(ignore_permissions=True)
+
def create_loan_security():
if not frappe.db.exists("Loan Security", "Test Security 1"):
- frappe.get_doc({
- "doctype": "Loan Security",
- "loan_security_type": "Stock",
- "loan_security_code": "532779",
- "loan_security_name": "Test Security 1",
- "unit_of_measure": "Nos",
- "haircut": 50.00,
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Loan Security",
+ "loan_security_type": "Stock",
+ "loan_security_code": "532779",
+ "loan_security_name": "Test Security 1",
+ "unit_of_measure": "Nos",
+ "haircut": 50.00,
+ }
+ ).insert(ignore_permissions=True)
if not frappe.db.exists("Loan Security", "Test Security 2"):
- frappe.get_doc({
- "doctype": "Loan Security",
- "loan_security_type": "Stock",
- "loan_security_code": "531335",
- "loan_security_name": "Test Security 2",
- "unit_of_measure": "Nos",
- "haircut": 50.00,
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Loan Security",
+ "loan_security_type": "Stock",
+ "loan_security_code": "531335",
+ "loan_security_name": "Test Security 2",
+ "unit_of_measure": "Nos",
+ "haircut": 50.00,
+ }
+ ).insert(ignore_permissions=True)
+
def create_loan_security_pledge(applicant, pledges, loan_application=None, loan=None):
lsp = frappe.new_doc("Loan Security Pledge")
- lsp.applicant_type = 'Customer'
+ lsp.applicant_type = "Customer"
lsp.applicant = applicant
lsp.company = "_Test Company"
lsp.loan_application = loan_application
@@ -920,64 +1122,82 @@ def create_loan_security_pledge(applicant, pledges, loan_application=None, loan=
lsp.loan = loan
for pledge in pledges:
- lsp.append('securities', {
- "loan_security": pledge['loan_security'],
- "qty": pledge['qty']
- })
+ lsp.append("securities", {"loan_security": pledge["loan_security"], "qty": pledge["qty"]})
lsp.save()
lsp.submit()
return lsp
+
def make_loan_disbursement_entry(loan, amount, disbursement_date=None):
- loan_disbursement_entry = frappe.get_doc({
- "doctype": "Loan Disbursement",
- "against_loan": loan,
- "disbursement_date": disbursement_date,
- "company": "_Test Company",
- "disbursed_amount": amount,
- "cost_center": 'Main - _TC'
- }).insert(ignore_permissions=True)
+ loan_disbursement_entry = frappe.get_doc(
+ {
+ "doctype": "Loan Disbursement",
+ "against_loan": loan,
+ "disbursement_date": disbursement_date,
+ "company": "_Test Company",
+ "disbursed_amount": amount,
+ "cost_center": "Main - _TC",
+ }
+ ).insert(ignore_permissions=True)
loan_disbursement_entry.save()
loan_disbursement_entry.submit()
return loan_disbursement_entry
+
def create_loan_security_price(loan_security, loan_security_price, uom, from_date, to_date):
- if not frappe.db.get_value("Loan Security Price",{"loan_security": loan_security,
- "valid_from": ("<=", from_date), "valid_upto": (">=", to_date)}, 'name'):
+ if not frappe.db.get_value(
+ "Loan Security Price",
+ {"loan_security": loan_security, "valid_from": ("<=", from_date), "valid_upto": (">=", to_date)},
+ "name",
+ ):
+
+ lsp = frappe.get_doc(
+ {
+ "doctype": "Loan Security Price",
+ "loan_security": loan_security,
+ "loan_security_price": loan_security_price,
+ "uom": uom,
+ "valid_from": from_date,
+ "valid_upto": to_date,
+ }
+ ).insert(ignore_permissions=True)
- lsp = frappe.get_doc({
- "doctype": "Loan Security Price",
- "loan_security": loan_security,
- "loan_security_price": loan_security_price,
- "uom": uom,
- "valid_from":from_date,
- "valid_upto": to_date
- }).insert(ignore_permissions=True)
def create_repayment_entry(loan, applicant, posting_date, paid_amount):
- lr = frappe.get_doc({
- "doctype": "Loan Repayment",
- "against_loan": loan,
- "company": "_Test Company",
- "posting_date": posting_date or nowdate(),
- "applicant": applicant,
- "amount_paid": paid_amount,
- "loan_type": "Stock Loan"
- }).insert(ignore_permissions=True)
+ lr = frappe.get_doc(
+ {
+ "doctype": "Loan Repayment",
+ "against_loan": loan,
+ "company": "_Test Company",
+ "posting_date": posting_date or nowdate(),
+ "applicant": applicant,
+ "amount_paid": paid_amount,
+ "loan_type": "Stock Loan",
+ }
+ ).insert(ignore_permissions=True)
return lr
-def create_loan_application(company, applicant, loan_type, proposed_pledges, repayment_method=None,
- repayment_periods=None, posting_date=None, do_not_save=False):
- loan_application = frappe.new_doc('Loan Application')
- loan_application.applicant_type = 'Customer'
+
+def create_loan_application(
+ company,
+ applicant,
+ loan_type,
+ proposed_pledges,
+ repayment_method=None,
+ repayment_periods=None,
+ posting_date=None,
+ do_not_save=False,
+):
+ loan_application = frappe.new_doc("Loan Application")
+ loan_application.applicant_type = "Customer"
loan_application.company = company
loan_application.applicant = applicant
loan_application.loan_type = loan_type
@@ -989,7 +1209,7 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep
loan_application.repayment_periods = repayment_periods
for pledge in proposed_pledges:
- loan_application.append('proposed_pledges', pledge)
+ loan_application.append("proposed_pledges", pledge)
if do_not_save:
return loan_application
@@ -997,75 +1217,99 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep
loan_application.save()
loan_application.submit()
- loan_application.status = 'Approved'
+ loan_application.status = "Approved"
loan_application.save()
return loan_application.name
-def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_periods,
- applicant_type=None, repayment_start_date=None, posting_date=None):
+def create_loan(
+ applicant,
+ loan_type,
+ loan_amount,
+ repayment_method,
+ repayment_periods,
+ applicant_type=None,
+ repayment_start_date=None,
+ posting_date=None,
+):
- loan = frappe.get_doc({
- "doctype": "Loan",
- "applicant_type": applicant_type or "Employee",
- "company": "_Test Company",
- "applicant": applicant,
- "loan_type": loan_type,
- "loan_amount": loan_amount,
- "repayment_method": repayment_method,
- "repayment_periods": repayment_periods,
- "repayment_start_date": repayment_start_date or nowdate(),
- "is_term_loan": 1,
- "posting_date": posting_date or nowdate()
- })
+ loan = frappe.get_doc(
+ {
+ "doctype": "Loan",
+ "applicant_type": applicant_type or "Employee",
+ "company": "_Test Company",
+ "applicant": applicant,
+ "loan_type": loan_type,
+ "loan_amount": loan_amount,
+ "repayment_method": repayment_method,
+ "repayment_periods": repayment_periods,
+ "repayment_start_date": repayment_start_date or nowdate(),
+ "is_term_loan": 1,
+ "posting_date": posting_date or nowdate(),
+ }
+ )
loan.save()
return loan
-def create_loan_with_security(applicant, loan_type, repayment_method, repayment_periods, loan_application, posting_date=None, repayment_start_date=None):
- loan = frappe.get_doc({
- "doctype": "Loan",
- "company": "_Test Company",
- "applicant_type": "Customer",
- "posting_date": posting_date or nowdate(),
- "loan_application": loan_application,
- "applicant": applicant,
- "loan_type": loan_type,
- "is_term_loan": 1,
- "is_secured_loan": 1,
- "repayment_method": repayment_method,
- "repayment_periods": repayment_periods,
- "repayment_start_date": repayment_start_date or nowdate(),
- "mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'),
- "payment_account": 'Payment Account - _TC',
- "loan_account": 'Loan Account - _TC',
- "interest_income_account": 'Interest Income Account - _TC',
- "penalty_income_account": 'Penalty Income Account - _TC',
- })
+
+def create_loan_with_security(
+ applicant,
+ loan_type,
+ repayment_method,
+ repayment_periods,
+ loan_application,
+ posting_date=None,
+ repayment_start_date=None,
+):
+ loan = frappe.get_doc(
+ {
+ "doctype": "Loan",
+ "company": "_Test Company",
+ "applicant_type": "Customer",
+ "posting_date": posting_date or nowdate(),
+ "loan_application": loan_application,
+ "applicant": applicant,
+ "loan_type": loan_type,
+ "is_term_loan": 1,
+ "is_secured_loan": 1,
+ "repayment_method": repayment_method,
+ "repayment_periods": repayment_periods,
+ "repayment_start_date": repayment_start_date or nowdate(),
+ "mode_of_payment": frappe.db.get_value("Mode of Payment", {"type": "Cash"}, "name"),
+ "payment_account": "Payment Account - _TC",
+ "loan_account": "Loan Account - _TC",
+ "interest_income_account": "Interest Income Account - _TC",
+ "penalty_income_account": "Penalty Income Account - _TC",
+ }
+ )
loan.save()
return loan
+
def create_demand_loan(applicant, loan_type, loan_application, posting_date=None):
- loan = frappe.get_doc({
- "doctype": "Loan",
- "company": "_Test Company",
- "applicant_type": "Customer",
- "posting_date": posting_date or nowdate(),
- 'loan_application': loan_application,
- "applicant": applicant,
- "loan_type": loan_type,
- "is_term_loan": 0,
- "is_secured_loan": 1,
- "mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'),
- "payment_account": 'Payment Account - _TC',
- "loan_account": 'Loan Account - _TC',
- "interest_income_account": 'Interest Income Account - _TC',
- "penalty_income_account": 'Penalty Income Account - _TC',
- })
+ loan = frappe.get_doc(
+ {
+ "doctype": "Loan",
+ "company": "_Test Company",
+ "applicant_type": "Customer",
+ "posting_date": posting_date or nowdate(),
+ "loan_application": loan_application,
+ "applicant": applicant,
+ "loan_type": loan_type,
+ "is_term_loan": 0,
+ "is_secured_loan": 1,
+ "mode_of_payment": frappe.db.get_value("Mode of Payment", {"type": "Cash"}, "name"),
+ "payment_account": "Payment Account - _TC",
+ "loan_account": "Loan Account - _TC",
+ "interest_income_account": "Interest Income Account - _TC",
+ "penalty_income_account": "Penalty Income Account - _TC",
+ }
+ )
loan.save()
diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py
index 2abd8ecbe48..41d8c2a9e28 100644
--- a/erpnext/loan_management/doctype/loan_application/loan_application.py
+++ b/erpnext/loan_management/doctype/loan_application/loan_application.py
@@ -30,8 +30,13 @@ class LoanApplication(Document):
self.validate_loan_amount()
if self.is_term_loan:
- validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount,
- self.repayment_periods, self.is_term_loan)
+ validate_repayment_method(
+ self.repayment_method,
+ self.loan_amount,
+ self.repayment_amount,
+ self.repayment_periods,
+ self.is_term_loan,
+ )
self.validate_loan_type()
@@ -47,21 +52,35 @@ class LoanApplication(Document):
if not self.loan_amount:
frappe.throw(_("Loan Amount is mandatory"))
- maximum_loan_limit = frappe.db.get_value('Loan Type', self.loan_type, 'maximum_loan_amount')
+ maximum_loan_limit = frappe.db.get_value("Loan Type", self.loan_type, "maximum_loan_amount")
if maximum_loan_limit and self.loan_amount > maximum_loan_limit:
- frappe.throw(_("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit))
+ frappe.throw(
+ _("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit)
+ )
if self.maximum_loan_amount and self.loan_amount > self.maximum_loan_amount:
- frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount))
+ frappe.throw(
+ _("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(
+ self.maximum_loan_amount
+ )
+ )
def check_sanctioned_amount_limit(self):
- sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
+ sanctioned_amount_limit = get_sanctioned_amount_limit(
+ self.applicant_type, self.applicant, self.company
+ )
if sanctioned_amount_limit:
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
- if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit):
- frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))
+ if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(
+ sanctioned_amount_limit
+ ):
+ frappe.throw(
+ _("Sanctioned Amount limit crossed for {0} {1}").format(
+ self.applicant_type, frappe.bold(self.applicant)
+ )
+ )
def set_pledge_amount(self):
for proposed_pledge in self.proposed_pledges:
@@ -72,26 +91,31 @@ class LoanApplication(Document):
proposed_pledge.loan_security_price = get_loan_security_price(proposed_pledge.loan_security)
if not proposed_pledge.qty:
- proposed_pledge.qty = cint(proposed_pledge.amount/proposed_pledge.loan_security_price)
+ proposed_pledge.qty = cint(proposed_pledge.amount / proposed_pledge.loan_security_price)
proposed_pledge.amount = proposed_pledge.qty * proposed_pledge.loan_security_price
- proposed_pledge.post_haircut_amount = cint(proposed_pledge.amount - (proposed_pledge.amount * proposed_pledge.haircut/100))
+ proposed_pledge.post_haircut_amount = cint(
+ proposed_pledge.amount - (proposed_pledge.amount * proposed_pledge.haircut / 100)
+ )
def get_repayment_details(self):
if self.is_term_loan:
if self.repayment_method == "Repay Over Number of Periods":
- self.repayment_amount = get_monthly_repayment_amount(self.loan_amount, self.rate_of_interest, self.repayment_periods)
+ self.repayment_amount = get_monthly_repayment_amount(
+ self.loan_amount, self.rate_of_interest, self.repayment_periods
+ )
if self.repayment_method == "Repay Fixed Amount per Period":
- monthly_interest_rate = flt(self.rate_of_interest) / (12 *100)
+ monthly_interest_rate = flt(self.rate_of_interest) / (12 * 100)
if monthly_interest_rate:
- min_repayment_amount = self.loan_amount*monthly_interest_rate
+ min_repayment_amount = self.loan_amount * monthly_interest_rate
if self.repayment_amount - min_repayment_amount <= 0:
- frappe.throw(_("Repayment Amount must be greater than " \
- + str(flt(min_repayment_amount, 2))))
- self.repayment_periods = math.ceil((math.log(self.repayment_amount) -
- math.log(self.repayment_amount - min_repayment_amount)) /(math.log(1 + monthly_interest_rate)))
+ frappe.throw(_("Repayment Amount must be greater than " + str(flt(min_repayment_amount, 2))))
+ self.repayment_periods = math.ceil(
+ (math.log(self.repayment_amount) - math.log(self.repayment_amount - min_repayment_amount))
+ / (math.log(1 + monthly_interest_rate))
+ )
else:
self.repayment_periods = self.loan_amount / self.repayment_amount
@@ -104,8 +128,8 @@ class LoanApplication(Document):
self.total_payable_amount = 0
self.total_payable_interest = 0
- while(balance_amount > 0):
- interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100))
+ while balance_amount > 0:
+ interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12 * 100))
balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount)
self.total_payable_interest += interest_amount
@@ -124,12 +148,21 @@ class LoanApplication(Document):
if not self.loan_amount and self.is_secured_loan and self.proposed_pledges:
self.loan_amount = self.maximum_loan_amount
+
@frappe.whitelist()
def create_loan(source_name, target_doc=None, submit=0):
def update_accounts(source_doc, target_doc, source_parent):
- account_details = frappe.get_all("Loan Type",
- fields=["mode_of_payment", "payment_account","loan_account", "interest_income_account", "penalty_income_account"],
- filters = {'name': source_doc.loan_type})[0]
+ account_details = frappe.get_all(
+ "Loan Type",
+ fields=[
+ "mode_of_payment",
+ "payment_account",
+ "loan_account",
+ "interest_income_account",
+ "penalty_income_account",
+ ],
+ filters={"name": source_doc.loan_type},
+ )[0]
if source_doc.is_secured_loan:
target_doc.maximum_loan_amount = 0
@@ -141,22 +174,25 @@ def create_loan(source_name, target_doc=None, submit=0):
target_doc.penalty_income_account = account_details.penalty_income_account
target_doc.loan_application = source_name
-
- doclist = get_mapped_doc("Loan Application", source_name, {
- "Loan Application": {
- "doctype": "Loan",
- "validation": {
- "docstatus": ["=", 1]
- },
- "postprocess": update_accounts
- }
- }, target_doc)
+ doclist = get_mapped_doc(
+ "Loan Application",
+ source_name,
+ {
+ "Loan Application": {
+ "doctype": "Loan",
+ "validation": {"docstatus": ["=", 1]},
+ "postprocess": update_accounts,
+ }
+ },
+ target_doc,
+ )
if submit:
doclist.submit()
return doclist
+
@frappe.whitelist()
def create_pledge(loan_application, loan=None):
loan_application_doc = frappe.get_doc("Loan Application", loan_application)
@@ -172,12 +208,15 @@ def create_pledge(loan_application, loan=None):
for pledge in loan_application_doc.proposed_pledges:
- lsp.append('securities', {
- "loan_security": pledge.loan_security,
- "qty": pledge.qty,
- "loan_security_price": pledge.loan_security_price,
- "haircut": pledge.haircut
- })
+ lsp.append(
+ "securities",
+ {
+ "loan_security": pledge.loan_security,
+ "qty": pledge.qty,
+ "loan_security_price": pledge.loan_security_price,
+ "haircut": pledge.haircut,
+ },
+ )
lsp.save()
lsp.submit()
@@ -187,15 +226,14 @@ def create_pledge(loan_application, loan=None):
return lsp.name
-#This is a sandbox method to get the proposed pledges
+
+# This is a sandbox method to get the proposed pledges
@frappe.whitelist()
def get_proposed_pledge(securities):
if isinstance(securities, string_types):
securities = json.loads(securities)
- proposed_pledges = {
- 'securities': []
- }
+ proposed_pledges = {"securities": []}
maximum_loan_amount = 0
for security in securities:
@@ -206,15 +244,15 @@ def get_proposed_pledge(securities):
security.loan_security_price = get_loan_security_price(security.loan_security)
if not security.qty:
- security.qty = cint(security.amount/security.loan_security_price)
+ security.qty = cint(security.amount / security.loan_security_price)
security.amount = security.qty * security.loan_security_price
- security.post_haircut_amount = cint(security.amount - (security.amount * security.haircut/100))
+ security.post_haircut_amount = cint(security.amount - (security.amount * security.haircut / 100))
maximum_loan_amount += security.post_haircut_amount
- proposed_pledges['securities'].append(security)
+ proposed_pledges["securities"].append(security)
- proposed_pledges['maximum_loan_amount'] = maximum_loan_amount
+ proposed_pledges["maximum_loan_amount"] = maximum_loan_amount
return proposed_pledges
diff --git a/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py b/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py
index 01ef9f9d78a..1d90e9bb114 100644
--- a/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py
+++ b/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py
@@ -1,11 +1,7 @@
-
-
def get_data():
- return {
- 'fieldname': 'loan_application',
- 'transactions': [
- {
- 'items': ['Loan', 'Loan Security Pledge']
- },
- ],
- }
+ return {
+ "fieldname": "loan_application",
+ "transactions": [
+ {"items": ["Loan", "Loan Security Pledge"]},
+ ],
+ }
diff --git a/erpnext/loan_management/doctype/loan_application/test_loan_application.py b/erpnext/loan_management/doctype/loan_application/test_loan_application.py
index 640709c095f..2a4bb882a8e 100644
--- a/erpnext/loan_management/doctype/loan_application/test_loan_application.py
+++ b/erpnext/loan_management/doctype/loan_application/test_loan_application.py
@@ -15,27 +15,45 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
class TestLoanApplication(unittest.TestCase):
def setUp(self):
create_loan_accounts()
- create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Disbursement Account - _TC', 'Payment Account - _TC', 'Loan Account - _TC',
- 'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18)
+ create_loan_type(
+ "Home Loan",
+ 500000,
+ 9.2,
+ 0,
+ 1,
+ 0,
+ "Cash",
+ "Disbursement Account - _TC",
+ "Payment Account - _TC",
+ "Loan Account - _TC",
+ "Interest Income Account - _TC",
+ "Penalty Income Account - _TC",
+ "Repay Over Number of Periods",
+ 18,
+ )
self.applicant = make_employee("kate_loan@loan.com", "_Test Company")
- make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant, currency='INR')
+ make_salary_structure(
+ "Test Salary Structure Loan", "Monthly", employee=self.applicant, currency="INR"
+ )
self.create_loan_application()
def create_loan_application(self):
loan_application = frappe.new_doc("Loan Application")
- loan_application.update({
- "applicant": self.applicant,
- "loan_type": "Home Loan",
- "rate_of_interest": 9.2,
- "loan_amount": 250000,
- "repayment_method": "Repay Over Number of Periods",
- "repayment_periods": 18,
- "company": "_Test Company"
- })
+ loan_application.update(
+ {
+ "applicant": self.applicant,
+ "loan_type": "Home Loan",
+ "rate_of_interest": 9.2,
+ "loan_amount": 250000,
+ "repayment_method": "Repay Over Number of Periods",
+ "repayment_periods": 18,
+ "company": "_Test Company",
+ }
+ )
loan_application.insert()
def test_loan_totals(self):
- loan_application = frappe.get_doc("Loan Application", {"applicant":self.applicant})
+ loan_application = frappe.get_doc("Loan Application", {"applicant": self.applicant})
self.assertEqual(loan_application.total_payable_interest, 18599)
self.assertEqual(loan_application.total_payable_amount, 268599)
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
index 7811d56a758..50926d77268 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
@@ -14,11 +14,15 @@
"applicant",
"section_break_7",
"disbursement_date",
+ "clearance_date",
"column_break_8",
"disbursed_amount",
"accounting_dimensions_section",
"cost_center",
- "customer_details_section",
+ "accounting_details",
+ "disbursement_account",
+ "column_break_16",
+ "loan_account",
"bank_account",
"disbursement_references_section",
"reference_date",
@@ -106,11 +110,6 @@
"fieldtype": "Section Break",
"label": "Disbursement Details"
},
- {
- "fieldname": "customer_details_section",
- "fieldtype": "Section Break",
- "label": "Customer Details"
- },
{
"fetch_from": "against_loan.applicant_type",
"fieldname": "applicant_type",
@@ -149,15 +148,48 @@
"fieldname": "reference_number",
"fieldtype": "Data",
"label": "Reference Number"
+ },
+ {
+ "fieldname": "clearance_date",
+ "fieldtype": "Date",
+ "label": "Clearance Date",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "accounting_details",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fetch_from": "against_loan.disbursement_account",
+ "fieldname": "disbursement_account",
+ "fieldtype": "Link",
+ "label": "Disbursement Account",
+ "options": "Account",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_16",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "against_loan.loan_account",
+ "fieldname": "loan_account",
+ "fieldtype": "Link",
+ "label": "Loan Account",
+ "options": "Account",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-19 18:09:32.175355",
+ "modified": "2022-02-17 18:23:44.157598",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Disbursement",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -194,5 +226,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
index df3aadfb18d..10174e531a1 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
@@ -18,7 +18,6 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_
class LoanDisbursement(AccountsController):
-
def validate(self):
self.set_missing_values()
self.validate_disbursal_amount()
@@ -30,7 +29,7 @@ class LoanDisbursement(AccountsController):
def on_cancel(self):
self.set_status_and_amounts(cancel=1)
self.make_gl_entries(cancel=1)
- self.ignore_linked_doctypes = ['GL Entry']
+ self.ignore_linked_doctypes = ["GL Entry"]
def set_missing_values(self):
if not self.disbursement_date:
@@ -42,9 +41,6 @@ class LoanDisbursement(AccountsController):
if not self.posting_date:
self.posting_date = self.disbursement_date or nowdate()
- if not self.bank_account and self.applicant_type == "Customer":
- self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account")
-
def validate_disbursal_amount(self):
possible_disbursal_amount = get_disbursal_amount(self.against_loan)
@@ -52,21 +48,36 @@ class LoanDisbursement(AccountsController):
frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount))
def set_status_and_amounts(self, cancel=0):
- loan_details = frappe.get_all("Loan",
- fields = ["loan_amount", "disbursed_amount", "total_payment", "total_principal_paid", "total_interest_payable",
- "status", "is_term_loan", "is_secured_loan"], filters= { "name": self.against_loan })[0]
+ loan_details = frappe.get_all(
+ "Loan",
+ fields=[
+ "loan_amount",
+ "disbursed_amount",
+ "total_payment",
+ "total_principal_paid",
+ "total_interest_payable",
+ "status",
+ "is_term_loan",
+ "is_secured_loan",
+ ],
+ filters={"name": self.against_loan},
+ )[0]
if cancel:
disbursed_amount, status, total_payment = self.get_values_on_cancel(loan_details)
else:
disbursed_amount, status, total_payment = self.get_values_on_submit(loan_details)
- frappe.db.set_value("Loan", self.against_loan, {
- "disbursement_date": self.disbursement_date,
- "disbursed_amount": disbursed_amount,
- "status": status,
- "total_payment": total_payment
- })
+ frappe.db.set_value(
+ "Loan",
+ self.against_loan,
+ {
+ "disbursement_date": self.disbursement_date,
+ "disbursed_amount": disbursed_amount,
+ "status": status,
+ "total_payment": total_payment,
+ },
+ )
def get_values_on_cancel(self, loan_details):
disbursed_amount = loan_details.disbursed_amount - self.disbursed_amount
@@ -94,8 +105,11 @@ class LoanDisbursement(AccountsController):
total_payment = loan_details.total_payment
if loan_details.status in ("Disbursed", "Partially Disbursed") and not loan_details.is_term_loan:
- process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1),
- loan=self.against_loan, accrual_type="Disbursement")
+ process_loan_interest_accrual_for_demand_loans(
+ posting_date=add_days(self.disbursement_date, -1),
+ loan=self.against_loan,
+ accrual_type="Disbursement",
+ )
if disbursed_amount > loan_details.loan_amount:
topup_amount = disbursed_amount - loan_details.loan_amount
@@ -117,75 +131,98 @@ class LoanDisbursement(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
- loan_details = frappe.get_doc("Loan", self.against_loan)
gle_map.append(
- self.get_gl_dict({
- "account": loan_details.loan_account,
- "against": loan_details.disbursement_account,
- "debit": self.disbursed_amount,
- "debit_in_account_currency": self.disbursed_amount,
- "against_voucher_type": "Loan",
- "against_voucher": self.against_loan,
- "remarks": _("Disbursement against loan:") + self.against_loan,
- "cost_center": self.cost_center,
- "party_type": self.applicant_type,
- "party": self.applicant,
- "posting_date": self.disbursement_date
- })
+ self.get_gl_dict(
+ {
+ "account": self.loan_account,
+ "against": self.disbursement_account,
+ "debit": self.disbursed_amount,
+ "debit_in_account_currency": self.disbursed_amount,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.against_loan,
+ "remarks": _("Disbursement against loan:") + self.against_loan,
+ "cost_center": self.cost_center,
+ "party_type": self.applicant_type,
+ "party": self.applicant,
+ "posting_date": self.disbursement_date,
+ }
+ )
)
gle_map.append(
- self.get_gl_dict({
- "account": loan_details.disbursement_account,
- "against": loan_details.loan_account,
- "credit": self.disbursed_amount,
- "credit_in_account_currency": self.disbursed_amount,
- "against_voucher_type": "Loan",
- "against_voucher": self.against_loan,
- "remarks": _("Disbursement against loan:") + self.against_loan,
- "cost_center": self.cost_center,
- "posting_date": self.disbursement_date
- })
+ self.get_gl_dict(
+ {
+ "account": self.disbursement_account,
+ "against": self.loan_account,
+ "credit": self.disbursed_amount,
+ "credit_in_account_currency": self.disbursed_amount,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.against_loan,
+ "remarks": _("Disbursement against loan:") + self.against_loan,
+ "cost_center": self.cost_center,
+ "posting_date": self.disbursement_date,
+ }
+ )
)
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj)
+
def get_total_pledged_security_value(loan):
update_time = get_datetime()
- loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price",
- fields=["loan_security", "loan_security_price"],
- filters = {
- "valid_from": ("<=", update_time),
- "valid_upto": (">=", update_time)
- }, as_list=1))
+ loan_security_price_map = frappe._dict(
+ frappe.get_all(
+ "Loan Security Price",
+ fields=["loan_security", "loan_security_price"],
+ filters={"valid_from": ("<=", update_time), "valid_upto": (">=", update_time)},
+ as_list=1,
+ )
+ )
- hair_cut_map = frappe._dict(frappe.get_all('Loan Security',
- fields=["name", "haircut"], as_list=1))
+ hair_cut_map = frappe._dict(
+ frappe.get_all("Loan Security", fields=["name", "haircut"], as_list=1)
+ )
security_value = 0.0
pledged_securities = get_pledged_security_qty(loan)
for security, qty in pledged_securities.items():
after_haircut_percentage = 100 - hair_cut_map.get(security)
- security_value += (loan_security_price_map.get(security) * qty * after_haircut_percentage)/100
+ security_value += (loan_security_price_map.get(security) * qty * after_haircut_percentage) / 100
return security_value
+
@frappe.whitelist()
def get_disbursal_amount(loan, on_current_security_price=0):
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
get_pending_principal_amount,
)
- loan_details = frappe.get_value("Loan", loan, ["loan_amount", "disbursed_amount", "total_payment",
- "total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan",
- "maximum_loan_amount", "written_off_amount"], as_dict=1)
+ loan_details = frappe.get_value(
+ "Loan",
+ loan,
+ [
+ "loan_amount",
+ "disbursed_amount",
+ "total_payment",
+ "total_principal_paid",
+ "total_interest_payable",
+ "status",
+ "is_term_loan",
+ "is_secured_loan",
+ "maximum_loan_amount",
+ "written_off_amount",
+ ],
+ as_dict=1,
+ )
- if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan,
- 'status': 'Pending'}):
+ if loan_details.is_secured_loan and frappe.get_all(
+ "Loan Security Shortfall", filters={"loan": loan, "status": "Pending"}
+ ):
return 0
pending_principal_amount = get_pending_principal_amount(loan_details)
@@ -202,10 +239,14 @@ def get_disbursal_amount(loan, on_current_security_price=0):
disbursal_amount = flt(security_value) - flt(pending_principal_amount)
- if loan_details.is_term_loan and (disbursal_amount + loan_details.loan_amount) > loan_details.loan_amount:
+ if (
+ loan_details.is_term_loan
+ and (disbursal_amount + loan_details.loan_amount) > loan_details.loan_amount
+ ):
disbursal_amount = loan_details.loan_amount - loan_details.disbursed_amount
return disbursal_amount
+
def get_maximum_amount_as_per_pledged_security(loan):
- return flt(frappe.db.get_value('Loan Security Pledge', {'loan': loan}, 'sum(maximum_loan_value)'))
+ return flt(frappe.db.get_value("Loan Security Pledge", {"loan": loan}, "sum(maximum_loan_value)"))
diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
index 10be750b449..4daa2edb28a 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
@@ -40,34 +40,50 @@ from erpnext.selling.doctype.customer.test_customer import get_customer_dict
class TestLoanDisbursement(unittest.TestCase):
-
def setUp(self):
create_loan_accounts()
- create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
- 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
+ create_loan_type(
+ "Demand Loan",
+ 2000000,
+ 13.5,
+ 25,
+ 0,
+ 5,
+ "Cash",
+ "Disbursement Account - _TC",
+ "Payment Account - _TC",
+ "Loan Account - _TC",
+ "Interest Income Account - _TC",
+ "Penalty Income Account - _TC",
+ )
create_loan_security_type()
create_loan_security()
- create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
- create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
+ create_loan_security_price(
+ "Test Security 1", 500, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24))
+ )
+ create_loan_security_price(
+ "Test Security 2", 250, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24))
+ )
if not frappe.db.exists("Customer", "_Test Loan Customer"):
- frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)
+ frappe.get_doc(get_customer_dict("_Test Loan Customer")).insert(ignore_permissions=True)
- self.applicant = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name')
+ self.applicant = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name")
def test_loan_topup(self):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00
- }]
+ pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
- loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant, "Demand Loan", pledge
+ )
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()))
+ loan = create_demand_loan(
+ self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())
+ )
loan.submit()
@@ -76,18 +92,22 @@ class TestLoanDisbursement(unittest.TestCase):
no_of_days = date_diff(last_date, first_date) + 1
- accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
- / (days_in_year(get_datetime().year) * 100)
+ accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / (
+ days_in_year(get_datetime().year) * 100
+ )
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date=add_days(last_date, 1))
# Should not be able to create loan disbursement entry before repayment
- self.assertRaises(frappe.ValidationError, make_loan_disbursement_entry, loan.name,
- 500000, first_date)
+ self.assertRaises(
+ frappe.ValidationError, make_loan_disbursement_entry, loan.name, 500000, first_date
+ )
- repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), 611095.89)
+ repayment_entry = create_repayment_entry(
+ loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), 611095.89
+ )
repayment_entry.submit()
loan.reload()
@@ -96,49 +116,48 @@ class TestLoanDisbursement(unittest.TestCase):
make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16))
# check for disbursement accrual
- loan_interest_accrual = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name,
- 'accrual_type': 'Disbursement'})
+ loan_interest_accrual = frappe.db.get_value(
+ "Loan Interest Accrual", {"loan": loan.name, "accrual_type": "Disbursement"}
+ )
self.assertTrue(loan_interest_accrual)
def test_loan_topup_with_additional_pledge(self):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00
- }]
+ pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
- loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant, "Demand Loan", pledge
+ )
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan = create_demand_loan(
+ self.applicant, "Demand Loan", loan_application, posting_date="2019-10-01"
+ )
loan.submit()
self.assertEqual(loan.loan_amount, 1000000)
- first_date = '2019-10-01'
- last_date = '2019-10-30'
+ first_date = "2019-10-01"
+ last_date = "2019-10-30"
# Disbursed 10,00,000 amount
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
- process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
+ process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 1))
- previous_interest = amounts['interest_amount']
+ previous_interest = amounts["interest_amount"]
- pledge1 = [{
- "loan_security": "Test Security 1",
- "qty": 2000.00
- }]
+ pledge1 = [{"loan_security": "Test Security 1", "qty": 2000.00}]
create_loan_security_pledge(self.applicant, pledge1, loan=loan.name)
# Topup 500000
make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 1))
- process_loan_interest_accrual_for_demand_loans(posting_date = add_days(last_date, 15))
+ process_loan_interest_accrual_for_demand_loans(posting_date=add_days(last_date, 15))
amounts = calculate_amounts(loan.name, add_days(last_date, 15))
- per_day_interest = get_per_day_interest(1500000, 13.5, '2019-10-30')
+ per_day_interest = get_per_day_interest(1500000, 13.5, "2019-10-30")
interest = per_day_interest * 15
- self.assertEqual(amounts['pending_principal_amount'], 1500000)
- self.assertEqual(amounts['interest_amount'], flt(interest + previous_interest, 2))
+ self.assertEqual(amounts["pending_principal_amount"], 1500000)
+ self.assertEqual(amounts["interest_amount"], flt(interest + previous_interest, 2))
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
index 1c800a06da0..0c4b051fba2 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
@@ -33,45 +33,51 @@ class LoanInterestAccrual(AccountsController):
self.update_is_accrued()
self.make_gl_entries(cancel=1)
- self.ignore_linked_doctypes = ['GL Entry']
+ self.ignore_linked_doctypes = ["GL Entry"]
def update_is_accrued(self):
- frappe.db.set_value('Repayment Schedule', self.repayment_schedule_name, 'is_accrued', 0)
+ frappe.db.set_value("Repayment Schedule", self.repayment_schedule_name, "is_accrued", 0)
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
if self.interest_amount:
gle_map.append(
- self.get_gl_dict({
- "account": self.loan_account,
- "party_type": self.applicant_type,
- "party": self.applicant,
- "against": self.interest_income_account,
- "debit": self.interest_amount,
- "debit_in_account_currency": self.interest_amount,
- "against_voucher_type": "Loan",
- "against_voucher": self.loan,
- "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format(
- self.last_accrual_date, self.posting_date, self.loan),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "posting_date": self.posting_date
- })
+ self.get_gl_dict(
+ {
+ "account": self.loan_account,
+ "party_type": self.applicant_type,
+ "party": self.applicant,
+ "against": self.interest_income_account,
+ "debit": self.interest_amount,
+ "debit_in_account_currency": self.interest_amount,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.loan,
+ "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format(
+ self.last_accrual_date, self.posting_date, self.loan
+ ),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "posting_date": self.posting_date,
+ }
+ )
)
gle_map.append(
- self.get_gl_dict({
- "account": self.interest_income_account,
- "against": self.loan_account,
- "credit": self.interest_amount,
- "credit_in_account_currency": self.interest_amount,
- "against_voucher_type": "Loan",
- "against_voucher": self.loan,
- "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format(
- self.last_accrual_date, self.posting_date, self.loan),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "posting_date": self.posting_date
- })
+ self.get_gl_dict(
+ {
+ "account": self.interest_income_account,
+ "against": self.loan_account,
+ "credit": self.interest_amount,
+ "credit_in_account_currency": self.interest_amount,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.loan,
+ "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format(
+ self.last_accrual_date, self.posting_date, self.loan
+ ),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "posting_date": self.posting_date,
+ }
+ )
)
if gle_map:
@@ -81,7 +87,9 @@ class LoanInterestAccrual(AccountsController):
# For Eg: If Loan disbursement date is '01-09-2019' and disbursed amount is 1000000 and
# rate of interest is 13.5 then first loan interest accural will be on '01-10-2019'
# which means interest will be accrued for 30 days which should be equal to 11095.89
-def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type):
+def calculate_accrual_amount_for_demand_loans(
+ loan, posting_date, process_loan_interest, accrual_type
+):
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
calculate_amounts,
get_pending_principal_amount,
@@ -95,51 +103,76 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i
pending_principal_amount = get_pending_principal_amount(loan)
- interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date)
+ interest_per_day = get_per_day_interest(
+ pending_principal_amount, loan.rate_of_interest, posting_date
+ )
payable_interest = interest_per_day * no_of_days
- pending_amounts = calculate_amounts(loan.name, posting_date, payment_type='Loan Closure')
+ pending_amounts = calculate_amounts(loan.name, posting_date, payment_type="Loan Closure")
- args = frappe._dict({
- 'loan': loan.name,
- 'applicant_type': loan.applicant_type,
- 'applicant': loan.applicant,
- 'interest_income_account': loan.interest_income_account,
- 'loan_account': loan.loan_account,
- 'pending_principal_amount': pending_principal_amount,
- 'interest_amount': payable_interest,
- 'total_pending_interest_amount': pending_amounts['interest_amount'],
- 'penalty_amount': pending_amounts['penalty_amount'],
- 'process_loan_interest': process_loan_interest,
- 'posting_date': posting_date,
- 'accrual_type': accrual_type
- })
+ args = frappe._dict(
+ {
+ "loan": loan.name,
+ "applicant_type": loan.applicant_type,
+ "applicant": loan.applicant,
+ "interest_income_account": loan.interest_income_account,
+ "loan_account": loan.loan_account,
+ "pending_principal_amount": pending_principal_amount,
+ "interest_amount": payable_interest,
+ "total_pending_interest_amount": pending_amounts["interest_amount"],
+ "penalty_amount": pending_amounts["penalty_amount"],
+ "process_loan_interest": process_loan_interest,
+ "posting_date": posting_date,
+ "accrual_type": accrual_type,
+ }
+ )
if flt(payable_interest, precision) > 0.0:
make_loan_interest_accrual_entry(args)
-def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None, accrual_type="Regular"):
- query_filters = {
- "status": ('in', ['Disbursed', 'Partially Disbursed']),
- "docstatus": 1
- }
+
+def make_accrual_interest_entry_for_demand_loans(
+ posting_date, process_loan_interest, open_loans=None, loan_type=None, accrual_type="Regular"
+):
+ query_filters = {"status": ("in", ["Disbursed", "Partially Disbursed"]), "docstatus": 1}
if loan_type:
- query_filters.update({
- "loan_type": loan_type
- })
+ query_filters.update({"loan_type": loan_type})
if not open_loans:
- open_loans = frappe.get_all("Loan",
- fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "loan_amount",
- "is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant",
- "rate_of_interest", "total_interest_payable", "written_off_amount", "total_principal_paid", "repayment_start_date"],
- filters=query_filters)
+ open_loans = frappe.get_all(
+ "Loan",
+ fields=[
+ "name",
+ "total_payment",
+ "total_amount_paid",
+ "loan_account",
+ "interest_income_account",
+ "loan_amount",
+ "is_term_loan",
+ "status",
+ "disbursement_date",
+ "disbursed_amount",
+ "applicant_type",
+ "applicant",
+ "rate_of_interest",
+ "total_interest_payable",
+ "written_off_amount",
+ "total_principal_paid",
+ "repayment_start_date",
+ ],
+ filters=query_filters,
+ )
for loan in open_loans:
- calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type)
+ calculate_accrual_amount_for_demand_loans(
+ loan, posting_date, process_loan_interest, accrual_type
+ )
-def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_interest, term_loan=None, loan_type=None, accrual_type="Regular"):
+
+def make_accrual_interest_entry_for_term_loans(
+ posting_date, process_loan_interest, term_loan=None, loan_type=None, accrual_type="Regular"
+):
curr_date = posting_date or add_days(nowdate(), 1)
term_loans = get_term_loans(curr_date, term_loan, loan_type)
@@ -148,37 +181,44 @@ def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_intere
for loan in term_loans:
accrued_entries.append(loan.payment_entry)
- args = frappe._dict({
- 'loan': loan.name,
- 'applicant_type': loan.applicant_type,
- 'applicant': loan.applicant,
- 'interest_income_account': loan.interest_income_account,
- 'loan_account': loan.loan_account,
- 'interest_amount': loan.interest_amount,
- 'payable_principal': loan.principal_amount,
- 'process_loan_interest': process_loan_interest,
- 'repayment_schedule_name': loan.payment_entry,
- 'posting_date': posting_date,
- 'accrual_type': accrual_type
- })
+ args = frappe._dict(
+ {
+ "loan": loan.name,
+ "applicant_type": loan.applicant_type,
+ "applicant": loan.applicant,
+ "interest_income_account": loan.interest_income_account,
+ "loan_account": loan.loan_account,
+ "interest_amount": loan.interest_amount,
+ "payable_principal": loan.principal_amount,
+ "process_loan_interest": process_loan_interest,
+ "repayment_schedule_name": loan.payment_entry,
+ "posting_date": posting_date,
+ "accrual_type": accrual_type,
+ }
+ )
make_loan_interest_accrual_entry(args)
if accrued_entries:
- frappe.db.sql("""UPDATE `tabRepayment Schedule`
- SET is_accrued = 1 where name in (%s)""" #nosec
- % ", ".join(['%s']*len(accrued_entries)), tuple(accrued_entries))
+ frappe.db.sql(
+ """UPDATE `tabRepayment Schedule`
+ SET is_accrued = 1 where name in (%s)""" # nosec
+ % ", ".join(["%s"] * len(accrued_entries)),
+ tuple(accrued_entries),
+ )
+
def get_term_loans(date, term_loan=None, loan_type=None):
- condition = ''
+ condition = ""
if term_loan:
- condition +=' AND l.name = %s' % frappe.db.escape(term_loan)
+ condition += " AND l.name = %s" % frappe.db.escape(term_loan)
if loan_type:
- condition += ' AND l.loan_type = %s' % frappe.db.escape(loan_type)
+ condition += " AND l.loan_type = %s" % frappe.db.escape(loan_type)
- term_loans = frappe.db.sql("""SELECT l.name, l.total_payment, l.total_amount_paid, l.loan_account,
+ term_loans = frappe.db.sql(
+ """SELECT l.name, l.total_payment, l.total_amount_paid, l.loan_account,
l.interest_income_account, l.is_term_loan, l.disbursement_date, l.applicant_type, l.applicant,
l.rate_of_interest, l.total_interest_payable, l.repayment_start_date, rs.name as payment_entry,
rs.payment_date, rs.principal_amount, rs.interest_amount, rs.is_accrued , rs.balance_loan_amount
@@ -189,10 +229,16 @@ def get_term_loans(date, term_loan=None, loan_type=None):
AND rs.payment_date <= %s
AND rs.is_accrued=0 {0}
AND l.status = 'Disbursed'
- ORDER BY rs.payment_date""".format(condition), (getdate(date)), as_dict=1)
+ ORDER BY rs.payment_date""".format(
+ condition
+ ),
+ (getdate(date)),
+ as_dict=1,
+ )
return term_loans
+
def make_loan_interest_accrual_entry(args):
precision = cint(frappe.db.get_default("currency_precision")) or 2
@@ -204,7 +250,9 @@ def make_loan_interest_accrual_entry(args):
loan_interest_accrual.loan_account = args.loan_account
loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision)
loan_interest_accrual.interest_amount = flt(args.interest_amount, precision)
- loan_interest_accrual.total_pending_interest_amount = flt(args.total_pending_interest_amount, precision)
+ loan_interest_accrual.total_pending_interest_amount = flt(
+ args.total_pending_interest_amount, precision
+ )
loan_interest_accrual.penalty_amount = flt(args.penalty_amount, precision)
loan_interest_accrual.posting_date = args.posting_date or nowdate()
loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest
@@ -223,15 +271,20 @@ def get_no_of_days_for_interest_accural(loan, posting_date):
return no_of_days
+
def get_last_accrual_date(loan):
- last_posting_date = frappe.db.sql(""" SELECT MAX(posting_date) from `tabLoan Interest Accrual`
- WHERE loan = %s and docstatus = 1""", (loan))
+ last_posting_date = frappe.db.sql(
+ """ SELECT MAX(posting_date) from `tabLoan Interest Accrual`
+ WHERE loan = %s and docstatus = 1""",
+ (loan),
+ )
if last_posting_date[0][0]:
# interest for last interest accrual date is already booked, so add 1 day
return add_days(last_posting_date[0][0], 1)
else:
- return frappe.db.get_value('Loan', loan, 'disbursement_date')
+ return frappe.db.get_value("Loan", loan, "disbursement_date")
+
def days_in_year(year):
days = 365
@@ -241,8 +294,11 @@ def days_in_year(year):
return days
+
def get_per_day_interest(principal_amount, rate_of_interest, posting_date=None):
if not posting_date:
posting_date = getdate()
- return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100))
+ return flt(
+ (principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100)
+ )
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
index e8c77506fcb..fd59393b827 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
@@ -30,78 +30,98 @@ class TestLoanInterestAccrual(unittest.TestCase):
def setUp(self):
create_loan_accounts()
- create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
- 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
+ create_loan_type(
+ "Demand Loan",
+ 2000000,
+ 13.5,
+ 25,
+ 0,
+ 5,
+ "Cash",
+ "Disbursement Account - _TC",
+ "Payment Account - _TC",
+ "Loan Account - _TC",
+ "Interest Income Account - _TC",
+ "Penalty Income Account - _TC",
+ )
create_loan_security_type()
create_loan_security()
- create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
+ create_loan_security_price(
+ "Test Security 1", 500, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24))
+ )
if not frappe.db.exists("Customer", "_Test Loan Customer"):
- frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)
+ frappe.get_doc(get_customer_dict("_Test Loan Customer")).insert(ignore_permissions=True)
- self.applicant = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name')
+ self.applicant = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name")
def test_loan_interest_accural(self):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00
- }]
+ pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
- loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant, "Demand Loan", pledge
+ )
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant, "Demand Loan", loan_application,
- posting_date=get_first_day(nowdate()))
+ loan = create_demand_loan(
+ self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())
+ )
loan.submit()
- first_date = '2019-10-01'
- last_date = '2019-10-30'
+ first_date = "2019-10-01"
+ last_date = "2019-10-30"
no_of_days = date_diff(last_date, first_date) + 1
- accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
- / (days_in_year(get_datetime(first_date).year) * 100)
+ accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / (
+ days_in_year(get_datetime(first_date).year) * 100
+ )
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
- loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name})
+ loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {"loan": loan.name})
self.assertEqual(flt(loan_interest_accural.interest_amount, 0), flt(accrued_interest_amount, 0))
def test_accumulated_amounts(self):
- pledge = [{
- "loan_security": "Test Security 1",
- "qty": 4000.00
- }]
+ pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
- loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge)
+ loan_application = create_loan_application(
+ "_Test Company", self.applicant, "Demand Loan", pledge
+ )
create_pledge(loan_application)
- loan = create_demand_loan(self.applicant, "Demand Loan", loan_application,
- posting_date=get_first_day(nowdate()))
+ loan = create_demand_loan(
+ self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())
+ )
loan.submit()
- first_date = '2019-10-01'
- last_date = '2019-10-30'
+ first_date = "2019-10-01"
+ last_date = "2019-10-30"
no_of_days = date_diff(last_date, first_date) + 1
- accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
- / (days_in_year(get_datetime(first_date).year) * 100)
+ accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / (
+ days_in_year(get_datetime(first_date).year) * 100
+ )
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
- loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name})
+ loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {"loan": loan.name})
self.assertEqual(flt(loan_interest_accrual.interest_amount, 0), flt(accrued_interest_amount, 0))
- next_start_date = '2019-10-31'
- next_end_date = '2019-11-29'
+ next_start_date = "2019-10-31"
+ next_end_date = "2019-11-29"
no_of_days = date_diff(next_end_date, next_start_date) + 1
process = process_loan_interest_accrual_for_demand_loans(posting_date=next_end_date)
- new_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
- / (days_in_year(get_datetime(first_date).year) * 100)
+ new_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / (
+ days_in_year(get_datetime(first_date).year) * 100
+ )
total_pending_interest_amount = flt(accrued_interest_amount + new_accrued_interest_amount, 0)
- loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name,
- 'process_loan_interest_accrual': process})
- self.assertEqual(flt(loan_interest_accrual.total_pending_interest_amount, 0), total_pending_interest_amount)
+ loan_interest_accrual = frappe.get_doc(
+ "Loan Interest Accrual", {"loan": loan.name, "process_loan_interest_accrual": process}
+ )
+ self.assertEqual(
+ flt(loan_interest_accrual.total_pending_interest_amount, 0), total_pending_interest_amount
+ )
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
index 93ef2170420..480e010b49a 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
@@ -1,7 +1,7 @@
{
"actions": [],
"autoname": "LM-REP-.####",
- "creation": "2019-09-03 14:44:39.977266",
+ "creation": "2022-01-25 10:30:02.767941",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
@@ -13,6 +13,7 @@
"column_break_3",
"company",
"posting_date",
+ "clearance_date",
"rate_of_interest",
"payroll_payable_account",
"is_term_loan",
@@ -37,7 +38,12 @@
"total_penalty_paid",
"total_interest_paid",
"repayment_details",
- "amended_from"
+ "amended_from",
+ "accounting_details_section",
+ "payment_account",
+ "penalty_income_account",
+ "column_break_36",
+ "loan_account"
],
"fields": [
{
@@ -260,12 +266,52 @@
"fieldname": "repay_from_salary",
"fieldtype": "Check",
"label": "Repay From Salary"
+ },
+ {
+ "fieldname": "clearance_date",
+ "fieldtype": "Date",
+ "label": "Clearance Date",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "accounting_details_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fetch_from": "against_loan.payment_account",
+ "fieldname": "payment_account",
+ "fieldtype": "Link",
+ "label": "Repayment Account",
+ "options": "Account",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_36",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "against_loan.loan_account",
+ "fieldname": "loan_account",
+ "fieldtype": "Link",
+ "label": "Loan Account",
+ "options": "Account",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "against_loan.penalty_income_account",
+ "fieldname": "penalty_income_account",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Penalty Income Account",
+ "options": "Account"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-01-06 01:51:06.707782",
+ "modified": "2022-02-18 19:10:07.742298",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment",
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index a6e526a0490..6a6eb591629 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -23,7 +23,6 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_
class LoanRepayment(AccountsController):
-
def validate(self):
amounts = calculate_amounts(self.against_loan, self.posting_date)
self.set_missing_values(amounts)
@@ -43,7 +42,7 @@ class LoanRepayment(AccountsController):
self.check_future_accruals()
self.update_repayment_schedule(cancel=1)
self.mark_as_unpaid()
- self.ignore_linked_doctypes = ['GL Entry']
+ self.ignore_linked_doctypes = ["GL Entry"]
self.make_gl_entries(cancel=1)
def set_missing_values(self, amounts):
@@ -56,32 +55,38 @@ class LoanRepayment(AccountsController):
self.cost_center = erpnext.get_default_cost_center(self.company)
if not self.interest_payable:
- self.interest_payable = flt(amounts['interest_amount'], precision)
+ self.interest_payable = flt(amounts["interest_amount"], precision)
if not self.penalty_amount:
- self.penalty_amount = flt(amounts['penalty_amount'], precision)
+ self.penalty_amount = flt(amounts["penalty_amount"], precision)
if not self.pending_principal_amount:
- self.pending_principal_amount = flt(amounts['pending_principal_amount'], precision)
+ self.pending_principal_amount = flt(amounts["pending_principal_amount"], precision)
if not self.payable_principal_amount and self.is_term_loan:
- self.payable_principal_amount = flt(amounts['payable_principal_amount'], precision)
+ self.payable_principal_amount = flt(amounts["payable_principal_amount"], precision)
if not self.payable_amount:
- self.payable_amount = flt(amounts['payable_amount'], precision)
+ self.payable_amount = flt(amounts["payable_amount"], precision)
- shortfall_amount = flt(frappe.db.get_value('Loan Security Shortfall', {'loan': self.against_loan, 'status': 'Pending'},
- 'shortfall_amount'))
+ shortfall_amount = flt(
+ frappe.db.get_value(
+ "Loan Security Shortfall", {"loan": self.against_loan, "status": "Pending"}, "shortfall_amount"
+ )
+ )
if shortfall_amount:
self.shortfall_amount = shortfall_amount
- if amounts.get('due_date'):
- self.due_date = amounts.get('due_date')
+ if amounts.get("due_date"):
+ self.due_date = amounts.get("due_date")
def check_future_entries(self):
- future_repayment_date = frappe.db.get_value("Loan Repayment", {"posting_date": (">", self.posting_date),
- "docstatus": 1, "against_loan": self.against_loan}, 'posting_date')
+ future_repayment_date = frappe.db.get_value(
+ "Loan Repayment",
+ {"posting_date": (">", self.posting_date), "docstatus": 1, "against_loan": self.against_loan},
+ "posting_date",
+ )
if future_repayment_date:
frappe.throw("Repayment already made till date {0}".format(get_datetime(future_repayment_date)))
@@ -100,106 +105,167 @@ class LoanRepayment(AccountsController):
last_accrual_date = get_last_accrual_date(self.against_loan)
# get posting date upto which interest has to be accrued
- per_day_interest = get_per_day_interest(self.pending_principal_amount,
- self.rate_of_interest, self.posting_date)
+ per_day_interest = get_per_day_interest(
+ self.pending_principal_amount, self.rate_of_interest, self.posting_date
+ )
- no_of_days = flt(flt(self.total_interest_paid - self.interest_payable,
- precision)/per_day_interest, 0) - 1
+ no_of_days = (
+ flt(flt(self.total_interest_paid - self.interest_payable, precision) / per_day_interest, 0)
+ - 1
+ )
posting_date = add_days(last_accrual_date, no_of_days)
# book excess interest paid
- process = process_loan_interest_accrual_for_demand_loans(posting_date=posting_date,
- loan=self.against_loan, accrual_type="Repayment")
+ process = process_loan_interest_accrual_for_demand_loans(
+ posting_date=posting_date, loan=self.against_loan, accrual_type="Repayment"
+ )
# get loan interest accrual to update paid amount
- lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual':
- process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1)
+ lia = frappe.db.get_value(
+ "Loan Interest Accrual",
+ {"process_loan_interest_accrual": process},
+ ["name", "interest_amount", "payable_principal_amount"],
+ as_dict=1,
+ )
if lia:
- self.append('repayment_details', {
- 'loan_interest_accrual': lia.name,
- 'paid_interest_amount': flt(self.total_interest_paid - self.interest_payable, precision),
- 'paid_principal_amount': 0.0,
- 'accrual_type': 'Repayment'
- })
+ self.append(
+ "repayment_details",
+ {
+ "loan_interest_accrual": lia.name,
+ "paid_interest_amount": flt(self.total_interest_paid - self.interest_payable, precision),
+ "paid_principal_amount": 0.0,
+ "accrual_type": "Repayment",
+ },
+ )
def update_paid_amount(self):
- loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid',
- 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable',
- 'written_off_amount'], as_dict=1)
+ loan = frappe.get_value(
+ "Loan",
+ self.against_loan,
+ [
+ "total_amount_paid",
+ "total_principal_paid",
+ "status",
+ "is_secured_loan",
+ "total_payment",
+ "loan_amount",
+ "disbursed_amount",
+ "total_interest_payable",
+ "written_off_amount",
+ ],
+ as_dict=1,
+ )
- loan.update({
- 'total_amount_paid': loan.total_amount_paid + self.amount_paid,
- 'total_principal_paid': loan.total_principal_paid + self.principal_amount_paid
- })
+ loan.update(
+ {
+ "total_amount_paid": loan.total_amount_paid + self.amount_paid,
+ "total_principal_paid": loan.total_principal_paid + self.principal_amount_paid,
+ }
+ )
pending_principal_amount = get_pending_principal_amount(loan)
if not loan.is_secured_loan and pending_principal_amount <= 0:
- loan.update({'status': 'Loan Closure Requested'})
+ loan.update({"status": "Loan Closure Requested"})
for payment in self.repayment_details:
- frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
+ frappe.db.sql(
+ """ UPDATE `tabLoan Interest Accrual`
SET paid_principal_amount = `paid_principal_amount` + %s,
paid_interest_amount = `paid_interest_amount` + %s
WHERE name = %s""",
- (flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), payment.loan_interest_accrual))
+ (
+ flt(payment.paid_principal_amount),
+ flt(payment.paid_interest_amount),
+ payment.loan_interest_accrual,
+ ),
+ )
- frappe.db.sql(""" UPDATE `tabLoan`
+ frappe.db.sql(
+ """ UPDATE `tabLoan`
SET total_amount_paid = %s, total_principal_paid = %s, status = %s
- WHERE name = %s """, (loan.total_amount_paid, loan.total_principal_paid, loan.status,
- self.against_loan))
+ WHERE name = %s """,
+ (loan.total_amount_paid, loan.total_principal_paid, loan.status, self.against_loan),
+ )
update_shortfall_status(self.against_loan, self.principal_amount_paid)
def mark_as_unpaid(self):
- loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid',
- 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable',
- 'written_off_amount'], as_dict=1)
+ loan = frappe.get_value(
+ "Loan",
+ self.against_loan,
+ [
+ "total_amount_paid",
+ "total_principal_paid",
+ "status",
+ "is_secured_loan",
+ "total_payment",
+ "loan_amount",
+ "disbursed_amount",
+ "total_interest_payable",
+ "written_off_amount",
+ ],
+ as_dict=1,
+ )
no_of_repayments = len(self.repayment_details)
- loan.update({
- 'total_amount_paid': loan.total_amount_paid - self.amount_paid,
- 'total_principal_paid': loan.total_principal_paid - self.principal_amount_paid
- })
+ loan.update(
+ {
+ "total_amount_paid": loan.total_amount_paid - self.amount_paid,
+ "total_principal_paid": loan.total_principal_paid - self.principal_amount_paid,
+ }
+ )
- if loan.status == 'Loan Closure Requested':
+ if loan.status == "Loan Closure Requested":
if loan.disbursed_amount >= loan.loan_amount:
- loan['status'] = 'Disbursed'
+ loan["status"] = "Disbursed"
else:
- loan['status'] = 'Partially Disbursed'
+ loan["status"] = "Partially Disbursed"
for payment in self.repayment_details:
- frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
+ frappe.db.sql(
+ """ UPDATE `tabLoan Interest Accrual`
SET paid_principal_amount = `paid_principal_amount` - %s,
paid_interest_amount = `paid_interest_amount` - %s
WHERE name = %s""",
- (payment.paid_principal_amount, payment.paid_interest_amount, payment.loan_interest_accrual))
+ (payment.paid_principal_amount, payment.paid_interest_amount, payment.loan_interest_accrual),
+ )
# Cancel repayment interest accrual
# checking idx as a preventive measure, repayment accrual will always be the last entry
- if payment.accrual_type == 'Repayment' and payment.idx == no_of_repayments:
- lia_doc = frappe.get_doc('Loan Interest Accrual', payment.loan_interest_accrual)
+ if payment.accrual_type == "Repayment" and payment.idx == no_of_repayments:
+ lia_doc = frappe.get_doc("Loan Interest Accrual", payment.loan_interest_accrual)
lia_doc.cancel()
- frappe.db.sql(""" UPDATE `tabLoan`
+ frappe.db.sql(
+ """ UPDATE `tabLoan`
SET total_amount_paid = %s, total_principal_paid = %s, status = %s
- WHERE name = %s """, (loan.total_amount_paid, loan.total_principal_paid, loan.status, self.against_loan))
+ WHERE name = %s """,
+ (loan.total_amount_paid, loan.total_principal_paid, loan.status, self.against_loan),
+ )
def check_future_accruals(self):
- future_accrual_date = frappe.db.get_value("Loan Interest Accrual", {"posting_date": (">", self.posting_date),
- "docstatus": 1, "loan": self.against_loan}, 'posting_date')
+ future_accrual_date = frappe.db.get_value(
+ "Loan Interest Accrual",
+ {"posting_date": (">", self.posting_date), "docstatus": 1, "loan": self.against_loan},
+ "posting_date",
+ )
if future_accrual_date:
- frappe.throw("Cannot cancel. Interest accruals already processed till {0}".format(get_datetime(future_accrual_date)))
+ frappe.throw(
+ "Cannot cancel. Interest accruals already processed till {0}".format(
+ get_datetime(future_accrual_date)
+ )
+ )
def update_repayment_schedule(self, cancel=0):
if self.is_term_loan and self.principal_amount_paid > self.payable_principal_amount:
regenerate_repayment_schedule(self.against_loan, cancel)
def allocate_amounts(self, repayment_details):
- self.set('repayment_details', [])
+ self.set("repayment_details", [])
self.principal_amount_paid = 0
self.total_penalty_paid = 0
interest_paid = self.amount_paid
@@ -232,15 +298,15 @@ class LoanRepayment(AccountsController):
idx = 1
if interest_paid > 0:
- for lia, amounts in iteritems(repayment_details.get('pending_accrual_entries', [])):
+ for lia, amounts in iteritems(repayment_details.get("pending_accrual_entries", [])):
interest_amount = 0
- if amounts['interest_amount'] <= interest_paid:
- interest_amount = amounts['interest_amount']
+ if amounts["interest_amount"] <= interest_paid:
+ interest_amount = amounts["interest_amount"]
self.total_interest_paid += interest_amount
interest_paid -= interest_amount
elif interest_paid:
- if interest_paid >= amounts['interest_amount']:
- interest_amount = amounts['interest_amount']
+ if interest_paid >= amounts["interest_amount"]:
+ interest_amount = amounts["interest_amount"]
self.total_interest_paid += interest_amount
interest_paid = 0
else:
@@ -249,27 +315,32 @@ class LoanRepayment(AccountsController):
interest_paid = 0
if interest_amount:
- self.append('repayment_details', {
- 'loan_interest_accrual': lia,
- 'paid_interest_amount': interest_amount,
- 'paid_principal_amount': 0
- })
+ self.append(
+ "repayment_details",
+ {
+ "loan_interest_accrual": lia,
+ "paid_interest_amount": interest_amount,
+ "paid_principal_amount": 0,
+ },
+ )
updated_entries[lia] = idx
idx += 1
return interest_paid, updated_entries
- def allocate_principal_amount_for_term_loans(self, interest_paid, repayment_details, updated_entries):
+ def allocate_principal_amount_for_term_loans(
+ self, interest_paid, repayment_details, updated_entries
+ ):
if interest_paid > 0:
- for lia, amounts in iteritems(repayment_details.get('pending_accrual_entries', [])):
+ for lia, amounts in iteritems(repayment_details.get("pending_accrual_entries", [])):
paid_principal = 0
- if amounts['payable_principal_amount'] <= interest_paid:
- paid_principal = amounts['payable_principal_amount']
+ if amounts["payable_principal_amount"] <= interest_paid:
+ paid_principal = amounts["payable_principal_amount"]
self.principal_amount_paid += paid_principal
interest_paid -= paid_principal
elif interest_paid:
- if interest_paid >= amounts['payable_principal_amount']:
- paid_principal = amounts['payable_principal_amount']
+ if interest_paid >= amounts["payable_principal_amount"]:
+ paid_principal = amounts["payable_principal_amount"]
self.principal_amount_paid += paid_principal
interest_paid = 0
else:
@@ -279,30 +350,34 @@ class LoanRepayment(AccountsController):
if updated_entries.get(lia):
idx = updated_entries.get(lia)
- self.get('repayment_details')[idx-1].paid_principal_amount += paid_principal
+ self.get("repayment_details")[idx - 1].paid_principal_amount += paid_principal
else:
- self.append('repayment_details', {
- 'loan_interest_accrual': lia,
- 'paid_interest_amount': 0,
- 'paid_principal_amount': paid_principal
- })
+ self.append(
+ "repayment_details",
+ {
+ "loan_interest_accrual": lia,
+ "paid_interest_amount": 0,
+ "paid_principal_amount": paid_principal,
+ },
+ )
if interest_paid > 0:
self.principal_amount_paid += interest_paid
def allocate_excess_payment_for_demand_loans(self, interest_paid, repayment_details):
- if repayment_details['unaccrued_interest'] and interest_paid > 0:
+ if repayment_details["unaccrued_interest"] and interest_paid > 0:
# no of days for which to accrue interest
# Interest can only be accrued for an entire day and not partial
- if interest_paid > repayment_details['unaccrued_interest']:
- interest_paid -= repayment_details['unaccrued_interest']
- self.total_interest_paid += repayment_details['unaccrued_interest']
+ if interest_paid > repayment_details["unaccrued_interest"]:
+ interest_paid -= repayment_details["unaccrued_interest"]
+ self.total_interest_paid += repayment_details["unaccrued_interest"]
else:
# get no of days for which interest can be paid
- per_day_interest = get_per_day_interest(self.pending_principal_amount,
- self.rate_of_interest, self.posting_date)
+ per_day_interest = get_per_day_interest(
+ self.pending_principal_amount, self.rate_of_interest, self.posting_date
+ )
- no_of_days = cint(interest_paid/per_day_interest)
+ no_of_days = cint(interest_paid / per_day_interest)
self.total_interest_paid += no_of_days * per_day_interest
interest_paid -= no_of_days * per_day_interest
@@ -311,11 +386,11 @@ class LoanRepayment(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
- loan_details = frappe.get_doc("Loan", self.against_loan)
if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
- remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount,
- self.against_loan)
+ remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(
+ self.shortfall_amount, self.against_loan
+ )
elif self.shortfall_amount:
remarks = _("Shortfall Repayment of {0}").format(self.shortfall_amount)
else:
@@ -324,96 +399,117 @@ class LoanRepayment(AccountsController):
if self.repay_from_salary:
payment_account = self.payroll_payable_account
else:
- payment_account = loan_details.payment_account
+ payment_account = self.payment_account
if self.total_penalty_paid:
gle_map.append(
- self.get_gl_dict({
- "account": loan_details.loan_account,
- "against": loan_details.payment_account,
- "debit": self.total_penalty_paid,
- "debit_in_account_currency": self.total_penalty_paid,
- "against_voucher_type": "Loan",
- "against_voucher": self.against_loan,
- "remarks": _("Penalty against loan:") + self.against_loan,
- "cost_center": self.cost_center,
- "party_type": self.applicant_type,
- "party": self.applicant,
- "posting_date": getdate(self.posting_date)
- })
+ self.get_gl_dict(
+ {
+ "account": self.loan_account,
+ "against": payment_account,
+ "debit": self.total_penalty_paid,
+ "debit_in_account_currency": self.total_penalty_paid,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.against_loan,
+ "remarks": _("Penalty against loan:") + self.against_loan,
+ "cost_center": self.cost_center,
+ "party_type": self.applicant_type,
+ "party": self.applicant,
+ "posting_date": getdate(self.posting_date),
+ }
+ )
)
gle_map.append(
- self.get_gl_dict({
- "account": loan_details.penalty_income_account,
- "against": loan_details.loan_account,
- "credit": self.total_penalty_paid,
- "credit_in_account_currency": self.total_penalty_paid,
- "against_voucher_type": "Loan",
- "against_voucher": self.against_loan,
- "remarks": _("Penalty against loan:") + self.against_loan,
- "cost_center": self.cost_center,
- "posting_date": getdate(self.posting_date)
- })
+ self.get_gl_dict(
+ {
+ "account": self.penalty_income_account,
+ "against": self.loan_account,
+ "credit": self.total_penalty_paid,
+ "credit_in_account_currency": self.total_penalty_paid,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.against_loan,
+ "remarks": _("Penalty against loan:") + self.against_loan,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(self.posting_date),
+ }
+ )
)
gle_map.append(
- self.get_gl_dict({
- "account": payment_account,
- "against": loan_details.loan_account + ", " + loan_details.interest_income_account
- + ", " + loan_details.penalty_income_account,
- "debit": self.amount_paid,
- "debit_in_account_currency": self.amount_paid,
- "against_voucher_type": "Loan",
- "against_voucher": self.against_loan,
- "remarks": remarks,
- "cost_center": self.cost_center,
- "posting_date": getdate(self.posting_date),
- "party_type": loan_details.applicant_type if self.repay_from_salary else '',
- "party": loan_details.applicant if self.repay_from_salary else ''
- })
+ self.get_gl_dict(
+ {
+ "account": payment_account,
+ "against": self.loan_account + ", " + self.penalty_income_account,
+ "debit": self.amount_paid,
+ "debit_in_account_currency": self.amount_paid,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.against_loan,
+ "remarks": remarks,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(self.posting_date),
+ "party_type": self.applicant_type if self.repay_from_salary else "",
+ "party": self.applicant if self.repay_from_salary else "",
+ }
+ )
)
gle_map.append(
- self.get_gl_dict({
- "account": loan_details.loan_account,
- "party_type": loan_details.applicant_type,
- "party": loan_details.applicant,
- "against": payment_account,
- "credit": self.amount_paid,
- "credit_in_account_currency": self.amount_paid,
- "against_voucher_type": "Loan",
- "against_voucher": self.against_loan,
- "remarks": remarks,
- "cost_center": self.cost_center,
- "posting_date": getdate(self.posting_date)
- })
+ self.get_gl_dict(
+ {
+ "account": self.loan_account,
+ "party_type": self.applicant_type,
+ "party": self.applicant,
+ "against": payment_account,
+ "credit": self.amount_paid,
+ "credit_in_account_currency": self.amount_paid,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.against_loan,
+ "remarks": remarks,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(self.posting_date),
+ }
+ )
)
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
-def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
- payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None,
- payroll_payable_account=None):
- lr = frappe.get_doc({
- "doctype": "Loan Repayment",
- "against_loan": loan,
- "payment_type": payment_type,
- "company": company,
- "posting_date": posting_date,
- "applicant": applicant,
- "penalty_amount": penalty_amount,
- "interest_payable": interest_payable,
- "payable_principal_amount": payable_principal_amount,
- "amount_paid": amount_paid,
- "loan_type": loan_type,
- "payroll_payable_account": payroll_payable_account
- }).insert()
+def create_repayment_entry(
+ loan,
+ applicant,
+ company,
+ posting_date,
+ loan_type,
+ payment_type,
+ interest_payable,
+ payable_principal_amount,
+ amount_paid,
+ penalty_amount=None,
+ payroll_payable_account=None,
+):
+
+ lr = frappe.get_doc(
+ {
+ "doctype": "Loan Repayment",
+ "against_loan": loan,
+ "payment_type": payment_type,
+ "company": company,
+ "posting_date": posting_date,
+ "applicant": applicant,
+ "penalty_amount": penalty_amount,
+ "interest_payable": interest_payable,
+ "payable_principal_amount": payable_principal_amount,
+ "amount_paid": amount_paid,
+ "loan_type": loan_type,
+ "payroll_payable_account": payroll_payable_account,
+ }
+ ).insert()
return lr
+
def get_accrued_interest_entries(against_loan, posting_date=None):
if not posting_date:
posting_date = getdate()
@@ -433,35 +529,43 @@ def get_accrued_interest_entries(against_loan, posting_date=None):
AND
docstatus = 1
ORDER BY posting_date
- """, (against_loan, posting_date), as_dict=1)
+ """,
+ (against_loan, posting_date),
+ as_dict=1,
+ )
return unpaid_accrued_entries
+
def get_penalty_details(against_loan):
- penalty_details = frappe.db.sql("""
+ penalty_details = frappe.db.sql(
+ """
SELECT posting_date, (penalty_amount - total_penalty_paid) as pending_penalty_amount
FROM `tabLoan Repayment` where posting_date >= (SELECT MAX(posting_date) from `tabLoan Repayment`
where against_loan = %s) and docstatus = 1 and against_loan = %s
- """, (against_loan, against_loan))
+ """,
+ (against_loan, against_loan),
+ )
if penalty_details:
return penalty_details[0][0], flt(penalty_details[0][1])
else:
return None, 0
+
def regenerate_repayment_schedule(loan, cancel=0):
from erpnext.loan_management.doctype.loan.loan import (
add_single_month,
get_monthly_repayment_amount,
)
- loan_doc = frappe.get_doc('Loan', loan)
+ loan_doc = frappe.get_doc("Loan", loan)
next_accrual_date = None
accrued_entries = 0
last_repayment_amount = 0
last_balance_amount = 0
- for term in reversed(loan_doc.get('repayment_schedule')):
+ for term in reversed(loan_doc.get("repayment_schedule")):
if not term.is_accrued:
next_accrual_date = term.payment_date
loan_doc.remove(term)
@@ -476,20 +580,24 @@ def regenerate_repayment_schedule(loan, cancel=0):
balance_amount = get_pending_principal_amount(loan_doc)
- if loan_doc.repayment_method == 'Repay Fixed Amount per Period':
- monthly_repayment_amount = flt(balance_amount/len(loan_doc.get('repayment_schedule')) - accrued_entries)
+ if loan_doc.repayment_method == "Repay Fixed Amount per Period":
+ monthly_repayment_amount = flt(
+ balance_amount / len(loan_doc.get("repayment_schedule")) - accrued_entries
+ )
else:
- if not cancel:
- monthly_repayment_amount = get_monthly_repayment_amount(balance_amount,
- loan_doc.rate_of_interest, loan_doc.repayment_periods - accrued_entries)
+ repayment_period = loan_doc.repayment_periods - accrued_entries
+ if not cancel and repayment_period > 0:
+ monthly_repayment_amount = get_monthly_repayment_amount(
+ balance_amount, loan_doc.rate_of_interest, repayment_period
+ )
else:
monthly_repayment_amount = last_repayment_amount
balance_amount = last_balance_amount
payment_date = next_accrual_date
- while(balance_amount > 0):
- interest_amount = flt(balance_amount * flt(loan_doc.rate_of_interest) / (12*100))
+ while balance_amount > 0:
+ interest_amount = flt(balance_amount * flt(loan_doc.rate_of_interest) / (12 * 100))
principal_amount = monthly_repayment_amount - interest_amount
balance_amount = flt(balance_amount + interest_amount - monthly_repayment_amount)
if balance_amount < 0:
@@ -497,31 +605,45 @@ def regenerate_repayment_schedule(loan, cancel=0):
balance_amount = 0.0
total_payment = principal_amount + interest_amount
- loan_doc.append("repayment_schedule", {
- "payment_date": payment_date,
- "principal_amount": principal_amount,
- "interest_amount": interest_amount,
- "total_payment": total_payment,
- "balance_loan_amount": balance_amount
- })
+ loan_doc.append(
+ "repayment_schedule",
+ {
+ "payment_date": payment_date,
+ "principal_amount": principal_amount,
+ "interest_amount": interest_amount,
+ "total_payment": total_payment,
+ "balance_loan_amount": balance_amount,
+ },
+ )
next_payment_date = add_single_month(payment_date)
payment_date = next_payment_date
loan_doc.save()
+
def get_pending_principal_amount(loan):
- if loan.status in ('Disbursed', 'Closed') or loan.disbursed_amount >= loan.loan_amount:
- pending_principal_amount = flt(loan.total_payment) - flt(loan.total_principal_paid) \
- - flt(loan.total_interest_payable) - flt(loan.written_off_amount)
+ if loan.status in ("Disbursed", "Closed") or loan.disbursed_amount >= loan.loan_amount:
+ pending_principal_amount = (
+ flt(loan.total_payment)
+ - flt(loan.total_principal_paid)
+ - flt(loan.total_interest_payable)
+ - flt(loan.written_off_amount)
+ )
else:
- pending_principal_amount = flt(loan.disbursed_amount) - flt(loan.total_principal_paid) \
- - flt(loan.total_interest_payable) - flt(loan.written_off_amount)
+ pending_principal_amount = (
+ flt(loan.disbursed_amount)
+ - flt(loan.total_principal_paid)
+ - flt(loan.total_interest_payable)
+ - flt(loan.written_off_amount)
+ )
return pending_principal_amount
+
# This function returns the amounts that are payable at the time of loan repayment based on posting date
# So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable
+
def get_amounts(amounts, against_loan, posting_date):
precision = cint(frappe.db.get_default("currency_precision")) or 2
@@ -535,8 +657,8 @@ def get_amounts(amounts, against_loan, posting_date):
total_pending_interest = 0
penalty_amount = 0
payable_principal_amount = 0
- final_due_date = ''
- due_date = ''
+ final_due_date = ""
+ due_date = ""
for entry in accrued_interest_entries:
# Loan repayment due date is one day after the loan interest is accrued
@@ -552,16 +674,25 @@ def get_amounts(amounts, against_loan, posting_date):
no_of_late_days = date_diff(posting_date, due_date_after_grace_period) + 1
- if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary) and entry.accrual_type == 'Regular':
- penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)
+ if (
+ no_of_late_days > 0
+ and (not against_loan_doc.repay_from_salary)
+ and entry.accrual_type == "Regular"
+ ):
+ penalty_amount += (
+ entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days
+ )
total_pending_interest += entry.interest_amount
payable_principal_amount += entry.payable_principal_amount
- pending_accrual_entries.setdefault(entry.name, {
- 'interest_amount': flt(entry.interest_amount, precision),
- 'payable_principal_amount': flt(entry.payable_principal_amount, precision)
- })
+ pending_accrual_entries.setdefault(
+ entry.name,
+ {
+ "interest_amount": flt(entry.interest_amount, precision),
+ "payable_principal_amount": flt(entry.payable_principal_amount, precision),
+ },
+ )
if due_date and not final_due_date:
final_due_date = add_days(due_date, loan_type_details.grace_period_in_days)
@@ -577,14 +708,18 @@ def get_amounts(amounts, against_loan, posting_date):
if pending_days > 0:
principal_amount = flt(pending_principal_amount, precision)
- per_day_interest = get_per_day_interest(principal_amount, loan_type_details.rate_of_interest, posting_date)
- unaccrued_interest += (pending_days * per_day_interest)
+ per_day_interest = get_per_day_interest(
+ principal_amount, loan_type_details.rate_of_interest, posting_date
+ )
+ unaccrued_interest += pending_days * per_day_interest
amounts["pending_principal_amount"] = flt(pending_principal_amount, precision)
amounts["payable_principal_amount"] = flt(payable_principal_amount, precision)
amounts["interest_amount"] = flt(total_pending_interest, precision)
amounts["penalty_amount"] = flt(penalty_amount + pending_penalty_amount, precision)
- amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision)
+ amounts["payable_amount"] = flt(
+ payable_principal_amount + total_pending_interest + penalty_amount, precision
+ )
amounts["pending_accrual_entries"] = pending_accrual_entries
amounts["unaccrued_interest"] = flt(unaccrued_interest, precision)
@@ -593,24 +728,28 @@ def get_amounts(amounts, against_loan, posting_date):
return amounts
+
@frappe.whitelist()
-def calculate_amounts(against_loan, posting_date, payment_type=''):
+def calculate_amounts(against_loan, posting_date, payment_type=""):
amounts = {
- 'penalty_amount': 0.0,
- 'interest_amount': 0.0,
- 'pending_principal_amount': 0.0,
- 'payable_principal_amount': 0.0,
- 'payable_amount': 0.0,
- 'unaccrued_interest': 0.0,
- 'due_date': ''
+ "penalty_amount": 0.0,
+ "interest_amount": 0.0,
+ "pending_principal_amount": 0.0,
+ "payable_principal_amount": 0.0,
+ "payable_amount": 0.0,
+ "unaccrued_interest": 0.0,
+ "due_date": "",
}
amounts = get_amounts(amounts, against_loan, posting_date)
# update values for closure
- if payment_type == 'Loan Closure':
- amounts['payable_principal_amount'] = amounts['pending_principal_amount']
- amounts['interest_amount'] += amounts['unaccrued_interest']
- amounts['payable_amount'] = amounts['payable_principal_amount'] + amounts['interest_amount']
+ if payment_type == "Loan Closure":
+ amounts["payable_principal_amount"] = amounts["pending_principal_amount"]
+ amounts["interest_amount"] += amounts["unaccrued_interest"]
+ amounts["payable_amount"] = amounts["payable_principal_amount"] + amounts["interest_amount"]
+ amounts["payable_amount"] = (
+ amounts["payable_principal_amount"] + amounts["interest_amount"] + amounts["penalty_amount"]
+ )
return amounts
diff --git a/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py b/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py
index 8d5c525ac31..1d96885e155 100644
--- a/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py
+++ b/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py
@@ -1,14 +1,8 @@
-
-
def get_data():
return {
- 'fieldname': 'loan_security',
- 'transactions': [
- {
- 'items': ['Loan Application', 'Loan Security Price']
- },
- {
- 'items': ['Loan Security Pledge', 'Loan Security Unpledge']
- }
- ]
+ "fieldname": "loan_security",
+ "transactions": [
+ {"items": ["Loan Application", "Loan Security Price"]},
+ {"items": ["Loan Security Pledge", "Loan Security Unpledge"]},
+ ],
}
diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py
index 7d02645609b..f0d59542753 100644
--- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py
+++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py
@@ -40,22 +40,28 @@ class LoanSecurityPledge(Document):
if security.loan_security not in security_list:
security_list.append(security.loan_security)
else:
- frappe.throw(_('Loan Security {0} added multiple times').format(frappe.bold(
- security.loan_security)))
+ frappe.throw(
+ _("Loan Security {0} added multiple times").format(frappe.bold(security.loan_security))
+ )
def validate_loan_security_type(self):
- existing_pledge = ''
+ existing_pledge = ""
if self.loan:
- existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan, 'docstatus': 1}, ['name'])
+ existing_pledge = frappe.db.get_value(
+ "Loan Security Pledge", {"loan": self.loan, "docstatus": 1}, ["name"]
+ )
if existing_pledge:
- loan_security_type = frappe.db.get_value('Pledge', {'parent': existing_pledge}, ['loan_security_type'])
+ loan_security_type = frappe.db.get_value(
+ "Pledge", {"parent": existing_pledge}, ["loan_security_type"]
+ )
else:
loan_security_type = self.securities[0].loan_security_type
- ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type",
- fields=["name", "loan_to_value_ratio"], as_list=1))
+ ltv_ratio_map = frappe._dict(
+ frappe.get_all("Loan Security Type", fields=["name", "loan_to_value_ratio"], as_list=1)
+ )
ltv_ratio = ltv_ratio_map.get(loan_security_type)
@@ -63,7 +69,6 @@ class LoanSecurityPledge(Document):
if ltv_ratio_map.get(security.loan_security_type) != ltv_ratio:
frappe.throw(_("Loan Securities with different LTV ratio cannot be pledged against one loan"))
-
def set_pledge_amount(self):
total_security_value = 0
maximum_loan_value = 0
@@ -77,10 +82,10 @@ class LoanSecurityPledge(Document):
pledge.loan_security_price = get_loan_security_price(pledge.loan_security)
if not pledge.qty:
- pledge.qty = cint(pledge.amount/pledge.loan_security_price)
+ pledge.qty = cint(pledge.amount / pledge.loan_security_price)
pledge.amount = pledge.qty * pledge.loan_security_price
- pledge.post_haircut_amount = cint(pledge.amount - (pledge.amount * pledge.haircut/100))
+ pledge.post_haircut_amount = cint(pledge.amount - (pledge.amount * pledge.haircut / 100))
total_security_value += pledge.amount
maximum_loan_value += pledge.post_haircut_amount
@@ -88,12 +93,19 @@ class LoanSecurityPledge(Document):
self.total_security_value = total_security_value
self.maximum_loan_value = maximum_loan_value
+
def update_loan(loan, maximum_value_against_pledge, cancel=0):
- maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount'])
+ maximum_loan_value = frappe.db.get_value("Loan", {"name": loan}, ["maximum_loan_amount"])
if cancel:
- frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s
- WHERE name=%s""", (maximum_loan_value - maximum_value_against_pledge, loan))
+ frappe.db.sql(
+ """ UPDATE `tabLoan` SET maximum_loan_amount=%s
+ WHERE name=%s""",
+ (maximum_loan_value - maximum_value_against_pledge, loan),
+ )
else:
- frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1
- WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan))
+ frappe.db.sql(
+ """ UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1
+ WHERE name=%s""",
+ (maximum_loan_value + maximum_value_against_pledge, loan),
+ )
diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py
index fca9dd6bcb9..45c4459ac3f 100644
--- a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py
+++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py
@@ -17,23 +17,37 @@ class LoanSecurityPrice(Document):
if self.valid_from > self.valid_upto:
frappe.throw(_("Valid From Time must be lesser than Valid Upto Time."))
- existing_loan_security = frappe.db.sql(""" SELECT name from `tabLoan Security Price`
+ existing_loan_security = frappe.db.sql(
+ """ SELECT name from `tabLoan Security Price`
WHERE loan_security = %s AND name != %s AND (valid_from BETWEEN %s and %s OR valid_upto BETWEEN %s and %s) """,
- (self.loan_security, self.name, self.valid_from, self.valid_upto, self.valid_from, self.valid_upto))
+ (
+ self.loan_security,
+ self.name,
+ self.valid_from,
+ self.valid_upto,
+ self.valid_from,
+ self.valid_upto,
+ ),
+ )
if existing_loan_security:
frappe.throw(_("Loan Security Price overlapping with {0}").format(existing_loan_security[0][0]))
+
@frappe.whitelist()
def get_loan_security_price(loan_security, valid_time=None):
if not valid_time:
valid_time = get_datetime()
- loan_security_price = frappe.db.get_value("Loan Security Price", {
- 'loan_security': loan_security,
- 'valid_from': ("<=",valid_time),
- 'valid_upto': (">=", valid_time)
- }, 'loan_security_price')
+ loan_security_price = frappe.db.get_value(
+ "Loan Security Price",
+ {
+ "loan_security": loan_security,
+ "valid_from": ("<=", valid_time),
+ "valid_upto": (">=", valid_time),
+ },
+ "loan_security_price",
+ )
if not loan_security_price:
frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(loan_security)))
diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
index 20e451b81ea..b901e626b43 100644
--- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
+++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
@@ -14,27 +14,42 @@ from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpled
class LoanSecurityShortfall(Document):
pass
+
def update_shortfall_status(loan, security_value, on_cancel=0):
- loan_security_shortfall = frappe.db.get_value("Loan Security Shortfall",
- {"loan": loan, "status": "Pending"}, ['name', 'shortfall_amount'], as_dict=1)
+ loan_security_shortfall = frappe.db.get_value(
+ "Loan Security Shortfall",
+ {"loan": loan, "status": "Pending"},
+ ["name", "shortfall_amount"],
+ as_dict=1,
+ )
if not loan_security_shortfall:
return
if security_value >= loan_security_shortfall.shortfall_amount:
- frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, {
- "status": "Completed",
- "shortfall_amount": loan_security_shortfall.shortfall_amount,
- "shortfall_percentage": 0
- })
+ frappe.db.set_value(
+ "Loan Security Shortfall",
+ loan_security_shortfall.name,
+ {
+ "status": "Completed",
+ "shortfall_amount": loan_security_shortfall.shortfall_amount,
+ "shortfall_percentage": 0,
+ },
+ )
else:
- frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name,
- "shortfall_amount", loan_security_shortfall.shortfall_amount - security_value)
+ frappe.db.set_value(
+ "Loan Security Shortfall",
+ loan_security_shortfall.name,
+ "shortfall_amount",
+ loan_security_shortfall.shortfall_amount - security_value,
+ )
@frappe.whitelist()
def add_security(loan):
- loan_details = frappe.db.get_value("Loan", loan, ['applicant', 'company', 'applicant_type'], as_dict=1)
+ loan_details = frappe.db.get_value(
+ "Loan", loan, ["applicant", "company", "applicant_type"], as_dict=1
+ )
loan_security_pledge = frappe.new_doc("Loan Security Pledge")
loan_security_pledge.loan = loan
@@ -44,33 +59,51 @@ def add_security(loan):
return loan_security_pledge.as_dict()
+
def check_for_ltv_shortfall(process_loan_security_shortfall):
update_time = get_datetime()
- loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price",
- fields=["loan_security", "loan_security_price"],
- filters = {
- "valid_from": ("<=", update_time),
- "valid_upto": (">=", update_time)
- }, as_list=1))
+ loan_security_price_map = frappe._dict(
+ frappe.get_all(
+ "Loan Security Price",
+ fields=["loan_security", "loan_security_price"],
+ filters={"valid_from": ("<=", update_time), "valid_upto": (">=", update_time)},
+ as_list=1,
+ )
+ )
- loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid', 'total_payment',
- 'total_interest_payable', 'disbursed_amount', 'status'],
- filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1})
+ loans = frappe.get_all(
+ "Loan",
+ fields=[
+ "name",
+ "loan_amount",
+ "total_principal_paid",
+ "total_payment",
+ "total_interest_payable",
+ "disbursed_amount",
+ "status",
+ ],
+ filters={"status": ("in", ["Disbursed", "Partially Disbursed"]), "is_secured_loan": 1},
+ )
- loan_shortfall_map = frappe._dict(frappe.get_all("Loan Security Shortfall",
- fields=["loan", "name"], filters={"status": "Pending"}, as_list=1))
+ loan_shortfall_map = frappe._dict(
+ frappe.get_all(
+ "Loan Security Shortfall", fields=["loan", "name"], filters={"status": "Pending"}, as_list=1
+ )
+ )
loan_security_map = {}
for loan in loans:
- if loan.status == 'Disbursed':
- outstanding_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
- - flt(loan.total_principal_paid)
+ if loan.status == "Disbursed":
+ outstanding_amount = (
+ flt(loan.total_payment) - flt(loan.total_interest_payable) - flt(loan.total_principal_paid)
+ )
else:
- outstanding_amount = flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \
- - flt(loan.total_principal_paid)
+ outstanding_amount = (
+ flt(loan.disbursed_amount) - flt(loan.total_interest_payable) - flt(loan.total_principal_paid)
+ )
pledged_securities = get_pledged_security_qty(loan.name)
ltv_ratio = 0.0
@@ -81,21 +114,36 @@ def check_for_ltv_shortfall(process_loan_security_shortfall):
ltv_ratio = get_ltv_ratio(security)
security_value += flt(loan_security_price_map.get(security)) * flt(qty)
- current_ratio = (outstanding_amount/security_value) * 100 if security_value else 0
+ current_ratio = (outstanding_amount / security_value) * 100 if security_value else 0
if current_ratio > ltv_ratio:
shortfall_amount = outstanding_amount - ((security_value * ltv_ratio) / 100)
- create_loan_security_shortfall(loan.name, outstanding_amount, security_value, shortfall_amount,
- current_ratio, process_loan_security_shortfall)
+ create_loan_security_shortfall(
+ loan.name,
+ outstanding_amount,
+ security_value,
+ shortfall_amount,
+ current_ratio,
+ process_loan_security_shortfall,
+ )
elif loan_shortfall_map.get(loan.name):
shortfall_amount = outstanding_amount - ((security_value * ltv_ratio) / 100)
if shortfall_amount <= 0:
shortfall = loan_shortfall_map.get(loan.name)
update_pending_shortfall(shortfall)
-def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, shortfall_ratio,
- process_loan_security_shortfall):
- existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name")
+
+def create_loan_security_shortfall(
+ loan,
+ loan_amount,
+ security_value,
+ shortfall_amount,
+ shortfall_ratio,
+ process_loan_security_shortfall,
+):
+ existing_shortfall = frappe.db.get_value(
+ "Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name"
+ )
if existing_shortfall:
ltv_shortfall = frappe.get_doc("Loan Security Shortfall", existing_shortfall)
@@ -111,16 +159,17 @@ def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_
ltv_shortfall.process_loan_security_shortfall = process_loan_security_shortfall
ltv_shortfall.save()
+
def get_ltv_ratio(loan_security):
- loan_security_type = frappe.db.get_value('Loan Security', loan_security, 'loan_security_type')
- ltv_ratio = frappe.db.get_value('Loan Security Type', loan_security_type, 'loan_to_value_ratio')
+ loan_security_type = frappe.db.get_value("Loan Security", loan_security, "loan_security_type")
+ ltv_ratio = frappe.db.get_value("Loan Security Type", loan_security_type, "loan_to_value_ratio")
return ltv_ratio
+
def update_pending_shortfall(shortfall):
# Get all pending loan security shortfall
- frappe.db.set_value("Loan Security Shortfall", shortfall,
- {
- "status": "Completed",
- "shortfall_amount": 0,
- "shortfall_percentage": 0
- })
+ frappe.db.set_value(
+ "Loan Security Shortfall",
+ shortfall,
+ {"status": "Completed", "shortfall_amount": 0, "shortfall_percentage": 0},
+ )
diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py b/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py
index daa9958808f..8fc4520ccd2 100644
--- a/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py
+++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py
@@ -1,14 +1,8 @@
-
-
def get_data():
return {
- 'fieldname': 'loan_security_type',
- 'transactions': [
- {
- 'items': ['Loan Security', 'Loan Security Price']
- },
- {
- 'items': ['Loan Security Pledge', 'Loan Security Unpledge']
- }
- ]
+ "fieldname": "loan_security_type",
+ "transactions": [
+ {"items": ["Loan Security", "Loan Security Price"]},
+ {"items": ["Loan Security Pledge", "Loan Security Unpledge"]},
+ ],
}
diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
index b716288fc59..731b65e9a29 100644
--- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
+++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
@@ -16,7 +16,7 @@ class LoanSecurityUnpledge(Document):
def on_cancel(self):
self.update_loan_status(cancel=1)
- self.db_set('status', 'Requested')
+ self.db_set("status", "Requested")
def validate_duplicate_securities(self):
security_list = []
@@ -24,8 +24,11 @@ class LoanSecurityUnpledge(Document):
if d.loan_security not in security_list:
security_list.append(d.loan_security)
else:
- frappe.throw(_("Row {0}: Loan Security {1} added multiple times").format(
- d.idx, frappe.bold(d.loan_security)))
+ frappe.throw(
+ _("Row {0}: Loan Security {1} added multiple times").format(
+ d.idx, frappe.bold(d.loan_security)
+ )
+ )
def validate_unpledge_qty(self):
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
@@ -37,18 +40,33 @@ class LoanSecurityUnpledge(Document):
pledge_qty_map = get_pledged_security_qty(self.loan)
- ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type",
- fields=["name", "loan_to_value_ratio"], as_list=1))
+ ltv_ratio_map = frappe._dict(
+ frappe.get_all("Loan Security Type", fields=["name", "loan_to_value_ratio"], as_list=1)
+ )
- loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price",
- fields=["loan_security", "loan_security_price"],
- filters = {
- "valid_from": ("<=", get_datetime()),
- "valid_upto": (">=", get_datetime())
- }, as_list=1))
+ loan_security_price_map = frappe._dict(
+ frappe.get_all(
+ "Loan Security Price",
+ fields=["loan_security", "loan_security_price"],
+ filters={"valid_from": ("<=", get_datetime()), "valid_upto": (">=", get_datetime())},
+ as_list=1,
+ )
+ )
- loan_details = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', 'loan_amount',
- 'total_interest_payable', 'written_off_amount', 'disbursed_amount', 'status'], as_dict=1)
+ loan_details = frappe.get_value(
+ "Loan",
+ self.loan,
+ [
+ "total_payment",
+ "total_principal_paid",
+ "loan_amount",
+ "total_interest_payable",
+ "written_off_amount",
+ "disbursed_amount",
+ "status",
+ ],
+ as_dict=1,
+ )
pending_principal_amount = get_pending_principal_amount(loan_details)
@@ -59,8 +77,13 @@ class LoanSecurityUnpledge(Document):
for security in self.securities:
pledged_qty = pledge_qty_map.get(security.loan_security, 0)
if security.qty > pledged_qty:
- msg = _("Row {0}: {1} {2} of {3} is pledged against Loan {4}.").format(security.idx, pledged_qty, security.uom,
- frappe.bold(security.loan_security), frappe.bold(self.loan))
+ msg = _("Row {0}: {1} {2} of {3} is pledged against Loan {4}.").format(
+ security.idx,
+ pledged_qty,
+ security.uom,
+ frappe.bold(security.loan_security),
+ frappe.bold(self.loan),
+ )
msg += " "
msg += _("You are trying to unpledge more.")
frappe.throw(msg, title=_("Loan Security Unpledge Error"))
@@ -79,14 +102,14 @@ class LoanSecurityUnpledge(Document):
if not security_value and flt(pending_principal_amount, 2) > 0:
self._throw(security_value, pending_principal_amount, ltv_ratio)
- if security_value and flt(pending_principal_amount/security_value) * 100 > ltv_ratio:
+ if security_value and flt(pending_principal_amount / security_value) * 100 > ltv_ratio:
self._throw(security_value, pending_principal_amount, ltv_ratio)
def _throw(self, security_value, pending_principal_amount, ltv_ratio):
msg = _("Loan Security Value after unpledge is {0}").format(frappe.bold(security_value))
- msg += ' '
+ msg += " "
msg += _("Pending principal amount is {0}").format(frappe.bold(flt(pending_principal_amount, 2)))
- msg += ' '
+ msg += " "
msg += _("Loan To Security Value ratio must always be {0}").format(frappe.bold(ltv_ratio))
frappe.throw(msg, title=_("Loan To Value ratio breach"))
@@ -96,13 +119,13 @@ class LoanSecurityUnpledge(Document):
def approve(self):
if self.status == "Approved" and not self.unpledge_time:
self.update_loan_status()
- self.db_set('unpledge_time', get_datetime())
+ self.db_set("unpledge_time", get_datetime())
def update_loan_status(self, cancel=0):
if cancel:
- loan_status = frappe.get_value('Loan', self.loan, 'status')
- if loan_status == 'Closed':
- frappe.db.set_value('Loan', self.loan, 'status', 'Loan Closure Requested')
+ loan_status = frappe.get_value("Loan", self.loan, "status")
+ if loan_status == "Closed":
+ frappe.db.set_value("Loan", self.loan, "status", "Loan Closure Requested")
else:
pledged_qty = 0
current_pledges = get_pledged_security_qty(self.loan)
@@ -111,34 +134,41 @@ class LoanSecurityUnpledge(Document):
pledged_qty += qty
if not pledged_qty:
- frappe.db.set_value('Loan', self.loan,
- {
- 'status': 'Closed',
- 'closure_date': getdate()
- })
+ frappe.db.set_value("Loan", self.loan, {"status": "Closed", "closure_date": getdate()})
+
@frappe.whitelist()
def get_pledged_security_qty(loan):
current_pledges = {}
- unpledges = frappe._dict(frappe.db.sql("""
+ unpledges = frappe._dict(
+ frappe.db.sql(
+ """
SELECT u.loan_security, sum(u.qty) as qty
FROM `tabLoan Security Unpledge` up, `tabUnpledge` u
WHERE up.loan = %s
AND u.parent = up.name
AND up.status = 'Approved'
GROUP BY u.loan_security
- """, (loan)))
+ """,
+ (loan),
+ )
+ )
- pledges = frappe._dict(frappe.db.sql("""
+ pledges = frappe._dict(
+ frappe.db.sql(
+ """
SELECT p.loan_security, sum(p.qty) as qty
FROM `tabLoan Security Pledge` lp, `tabPledge`p
WHERE lp.loan = %s
AND p.parent = lp.name
AND lp.status = 'Pledged'
GROUP BY p.loan_security
- """, (loan)))
+ """,
+ (loan),
+ )
+ )
for security, qty in iteritems(pledges):
current_pledges.setdefault(security, qty)
diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.py b/erpnext/loan_management/doctype/loan_type/loan_type.py
index 592229cf994..51ee05b277e 100644
--- a/erpnext/loan_management/doctype/loan_type/loan_type.py
+++ b/erpnext/loan_management/doctype/loan_type/loan_type.py
@@ -12,12 +12,20 @@ class LoanType(Document):
self.validate_accounts()
def validate_accounts(self):
- for fieldname in ['payment_account', 'loan_account', 'interest_income_account', 'penalty_income_account']:
- company = frappe.get_value("Account", self.get(fieldname), 'company')
+ for fieldname in [
+ "payment_account",
+ "loan_account",
+ "interest_income_account",
+ "penalty_income_account",
+ ]:
+ company = frappe.get_value("Account", self.get(fieldname), "company")
if company and company != self.company:
- frappe.throw(_("Account {0} does not belong to company {1}").format(frappe.bold(self.get(fieldname)),
- frappe.bold(self.company)))
+ frappe.throw(
+ _("Account {0} does not belong to company {1}").format(
+ frappe.bold(self.get(fieldname)), frappe.bold(self.company)
+ )
+ )
- if self.get('loan_account') == self.get('payment_account'):
- frappe.throw(_('Loan Account and Payment Account cannot be same'))
+ if self.get("loan_account") == self.get("payment_account"):
+ frappe.throw(_("Loan Account and Payment Account cannot be same"))
diff --git a/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py b/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py
index 19026da2f66..e2467c64558 100644
--- a/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py
+++ b/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py
@@ -1,14 +1,5 @@
-
-
def get_data():
return {
- 'fieldname': 'loan_type',
- 'transactions': [
- {
- 'items': ['Loan Repayment', 'Loan']
- },
- {
- 'items': ['Loan Application']
- }
- ]
+ "fieldname": "loan_type",
+ "transactions": [{"items": ["Loan Repayment", "Loan"]}, {"items": ["Loan Application"]}],
}
diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py
index 35be587f87f..e19fd15fc84 100644
--- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py
+++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py
@@ -22,11 +22,16 @@ class LoanWriteOff(AccountsController):
def validate_write_off_amount(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2
- total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan,
- ['total_payment', 'total_principal_paid','total_interest_payable', 'written_off_amount'])
+ total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value(
+ "Loan",
+ self.loan,
+ ["total_payment", "total_principal_paid", "total_interest_payable", "written_off_amount"],
+ )
- pending_principal_amount = flt(flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount),
- precision)
+ pending_principal_amount = flt(
+ flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount),
+ precision,
+ )
if self.write_off_amount > pending_principal_amount:
frappe.throw(_("Write off amount cannot be greater than pending principal amount"))
@@ -37,52 +42,55 @@ class LoanWriteOff(AccountsController):
def on_cancel(self):
self.update_outstanding_amount(cancel=1)
- self.ignore_linked_doctypes = ['GL Entry']
+ self.ignore_linked_doctypes = ["GL Entry"]
self.make_gl_entries(cancel=1)
def update_outstanding_amount(self, cancel=0):
- written_off_amount = frappe.db.get_value('Loan', self.loan, 'written_off_amount')
+ written_off_amount = frappe.db.get_value("Loan", self.loan, "written_off_amount")
if cancel:
written_off_amount -= self.write_off_amount
else:
written_off_amount += self.write_off_amount
- frappe.db.set_value('Loan', self.loan, 'written_off_amount', written_off_amount)
-
+ frappe.db.set_value("Loan", self.loan, "written_off_amount", written_off_amount)
def make_gl_entries(self, cancel=0):
gl_entries = []
loan_details = frappe.get_doc("Loan", self.loan)
gl_entries.append(
- self.get_gl_dict({
- "account": self.write_off_account,
- "against": loan_details.loan_account,
- "debit": self.write_off_amount,
- "debit_in_account_currency": self.write_off_amount,
- "against_voucher_type": "Loan",
- "against_voucher": self.loan,
- "remarks": _("Against Loan:") + self.loan,
- "cost_center": self.cost_center,
- "posting_date": getdate(self.posting_date)
- })
+ self.get_gl_dict(
+ {
+ "account": self.write_off_account,
+ "against": loan_details.loan_account,
+ "debit": self.write_off_amount,
+ "debit_in_account_currency": self.write_off_amount,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.loan,
+ "remarks": _("Against Loan:") + self.loan,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(self.posting_date),
+ }
+ )
)
gl_entries.append(
- self.get_gl_dict({
- "account": loan_details.loan_account,
- "party_type": loan_details.applicant_type,
- "party": loan_details.applicant,
- "against": self.write_off_account,
- "credit": self.write_off_amount,
- "credit_in_account_currency": self.write_off_amount,
- "against_voucher_type": "Loan",
- "against_voucher": self.loan,
- "remarks": _("Against Loan:") + self.loan,
- "cost_center": self.cost_center,
- "posting_date": getdate(self.posting_date)
- })
+ self.get_gl_dict(
+ {
+ "account": loan_details.loan_account,
+ "party_type": loan_details.applicant_type,
+ "party": loan_details.applicant,
+ "against": self.write_off_account,
+ "credit": self.write_off_amount,
+ "credit_in_account_currency": self.write_off_amount,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.loan,
+ "remarks": _("Against Loan:") + self.loan,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(self.posting_date),
+ }
+ )
)
make_gl_entries(gl_entries, cancel=cancel, merge_entries=False)
diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py
index 4c34ccd983e..81464a36c3d 100644
--- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py
@@ -17,24 +17,36 @@ class ProcessLoanInterestAccrual(Document):
open_loans = []
if self.loan:
- loan_doc = frappe.get_doc('Loan', self.loan)
+ loan_doc = frappe.get_doc("Loan", self.loan)
if loan_doc:
open_loans.append(loan_doc)
- if (not self.loan or not loan_doc.is_term_loan) and self.process_type != 'Term Loans':
- make_accrual_interest_entry_for_demand_loans(self.posting_date, self.name,
- open_loans = open_loans, loan_type = self.loan_type, accrual_type=self.accrual_type)
+ if (not self.loan or not loan_doc.is_term_loan) and self.process_type != "Term Loans":
+ make_accrual_interest_entry_for_demand_loans(
+ self.posting_date,
+ self.name,
+ open_loans=open_loans,
+ loan_type=self.loan_type,
+ accrual_type=self.accrual_type,
+ )
- if (not self.loan or loan_doc.is_term_loan) and self.process_type != 'Demand Loans':
- make_accrual_interest_entry_for_term_loans(self.posting_date, self.name, term_loan=self.loan,
- loan_type=self.loan_type, accrual_type=self.accrual_type)
+ if (not self.loan or loan_doc.is_term_loan) and self.process_type != "Demand Loans":
+ make_accrual_interest_entry_for_term_loans(
+ self.posting_date,
+ self.name,
+ term_loan=self.loan,
+ loan_type=self.loan_type,
+ accrual_type=self.accrual_type,
+ )
-def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=None, loan=None, accrual_type="Regular"):
- loan_process = frappe.new_doc('Process Loan Interest Accrual')
+def process_loan_interest_accrual_for_demand_loans(
+ posting_date=None, loan_type=None, loan=None, accrual_type="Regular"
+):
+ loan_process = frappe.new_doc("Process Loan Interest Accrual")
loan_process.posting_date = posting_date or nowdate()
loan_process.loan_type = loan_type
- loan_process.process_type = 'Demand Loans'
+ loan_process.process_type = "Demand Loans"
loan_process.loan = loan
loan_process.accrual_type = accrual_type
@@ -42,25 +54,26 @@ def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=
return loan_process.name
+
def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=None, loan=None):
if not term_loan_accrual_pending(posting_date or nowdate()):
return
- loan_process = frappe.new_doc('Process Loan Interest Accrual')
+ loan_process = frappe.new_doc("Process Loan Interest Accrual")
loan_process.posting_date = posting_date or nowdate()
loan_process.loan_type = loan_type
- loan_process.process_type = 'Term Loans'
+ loan_process.process_type = "Term Loans"
loan_process.loan = loan
loan_process.submit()
return loan_process.name
+
def term_loan_accrual_pending(date):
- pending_accrual = frappe.db.get_value('Repayment Schedule', {
- 'payment_date': ('<=', date),
- 'is_accrued': 0
- })
+ pending_accrual = frappe.db.get_value(
+ "Repayment Schedule", {"payment_date": ("<=", date), "is_accrued": 0}
+ )
return pending_accrual
diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py
index 4195960890c..ac85df761ce 100644
--- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py
+++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py
@@ -1,11 +1,5 @@
-
-
def get_data():
return {
- 'fieldname': 'process_loan_interest_accrual',
- 'transactions': [
- {
- 'items': ['Loan Interest Accrual']
- }
- ]
+ "fieldname": "process_loan_interest_accrual",
+ "transactions": [{"items": ["Loan Interest Accrual"]}],
}
diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py
index ba9fb0c449f..fffc5d4876b 100644
--- a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py
+++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py
@@ -13,16 +13,18 @@ from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_short
class ProcessLoanSecurityShortfall(Document):
def onload(self):
- self.set_onload('update_time', get_datetime())
+ self.set_onload("update_time", get_datetime())
def on_submit(self):
check_for_ltv_shortfall(self.name)
+
def create_process_loan_security_shortfall():
if check_for_secured_loans():
process = frappe.new_doc("Process Loan Security Shortfall")
process.update_time = get_datetime()
process.submit()
+
def check_for_secured_loans():
- return frappe.db.count('Loan', {'docstatus': 1, 'is_secured_loan': 1})
+ return frappe.db.count("Loan", {"docstatus": 1, "is_secured_loan": 1})
diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py
index fa9d18b6fab..4d7b1630bad 100644
--- a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py
+++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py
@@ -1,11 +1,5 @@
-
-
def get_data():
return {
- 'fieldname': 'process_loan_security_shortfall',
- 'transactions': [
- {
- 'items': ['Loan Security Shortfall']
- }
- ]
+ "fieldname": "process_loan_security_shortfall",
+ "transactions": [{"items": ["Loan Security Shortfall"]}],
}
diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py
index 6063b7bad8b..e7487cb34d3 100644
--- a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py
+++ b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py
@@ -9,9 +9,13 @@ from frappe.model.document import Document
class SanctionedLoanAmount(Document):
def validate(self):
- sanctioned_doc = frappe.db.exists('Sanctioned Loan Amount', {'applicant': self.applicant, 'company': self.company})
+ sanctioned_doc = frappe.db.exists(
+ "Sanctioned Loan Amount", {"applicant": self.applicant, "company": self.company}
+ )
if sanctioned_doc and sanctioned_doc != self.name:
- frappe.throw(_("Sanctioned Loan Amount already exists for {0} against company {1}").format(
- frappe.bold(self.applicant), frappe.bold(self.company)
- ))
+ frappe.throw(
+ _("Sanctioned Loan Amount already exists for {0} against company {1}").format(
+ frappe.bold(self.applicant), frappe.bold(self.company)
+ )
+ )
diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py
index 8ebca39061c..75c4b2877b1 100644
--- a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py
+++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py
@@ -18,84 +18,166 @@ def execute(filters=None):
def get_columns(filters):
columns = [
- {"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100},
- {"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150},
- {"label": _("Loan Security"), "fieldname": "loan_security", "fieldtype": "Link", "options": "Loan Security", "width": 160},
- {"label": _("Loan Security Code"), "fieldname": "loan_security_code", "fieldtype": "Data", "width": 100},
- {"label": _("Loan Security Name"), "fieldname": "loan_security_name", "fieldtype": "Data", "width": 150},
+ {
+ "label": _("Applicant Type"),
+ "fieldname": "applicant_type",
+ "options": "DocType",
+ "width": 100,
+ },
+ {
+ "label": _("Applicant Name"),
+ "fieldname": "applicant_name",
+ "fieldtype": "Dynamic Link",
+ "options": "applicant_type",
+ "width": 150,
+ },
+ {
+ "label": _("Loan Security"),
+ "fieldname": "loan_security",
+ "fieldtype": "Link",
+ "options": "Loan Security",
+ "width": 160,
+ },
+ {
+ "label": _("Loan Security Code"),
+ "fieldname": "loan_security_code",
+ "fieldtype": "Data",
+ "width": 100,
+ },
+ {
+ "label": _("Loan Security Name"),
+ "fieldname": "loan_security_name",
+ "fieldtype": "Data",
+ "width": 150,
+ },
{"label": _("Haircut"), "fieldname": "haircut", "fieldtype": "Percent", "width": 100},
- {"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120},
+ {
+ "label": _("Loan Security Type"),
+ "fieldname": "loan_security_type",
+ "fieldtype": "Link",
+ "options": "Loan Security Type",
+ "width": 120,
+ },
{"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80},
{"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100},
- {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100},
- {"label": _("Price Valid Upto"), "fieldname": "price_valid_upto", "fieldtype": "Datetime", "width": 100},
- {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100},
- {"label": _("% Of Applicant Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100},
- {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
+ {
+ "label": _("Latest Price"),
+ "fieldname": "latest_price",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 100,
+ },
+ {
+ "label": _("Price Valid Upto"),
+ "fieldname": "price_valid_upto",
+ "fieldtype": "Datetime",
+ "width": 100,
+ },
+ {
+ "label": _("Current Value"),
+ "fieldname": "current_value",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 100,
+ },
+ {
+ "label": _("% Of Applicant Portfolio"),
+ "fieldname": "portfolio_percent",
+ "fieldtype": "Percentage",
+ "width": 100,
+ },
+ {
+ "label": _("Currency"),
+ "fieldname": "currency",
+ "fieldtype": "Currency",
+ "options": "Currency",
+ "hidden": 1,
+ "width": 100,
+ },
]
return columns
+
def get_data(filters):
data = []
loan_security_details = get_loan_security_details()
- pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters,
- loan_security_details)
+ pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(
+ filters, loan_security_details
+ )
- currency = erpnext.get_company_currency(filters.get('company'))
+ currency = erpnext.get_company_currency(filters.get("company"))
for key, qty in iteritems(pledge_values):
if qty:
row = {}
- current_value = flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0))
- valid_upto = loan_security_details.get(key[1], {}).get('valid_upto')
+ current_value = flt(qty * loan_security_details.get(key[1], {}).get("latest_price", 0))
+ valid_upto = loan_security_details.get(key[1], {}).get("valid_upto")
row.update(loan_security_details.get(key[1]))
- row.update({
- 'applicant_type': applicant_type_map.get(key[0]),
- 'applicant_name': key[0],
- 'total_qty': qty,
- 'current_value': current_value,
- 'price_valid_upto': valid_upto,
- 'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2) if total_value_map.get(key[0]) \
+ row.update(
+ {
+ "applicant_type": applicant_type_map.get(key[0]),
+ "applicant_name": key[0],
+ "total_qty": qty,
+ "current_value": current_value,
+ "price_valid_upto": valid_upto,
+ "portfolio_percent": flt(current_value * 100 / total_value_map.get(key[0]), 2)
+ if total_value_map.get(key[0])
else 0.0,
- 'currency': currency
- })
+ "currency": currency,
+ }
+ )
data.append(row)
return data
+
def get_loan_security_details():
security_detail_map = {}
loan_security_price_map = {}
lsp_validity_map = {}
- loan_security_prices = frappe.db.sql("""
+ loan_security_prices = frappe.db.sql(
+ """
SELECT loan_security, loan_security_price, valid_upto
FROM `tabLoan Security Price` t1
WHERE valid_from >= (SELECT MAX(valid_from) FROM `tabLoan Security Price` t2
WHERE t1.loan_security = t2.loan_security)
- """, as_dict=1)
+ """,
+ as_dict=1,
+ )
for security in loan_security_prices:
loan_security_price_map.setdefault(security.loan_security, security.loan_security_price)
lsp_validity_map.setdefault(security.loan_security, security.valid_upto)
- loan_security_details = frappe.get_all('Loan Security', fields=['name as loan_security',
- 'loan_security_code', 'loan_security_name', 'haircut', 'loan_security_type',
- 'disabled'])
+ loan_security_details = frappe.get_all(
+ "Loan Security",
+ fields=[
+ "name as loan_security",
+ "loan_security_code",
+ "loan_security_name",
+ "haircut",
+ "loan_security_type",
+ "disabled",
+ ],
+ )
for security in loan_security_details:
- security.update({
- 'latest_price': flt(loan_security_price_map.get(security.loan_security)),
- 'valid_upto': lsp_validity_map.get(security.loan_security)
- })
+ security.update(
+ {
+ "latest_price": flt(loan_security_price_map.get(security.loan_security)),
+ "valid_upto": lsp_validity_map.get(security.loan_security),
+ }
+ )
security_detail_map.setdefault(security.loan_security, security)
return security_detail_map
+
def get_applicant_wise_total_loan_security_qty(filters, loan_security_details):
current_pledges = {}
total_value_map = {}
@@ -103,39 +185,53 @@ def get_applicant_wise_total_loan_security_qty(filters, loan_security_details):
applicant_wise_unpledges = {}
conditions = ""
- if filters.get('company'):
+ if filters.get("company"):
conditions = "AND company = %(company)s"
- unpledges = frappe.db.sql("""
+ unpledges = frappe.db.sql(
+ """
SELECT up.applicant, u.loan_security, sum(u.qty) as qty
FROM `tabLoan Security Unpledge` up, `tabUnpledge` u
WHERE u.parent = up.name
AND up.status = 'Approved'
{conditions}
GROUP BY up.applicant, u.loan_security
- """.format(conditions=conditions), filters, as_dict=1)
+ """.format(
+ conditions=conditions
+ ),
+ filters,
+ as_dict=1,
+ )
for unpledge in unpledges:
applicant_wise_unpledges.setdefault((unpledge.applicant, unpledge.loan_security), unpledge.qty)
- pledges = frappe.db.sql("""
+ pledges = frappe.db.sql(
+ """
SELECT lp.applicant_type, lp.applicant, p.loan_security, sum(p.qty) as qty
FROM `tabLoan Security Pledge` lp, `tabPledge`p
WHERE p.parent = lp.name
AND lp.status = 'Pledged'
{conditions}
GROUP BY lp.applicant, p.loan_security
- """.format(conditions=conditions), filters, as_dict=1)
+ """.format(
+ conditions=conditions
+ ),
+ filters,
+ as_dict=1,
+ )
for security in pledges:
current_pledges.setdefault((security.applicant, security.loan_security), security.qty)
total_value_map.setdefault(security.applicant, 0.0)
applicant_type_map.setdefault(security.applicant, security.applicant_type)
- current_pledges[(security.applicant, security.loan_security)] -= \
- applicant_wise_unpledges.get((security.applicant, security.loan_security), 0.0)
+ current_pledges[(security.applicant, security.loan_security)] -= applicant_wise_unpledges.get(
+ (security.applicant, security.loan_security), 0.0
+ )
- total_value_map[security.applicant] += current_pledges.get((security.applicant, security.loan_security)) \
- * loan_security_details.get(security.loan_security, {}).get('latest_price', 0)
+ total_value_map[security.applicant] += current_pledges.get(
+ (security.applicant, security.loan_security)
+ ) * loan_security_details.get(security.loan_security, {}).get("latest_price", 0)
return current_pledges, total_value_map, applicant_type_map
diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
index 7c512679567..9186ce61743 100644
--- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
+++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
@@ -17,41 +17,148 @@ def execute(filters=None):
data = get_active_loan_details(filters)
return columns, data
+
def get_columns(filters):
columns = [
{"label": _("Loan"), "fieldname": "loan", "fieldtype": "Link", "options": "Loan", "width": 160},
{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 160},
- {"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100},
- {"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150},
- {"label": _("Loan Type"), "fieldname": "loan_type", "fieldtype": "Link", "options": "Loan Type", "width": 100},
- {"label": _("Sanctioned Amount"), "fieldname": "sanctioned_amount", "fieldtype": "Currency", "options": "currency", "width": 120},
- {"label": _("Disbursed Amount"), "fieldname": "disbursed_amount", "fieldtype": "Currency", "options": "currency", "width": 120},
- {"label": _("Penalty Amount"), "fieldname": "penalty", "fieldtype": "Currency", "options": "currency", "width": 120},
- {"label": _("Accrued Interest"), "fieldname": "accrued_interest", "fieldtype": "Currency", "options": "currency", "width": 120},
- {"label": _("Total Repayment"), "fieldname": "total_repayment", "fieldtype": "Currency", "options": "currency", "width": 120},
- {"label": _("Principal Outstanding"), "fieldname": "principal_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120},
- {"label": _("Interest Outstanding"), "fieldname": "interest_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120},
- {"label": _("Total Outstanding"), "fieldname": "total_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120},
- {"label": _("Undue Booked Interest"), "fieldname": "undue_interest", "fieldtype": "Currency", "options": "currency", "width": 120},
- {"label": _("Interest %"), "fieldname": "rate_of_interest", "fieldtype": "Percent", "width": 100},
- {"label": _("Penalty Interest %"), "fieldname": "penalty_interest", "fieldtype": "Percent", "width": 100},
- {"label": _("Loan To Value Ratio"), "fieldname": "loan_to_value", "fieldtype": "Percent", "width": 100},
- {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
+ {
+ "label": _("Applicant Type"),
+ "fieldname": "applicant_type",
+ "options": "DocType",
+ "width": 100,
+ },
+ {
+ "label": _("Applicant Name"),
+ "fieldname": "applicant_name",
+ "fieldtype": "Dynamic Link",
+ "options": "applicant_type",
+ "width": 150,
+ },
+ {
+ "label": _("Loan Type"),
+ "fieldname": "loan_type",
+ "fieldtype": "Link",
+ "options": "Loan Type",
+ "width": 100,
+ },
+ {
+ "label": _("Sanctioned Amount"),
+ "fieldname": "sanctioned_amount",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Disbursed Amount"),
+ "fieldname": "disbursed_amount",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Penalty Amount"),
+ "fieldname": "penalty",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Accrued Interest"),
+ "fieldname": "accrued_interest",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Total Repayment"),
+ "fieldname": "total_repayment",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Principal Outstanding"),
+ "fieldname": "principal_outstanding",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Interest Outstanding"),
+ "fieldname": "interest_outstanding",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Total Outstanding"),
+ "fieldname": "total_outstanding",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Undue Booked Interest"),
+ "fieldname": "undue_interest",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120,
+ },
+ {
+ "label": _("Interest %"),
+ "fieldname": "rate_of_interest",
+ "fieldtype": "Percent",
+ "width": 100,
+ },
+ {
+ "label": _("Penalty Interest %"),
+ "fieldname": "penalty_interest",
+ "fieldtype": "Percent",
+ "width": 100,
+ },
+ {
+ "label": _("Loan To Value Ratio"),
+ "fieldname": "loan_to_value",
+ "fieldtype": "Percent",
+ "width": 100,
+ },
+ {
+ "label": _("Currency"),
+ "fieldname": "currency",
+ "fieldtype": "Currency",
+ "options": "Currency",
+ "hidden": 1,
+ "width": 100,
+ },
]
return columns
+
def get_active_loan_details(filters):
filter_obj = {"status": ("!=", "Closed")}
- if filters.get('company'):
- filter_obj.update({'company': filters.get('company')})
+ if filters.get("company"):
+ filter_obj.update({"company": filters.get("company")})
- loan_details = frappe.get_all("Loan",
- fields=["name as loan", "applicant_type", "applicant as applicant_name", "loan_type",
- "disbursed_amount", "rate_of_interest", "total_payment", "total_principal_paid",
- "total_interest_payable", "written_off_amount", "status"],
- filters=filter_obj)
+ loan_details = frappe.get_all(
+ "Loan",
+ fields=[
+ "name as loan",
+ "applicant_type",
+ "applicant as applicant_name",
+ "loan_type",
+ "disbursed_amount",
+ "rate_of_interest",
+ "total_payment",
+ "total_principal_paid",
+ "total_interest_payable",
+ "written_off_amount",
+ "status",
+ ],
+ filters=filter_obj,
+ )
loan_list = [d.loan for d in loan_details]
@@ -62,70 +169,105 @@ def get_active_loan_details(filters):
penal_interest_rate_map = get_penal_interest_rate_map()
payments = get_payments(loan_list)
accrual_map = get_interest_accruals(loan_list)
- currency = erpnext.get_company_currency(filters.get('company'))
+ currency = erpnext.get_company_currency(filters.get("company"))
for loan in loan_details:
- total_payment = loan.total_payment if loan.status == 'Disbursed' else loan.disbursed_amount
+ total_payment = loan.total_payment if loan.status == "Disbursed" else loan.disbursed_amount
- loan.update({
- "sanctioned_amount": flt(sanctioned_amount_map.get(loan.applicant_name)),
- "principal_outstanding": flt(total_payment) - flt(loan.total_principal_paid) \
- - flt(loan.total_interest_payable) - flt(loan.written_off_amount),
- "total_repayment": flt(payments.get(loan.loan)),
- "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")),
- "interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")),
- "penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")),
- "penalty_interest": penal_interest_rate_map.get(loan.loan_type),
- "undue_interest": flt(accrual_map.get(loan.loan, {}).get("undue_interest")),
- "loan_to_value": 0.0,
- "currency": currency
- })
+ loan.update(
+ {
+ "sanctioned_amount": flt(sanctioned_amount_map.get(loan.applicant_name)),
+ "principal_outstanding": flt(total_payment)
+ - flt(loan.total_principal_paid)
+ - flt(loan.total_interest_payable)
+ - flt(loan.written_off_amount),
+ "total_repayment": flt(payments.get(loan.loan)),
+ "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")),
+ "interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")),
+ "penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")),
+ "penalty_interest": penal_interest_rate_map.get(loan.loan_type),
+ "undue_interest": flt(accrual_map.get(loan.loan, {}).get("undue_interest")),
+ "loan_to_value": 0.0,
+ "currency": currency,
+ }
+ )
- loan['total_outstanding'] = loan['principal_outstanding'] + loan['interest_outstanding'] \
- + loan['penalty']
+ loan["total_outstanding"] = (
+ loan["principal_outstanding"] + loan["interest_outstanding"] + loan["penalty"]
+ )
if loan_wise_security_value.get(loan.loan):
- loan['loan_to_value'] = flt((loan['principal_outstanding'] * 100) / loan_wise_security_value.get(loan.loan))
+ loan["loan_to_value"] = flt(
+ (loan["principal_outstanding"] * 100) / loan_wise_security_value.get(loan.loan)
+ )
return loan_details
+
def get_sanctioned_amount_map():
- return frappe._dict(frappe.get_all("Sanctioned Loan Amount", fields=["applicant", "sanctioned_amount_limit"],
- as_list=1))
+ return frappe._dict(
+ frappe.get_all(
+ "Sanctioned Loan Amount", fields=["applicant", "sanctioned_amount_limit"], as_list=1
+ )
+ )
+
def get_payments(loans):
- return frappe._dict(frappe.get_all("Loan Repayment", fields=["against_loan", "sum(amount_paid)"],
- filters={"against_loan": ("in", loans)}, group_by="against_loan", as_list=1))
+ return frappe._dict(
+ frappe.get_all(
+ "Loan Repayment",
+ fields=["against_loan", "sum(amount_paid)"],
+ filters={"against_loan": ("in", loans)},
+ group_by="against_loan",
+ as_list=1,
+ )
+ )
+
def get_interest_accruals(loans):
accrual_map = {}
- interest_accruals = frappe.get_all("Loan Interest Accrual",
- fields=["loan", "interest_amount", "posting_date", "penalty_amount",
- "paid_interest_amount", "accrual_type"], filters={"loan": ("in", loans)}, order_by="posting_date desc")
+ interest_accruals = frappe.get_all(
+ "Loan Interest Accrual",
+ fields=[
+ "loan",
+ "interest_amount",
+ "posting_date",
+ "penalty_amount",
+ "paid_interest_amount",
+ "accrual_type",
+ ],
+ filters={"loan": ("in", loans)},
+ order_by="posting_date desc",
+ )
for entry in interest_accruals:
- accrual_map.setdefault(entry.loan, {
- "accrued_interest": 0.0,
- "undue_interest": 0.0,
- "interest_outstanding": 0.0,
- "last_accrual_date": '',
- "due_date": ''
- })
+ accrual_map.setdefault(
+ entry.loan,
+ {
+ "accrued_interest": 0.0,
+ "undue_interest": 0.0,
+ "interest_outstanding": 0.0,
+ "last_accrual_date": "",
+ "due_date": "",
+ },
+ )
- if entry.accrual_type == 'Regular':
- if not accrual_map[entry.loan]['due_date']:
- accrual_map[entry.loan]['due_date'] = add_days(entry.posting_date, 1)
- if not accrual_map[entry.loan]['last_accrual_date']:
- accrual_map[entry.loan]['last_accrual_date'] = entry.posting_date
+ if entry.accrual_type == "Regular":
+ if not accrual_map[entry.loan]["due_date"]:
+ accrual_map[entry.loan]["due_date"] = add_days(entry.posting_date, 1)
+ if not accrual_map[entry.loan]["last_accrual_date"]:
+ accrual_map[entry.loan]["last_accrual_date"] = entry.posting_date
- due_date = accrual_map[entry.loan]['due_date']
- last_accrual_date = accrual_map[entry.loan]['last_accrual_date']
+ due_date = accrual_map[entry.loan]["due_date"]
+ last_accrual_date = accrual_map[entry.loan]["last_accrual_date"]
if due_date and getdate(entry.posting_date) < getdate(due_date):
- accrual_map[entry.loan]["interest_outstanding"] += entry.interest_amount - entry.paid_interest_amount
+ accrual_map[entry.loan]["interest_outstanding"] += (
+ entry.interest_amount - entry.paid_interest_amount
+ )
else:
- accrual_map[entry.loan]['undue_interest'] += entry.interest_amount - entry.paid_interest_amount
+ accrual_map[entry.loan]["undue_interest"] += entry.interest_amount - entry.paid_interest_amount
accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount
@@ -134,8 +276,12 @@ def get_interest_accruals(loans):
return accrual_map
+
def get_penal_interest_rate_map():
- return frappe._dict(frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1))
+ return frappe._dict(
+ frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1)
+ )
+
def get_loan_wise_pledges(filters):
loan_wise_unpledges = {}
@@ -143,37 +289,51 @@ def get_loan_wise_pledges(filters):
conditions = ""
- if filters.get('company'):
+ if filters.get("company"):
conditions = "AND company = %(company)s"
- unpledges = frappe.db.sql("""
+ unpledges = frappe.db.sql(
+ """
SELECT up.loan, u.loan_security, sum(u.qty) as qty
FROM `tabLoan Security Unpledge` up, `tabUnpledge` u
WHERE u.parent = up.name
AND up.status = 'Approved'
{conditions}
GROUP BY up.loan, u.loan_security
- """.format(conditions=conditions), filters, as_dict=1)
+ """.format(
+ conditions=conditions
+ ),
+ filters,
+ as_dict=1,
+ )
for unpledge in unpledges:
loan_wise_unpledges.setdefault((unpledge.loan, unpledge.loan_security), unpledge.qty)
- pledges = frappe.db.sql("""
+ pledges = frappe.db.sql(
+ """
SELECT lp.loan, p.loan_security, sum(p.qty) as qty
FROM `tabLoan Security Pledge` lp, `tabPledge`p
WHERE p.parent = lp.name
AND lp.status = 'Pledged'
{conditions}
GROUP BY lp.loan, p.loan_security
- """.format(conditions=conditions), filters, as_dict=1)
+ """.format(
+ conditions=conditions
+ ),
+ filters,
+ as_dict=1,
+ )
for security in pledges:
current_pledges.setdefault((security.loan, security.loan_security), security.qty)
- current_pledges[(security.loan, security.loan_security)] -= \
- loan_wise_unpledges.get((security.loan, security.loan_security), 0.0)
+ current_pledges[(security.loan, security.loan_security)] -= loan_wise_unpledges.get(
+ (security.loan, security.loan_security), 0.0
+ )
return current_pledges
+
def get_loan_wise_security_value(filters, current_pledges):
loan_security_details = get_loan_security_details()
loan_wise_security_value = {}
@@ -181,7 +341,8 @@ def get_loan_wise_security_value(filters, current_pledges):
for key in current_pledges:
qty = current_pledges.get(key)
loan_wise_security_value.setdefault(key[0], 0.0)
- loan_wise_security_value[key[0]] += \
- flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0))
+ loan_wise_security_value[key[0]] += flt(
+ qty * loan_security_details.get(key[1], {}).get("latest_price", 0)
+ )
return loan_wise_security_value
diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py
index 68fd3d8e8b5..253b994ae04 100644
--- a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py
+++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py
@@ -11,101 +11,96 @@ def execute(filters=None):
data = get_data(filters)
return columns, data
+
def get_columns():
return [
- {
- "label": _("Posting Date"),
- "fieldtype": "Date",
- "fieldname": "posting_date",
- "width": 100
- },
- {
- "label": _("Loan Repayment"),
- "fieldtype": "Link",
- "fieldname": "loan_repayment",
- "options": "Loan Repayment",
- "width": 100
- },
- {
- "label": _("Against Loan"),
- "fieldtype": "Link",
- "fieldname": "against_loan",
- "options": "Loan",
- "width": 200
- },
- {
- "label": _("Applicant"),
- "fieldtype": "Data",
- "fieldname": "applicant",
- "width": 150
- },
- {
- "label": _("Payment Type"),
- "fieldtype": "Data",
- "fieldname": "payment_type",
- "width": 150
- },
- {
- "label": _("Principal Amount"),
- "fieldtype": "Currency",
- "fieldname": "principal_amount",
- "options": "currency",
- "width": 100
- },
- {
- "label": _("Interest Amount"),
- "fieldtype": "Currency",
- "fieldname": "interest",
- "options": "currency",
- "width": 100
- },
- {
- "label": _("Penalty Amount"),
- "fieldtype": "Currency",
- "fieldname": "penalty",
- "options": "currency",
- "width": 100
- },
- {
- "label": _("Payable Amount"),
- "fieldtype": "Currency",
- "fieldname": "payable_amount",
- "options": "currency",
- "width": 100
- },
- {
- "label": _("Paid Amount"),
- "fieldtype": "Currency",
- "fieldname": "paid_amount",
- "options": "currency",
- "width": 100
- },
- {
- "label": _("Currency"),
- "fieldtype": "Link",
- "fieldname": "currency",
- "options": "Currency",
- "width": 100
- }
- ]
+ {"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date", "width": 100},
+ {
+ "label": _("Loan Repayment"),
+ "fieldtype": "Link",
+ "fieldname": "loan_repayment",
+ "options": "Loan Repayment",
+ "width": 100,
+ },
+ {
+ "label": _("Against Loan"),
+ "fieldtype": "Link",
+ "fieldname": "against_loan",
+ "options": "Loan",
+ "width": 200,
+ },
+ {"label": _("Applicant"), "fieldtype": "Data", "fieldname": "applicant", "width": 150},
+ {"label": _("Payment Type"), "fieldtype": "Data", "fieldname": "payment_type", "width": 150},
+ {
+ "label": _("Principal Amount"),
+ "fieldtype": "Currency",
+ "fieldname": "principal_amount",
+ "options": "currency",
+ "width": 100,
+ },
+ {
+ "label": _("Interest Amount"),
+ "fieldtype": "Currency",
+ "fieldname": "interest",
+ "options": "currency",
+ "width": 100,
+ },
+ {
+ "label": _("Penalty Amount"),
+ "fieldtype": "Currency",
+ "fieldname": "penalty",
+ "options": "currency",
+ "width": 100,
+ },
+ {
+ "label": _("Payable Amount"),
+ "fieldtype": "Currency",
+ "fieldname": "payable_amount",
+ "options": "currency",
+ "width": 100,
+ },
+ {
+ "label": _("Paid Amount"),
+ "fieldtype": "Currency",
+ "fieldname": "paid_amount",
+ "options": "currency",
+ "width": 100,
+ },
+ {
+ "label": _("Currency"),
+ "fieldtype": "Link",
+ "fieldname": "currency",
+ "options": "Currency",
+ "width": 100,
+ },
+ ]
+
def get_data(filters):
data = []
query_filters = {
"docstatus": 1,
- "company": filters.get('company'),
+ "company": filters.get("company"),
}
- if filters.get('applicant'):
- query_filters.update({
- "applicant": filters.get('applicant')
- })
+ if filters.get("applicant"):
+ query_filters.update({"applicant": filters.get("applicant")})
- loan_repayments = frappe.get_all("Loan Repayment",
- filters = query_filters,
- fields=["posting_date", "applicant", "name", "against_loan", "payable_amount",
- "pending_principal_amount", "interest_payable", "penalty_amount", "amount_paid"]
+ loan_repayments = frappe.get_all(
+ "Loan Repayment",
+ filters=query_filters,
+ fields=[
+ "posting_date",
+ "applicant",
+ "name",
+ "against_loan",
+ "payable_amount",
+ "pending_principal_amount",
+ "interest_payable",
+ "penalty_amount",
+ "amount_paid",
+ ],
)
default_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
@@ -122,7 +117,7 @@ def get_data(filters):
"penalty": repayment.penalty_amount,
"payable_amount": repayment.payable_amount,
"paid_amount": repayment.amount_paid,
- "currency": default_currency
+ "currency": default_currency,
}
data.append(row)
diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py
index e3d99952902..4a459413876 100644
--- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py
+++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py
@@ -18,46 +18,110 @@ def execute(filters=None):
data = get_data(filters)
return columns, data
+
def get_columns(filters):
columns = [
- {"label": _("Loan Security"), "fieldname": "loan_security", "fieldtype": "Link", "options": "Loan Security", "width": 160},
- {"label": _("Loan Security Code"), "fieldname": "loan_security_code", "fieldtype": "Data", "width": 100},
- {"label": _("Loan Security Name"), "fieldname": "loan_security_name", "fieldtype": "Data", "width": 150},
+ {
+ "label": _("Loan Security"),
+ "fieldname": "loan_security",
+ "fieldtype": "Link",
+ "options": "Loan Security",
+ "width": 160,
+ },
+ {
+ "label": _("Loan Security Code"),
+ "fieldname": "loan_security_code",
+ "fieldtype": "Data",
+ "width": 100,
+ },
+ {
+ "label": _("Loan Security Name"),
+ "fieldname": "loan_security_name",
+ "fieldtype": "Data",
+ "width": 150,
+ },
{"label": _("Haircut"), "fieldname": "haircut", "fieldtype": "Percent", "width": 100},
- {"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120},
+ {
+ "label": _("Loan Security Type"),
+ "fieldname": "loan_security_type",
+ "fieldtype": "Link",
+ "options": "Loan Security Type",
+ "width": 120,
+ },
{"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80},
{"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100},
- {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100},
- {"label": _("Price Valid Upto"), "fieldname": "price_valid_upto", "fieldtype": "Datetime", "width": 100},
- {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100},
- {"label": _("% Of Total Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100},
- {"label": _("Pledged Applicant Count"), "fieldname": "pledged_applicant_count", "fieldtype": "Percentage", "width": 100},
- {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
+ {
+ "label": _("Latest Price"),
+ "fieldname": "latest_price",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 100,
+ },
+ {
+ "label": _("Price Valid Upto"),
+ "fieldname": "price_valid_upto",
+ "fieldtype": "Datetime",
+ "width": 100,
+ },
+ {
+ "label": _("Current Value"),
+ "fieldname": "current_value",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 100,
+ },
+ {
+ "label": _("% Of Total Portfolio"),
+ "fieldname": "portfolio_percent",
+ "fieldtype": "Percentage",
+ "width": 100,
+ },
+ {
+ "label": _("Pledged Applicant Count"),
+ "fieldname": "pledged_applicant_count",
+ "fieldtype": "Percentage",
+ "width": 100,
+ },
+ {
+ "label": _("Currency"),
+ "fieldname": "currency",
+ "fieldtype": "Currency",
+ "options": "Currency",
+ "hidden": 1,
+ "width": 100,
+ },
]
return columns
+
def get_data(filters):
data = []
loan_security_details = get_loan_security_details()
- current_pledges, total_portfolio_value = get_company_wise_loan_security_details(filters, loan_security_details)
- currency = erpnext.get_company_currency(filters.get('company'))
+ current_pledges, total_portfolio_value = get_company_wise_loan_security_details(
+ filters, loan_security_details
+ )
+ currency = erpnext.get_company_currency(filters.get("company"))
for security, value in iteritems(current_pledges):
- if value.get('qty'):
+ if value.get("qty"):
row = {}
- current_value = flt(value.get('qty', 0) * loan_security_details.get(security, {}).get('latest_price', 0))
- valid_upto = loan_security_details.get(security, {}).get('valid_upto')
+ current_value = flt(
+ value.get("qty", 0) * loan_security_details.get(security, {}).get("latest_price", 0)
+ )
+ valid_upto = loan_security_details.get(security, {}).get("valid_upto")
row.update(loan_security_details.get(security))
- row.update({
- 'total_qty': value.get('qty'),
- 'current_value': current_value,
- 'price_valid_upto': valid_upto,
- 'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2),
- 'pledged_applicant_count': value.get('applicant_count'),
- 'currency': currency
- })
+ row.update(
+ {
+ "total_qty": value.get("qty"),
+ "current_value": current_value,
+ "price_valid_upto": valid_upto,
+ "portfolio_percent": flt(current_value * 100 / total_portfolio_value, 2),
+ "pledged_applicant_count": value.get("applicant_count"),
+ "currency": currency,
+ }
+ )
data.append(row)
@@ -65,21 +129,19 @@ def get_data(filters):
def get_company_wise_loan_security_details(filters, loan_security_details):
- pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters,
- loan_security_details)
+ pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(
+ filters, loan_security_details
+ )
total_portfolio_value = 0
security_wise_map = {}
for key, qty in iteritems(pledge_values):
- security_wise_map.setdefault(key[1], {
- 'qty': 0.0,
- 'applicant_count': 0.0
- })
+ security_wise_map.setdefault(key[1], {"qty": 0.0, "applicant_count": 0.0})
- security_wise_map[key[1]]['qty'] += qty
+ security_wise_map[key[1]]["qty"] += qty
if qty:
- security_wise_map[key[1]]['applicant_count'] += 1
+ security_wise_map[key[1]]["applicant_count"] += 1
- total_portfolio_value += flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0))
+ total_portfolio_value += flt(qty * loan_security_details.get(key[1], {}).get("latest_price", 0))
return security_wise_map, total_portfolio_value
diff --git a/erpnext/loan_management/report/loan_security_status/loan_security_status.py b/erpnext/loan_management/report/loan_security_status/loan_security_status.py
index b7e716880e9..9a5a18001ea 100644
--- a/erpnext/loan_management/report/loan_security_status/loan_security_status.py
+++ b/erpnext/loan_management/report/loan_security_status/loan_security_status.py
@@ -11,66 +11,41 @@ def execute(filters=None):
data = get_data(filters)
return columns, data
+
def get_columns(filters):
- columns= [
+ columns = [
{
"label": _("Loan Security Pledge"),
"fieldtype": "Link",
"fieldname": "loan_security_pledge",
"options": "Loan Security Pledge",
- "width": 200
- },
- {
- "label": _("Loan"),
- "fieldtype": "Link",
- "fieldname": "loan",
- "options": "Loan",
- "width": 200
- },
- {
- "label": _("Applicant"),
- "fieldtype": "Data",
- "fieldname": "applicant",
- "width": 200
- },
- {
- "label": _("Status"),
- "fieldtype": "Data",
- "fieldname": "status",
- "width": 100
- },
- {
- "label": _("Pledge Time"),
- "fieldtype": "Data",
- "fieldname": "pledge_time",
- "width": 150
+ "width": 200,
},
+ {"label": _("Loan"), "fieldtype": "Link", "fieldname": "loan", "options": "Loan", "width": 200},
+ {"label": _("Applicant"), "fieldtype": "Data", "fieldname": "applicant", "width": 200},
+ {"label": _("Status"), "fieldtype": "Data", "fieldname": "status", "width": 100},
+ {"label": _("Pledge Time"), "fieldtype": "Data", "fieldname": "pledge_time", "width": 150},
{
"label": _("Loan Security"),
"fieldtype": "Link",
"fieldname": "loan_security",
"options": "Loan Security",
- "width": 150
- },
- {
- "label": _("Quantity"),
- "fieldtype": "Float",
- "fieldname": "qty",
- "width": 100
+ "width": 150,
},
+ {"label": _("Quantity"), "fieldtype": "Float", "fieldname": "qty", "width": 100},
{
"label": _("Loan Security Price"),
"fieldtype": "Currency",
"fieldname": "loan_security_price",
"options": "currency",
- "width": 200
+ "width": 200,
},
{
"label": _("Loan Security Value"),
"fieldtype": "Currency",
"fieldname": "loan_security_value",
"options": "currency",
- "width": 200
+ "width": 200,
},
{
"label": _("Currency"),
@@ -78,18 +53,20 @@ def get_columns(filters):
"fieldname": "currency",
"options": "Currency",
"width": 50,
- "hidden": 1
- }
+ "hidden": 1,
+ },
]
return columns
+
def get_data(filters):
data = []
conditions = get_conditions(filters)
- loan_security_pledges = frappe.db.sql("""
+ loan_security_pledges = frappe.db.sql(
+ """
SELECT
p.name, p.applicant, p.loan, p.status, p.pledge_time,
c.loan_security, c.qty, c.loan_security_price, c.amount
@@ -100,7 +77,12 @@ def get_data(filters):
AND c.parent = p.name
AND p.company = %(company)s
{conditions}
- """.format(conditions = conditions), (filters), as_dict=1) #nosec
+ """.format(
+ conditions=conditions
+ ),
+ (filters),
+ as_dict=1,
+ ) # nosec
default_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
@@ -121,6 +103,7 @@ def get_data(filters):
return data
+
def get_conditions(filters):
conditions = []
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
index 07d928c221f..256f66071f3 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
@@ -16,17 +16,17 @@ class MaintenanceSchedule(TransactionBase):
def generate_schedule(self):
if self.docstatus != 0:
return
- self.set('schedules', [])
+ self.set("schedules", [])
count = 1
- for d in self.get('items'):
+ for d in self.get("items"):
self.validate_maintenance_detail()
s_list = []
s_list = self.create_schedule_list(d.start_date, d.end_date, d.no_of_visits, d.sales_person)
for i in range(d.no_of_visits):
- child = self.append('schedules')
+ child = self.append("schedules")
child.item_code = d.item_code
child.item_name = d.item_name
- child.scheduled_date = s_list[i].strftime('%Y-%m-%d')
+ child.scheduled_date = s_list[i].strftime("%Y-%m-%d")
if d.serial_no:
child.serial_no = d.serial_no
child.idx = count
@@ -37,18 +37,14 @@ class MaintenanceSchedule(TransactionBase):
@frappe.whitelist()
def validate_end_date_visits(self):
- days_in_period = {
- "Weekly": 7,
- "Monthly": 30,
- "Quarterly": 91,
- "Half Yearly": 182,
- "Yearly": 365
- }
+ days_in_period = {"Weekly": 7, "Monthly": 30, "Quarterly": 91, "Half Yearly": 182, "Yearly": 365}
for item in self.items:
if item.periodicity and item.periodicity != "Random" and item.start_date:
if not item.end_date:
if item.no_of_visits:
- item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity])
+ item.end_date = add_days(
+ item.start_date, item.no_of_visits * days_in_period[item.periodicity]
+ )
else:
item.end_date = add_days(item.start_date, days_in_period[item.periodicity])
@@ -61,20 +57,23 @@ class MaintenanceSchedule(TransactionBase):
item.no_of_visits = cint(diff / days_in_period[item.periodicity])
elif item.no_of_visits > no_of_visits:
- item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity])
+ item.end_date = add_days(
+ item.start_date, item.no_of_visits * days_in_period[item.periodicity]
+ )
elif item.no_of_visits < no_of_visits:
- item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity])
-
+ item.end_date = add_days(
+ item.start_date, item.no_of_visits * days_in_period[item.periodicity]
+ )
def on_submit(self):
- if not self.get('schedules'):
+ if not self.get("schedules"):
throw(_("Please click on 'Generate Schedule' to get schedule"))
self.check_serial_no_added()
self.validate_schedule()
email_map = {}
- for d in self.get('items'):
+ for d in self.get("items"):
if d.serial_no:
serial_nos = get_valid_serial_nos(d.serial_no)
self.validate_serial_no(d.item_code, serial_nos, d.start_date)
@@ -90,29 +89,37 @@ class MaintenanceSchedule(TransactionBase):
if no_email_sp:
frappe.msgprint(
- _("Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}").format(
- self.owner, " " + " ".join(no_email_sp)
- )
+ _(
+ "Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}"
+ ).format(self.owner, " " + " ".join(no_email_sp))
)
- scheduled_date = frappe.db.sql("""select scheduled_date from
+ scheduled_date = frappe.db.sql(
+ """select scheduled_date from
`tabMaintenance Schedule Detail` where sales_person=%s and item_code=%s and
- parent=%s""", (d.sales_person, d.item_code, self.name), as_dict=1)
+ parent=%s""",
+ (d.sales_person, d.item_code, self.name),
+ as_dict=1,
+ )
for key in scheduled_date:
- description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer)
- event = frappe.get_doc({
- "doctype": "Event",
- "owner": email_map.get(d.sales_person, self.owner),
- "subject": description,
- "description": description,
- "starts_on": cstr(key["scheduled_date"]) + " 10:00:00",
- "event_type": "Private",
- })
+ description = frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(
+ self.name, d.item_code, self.customer
+ )
+ event = frappe.get_doc(
+ {
+ "doctype": "Event",
+ "owner": email_map.get(d.sales_person, self.owner),
+ "subject": description,
+ "description": description,
+ "starts_on": cstr(key["scheduled_date"]) + " 10:00:00",
+ "event_type": "Private",
+ }
+ )
event.add_participant(self.doctype, self.name)
event.insert(ignore_permissions=1)
- frappe.db.set(self, 'status', 'Submitted')
+ frappe.db.set(self, "status", "Submitted")
def create_schedule_list(self, start_date, end_date, no_of_visit, sales_person):
schedule_list = []
@@ -121,11 +128,12 @@ class MaintenanceSchedule(TransactionBase):
add_by = date_diff / no_of_visit
for visit in range(cint(no_of_visit)):
- if (getdate(start_date_copy) < getdate(end_date)):
+ if getdate(start_date_copy) < getdate(end_date):
start_date_copy = add_days(start_date_copy, add_by)
if len(schedule_list) < no_of_visit:
- schedule_date = self.validate_schedule_date_for_holiday_list(getdate(start_date_copy),
- sales_person)
+ schedule_date = self.validate_schedule_date_for_holiday_list(
+ getdate(start_date_copy), sales_person
+ )
if schedule_date > getdate(end_date):
schedule_date = getdate(end_date)
schedule_list.append(schedule_date)
@@ -139,9 +147,11 @@ class MaintenanceSchedule(TransactionBase):
if employee:
holiday_list = get_holiday_list_for_employee(employee)
else:
- holiday_list = frappe.get_cached_value('Company', self.company, "default_holiday_list")
+ holiday_list = frappe.get_cached_value("Company", self.company, "default_holiday_list")
- holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday` where parent=%s''', holiday_list)
+ holidays = frappe.db.sql_list(
+ """select holiday_date from `tabHoliday` where parent=%s""", holiday_list
+ )
if not validated and holidays:
@@ -157,25 +167,28 @@ class MaintenanceSchedule(TransactionBase):
def validate_dates_with_periodicity(self):
for d in self.get("items"):
- if d.start_date and d.end_date and d.periodicity and d.periodicity!="Random":
+ if d.start_date and d.end_date and d.periodicity and d.periodicity != "Random":
date_diff = (getdate(d.end_date) - getdate(d.start_date)).days + 1
days_in_period = {
"Weekly": 7,
"Monthly": 30,
"Quarterly": 90,
"Half Yearly": 180,
- "Yearly": 365
+ "Yearly": 365,
}
if date_diff < days_in_period[d.periodicity]:
- throw(_("Row {0}: To set {1} periodicity, difference between from and to date must be greater than or equal to {2}")
- .format(d.idx, d.periodicity, days_in_period[d.periodicity]))
+ throw(
+ _(
+ "Row {0}: To set {1} periodicity, difference between from and to date must be greater than or equal to {2}"
+ ).format(d.idx, d.periodicity, days_in_period[d.periodicity])
+ )
def validate_maintenance_detail(self):
- if not self.get('items'):
+ if not self.get("items"):
throw(_("Please enter Maintaince Details first"))
- for d in self.get('items'):
+ for d in self.get("items"):
if not d.item_code:
throw(_("Please select item code"))
elif not d.start_date or not d.end_date:
@@ -189,11 +202,14 @@ class MaintenanceSchedule(TransactionBase):
throw(_("Start date should be less than end date for Item {0}").format(d.item_code))
def validate_sales_order(self):
- for d in self.get('items'):
+ for d in self.get("items"):
if d.sales_order:
- chk = frappe.db.sql("""select ms.name from `tabMaintenance Schedule` ms,
+ chk = frappe.db.sql(
+ """select ms.name from `tabMaintenance Schedule` ms,
`tabMaintenance Schedule Item` msi where msi.parent=ms.name and
- msi.sales_order=%s and ms.docstatus=1""", d.sales_order)
+ msi.sales_order=%s and ms.docstatus=1""",
+ d.sales_order,
+ )
if chk:
throw(_("Maintenance Schedule {0} exists against {1}").format(chk[0][0], d.sales_order))
@@ -209,7 +225,7 @@ class MaintenanceSchedule(TransactionBase):
self.generate_schedule()
def on_update(self):
- frappe.db.set(self, 'status', 'Draft')
+ frappe.db.set(self, "status", "Draft")
def update_amc_date(self, serial_nos, amc_expiry_date=None):
for serial_no in serial_nos:
@@ -219,65 +235,96 @@ class MaintenanceSchedule(TransactionBase):
def validate_serial_no(self, item_code, serial_nos, amc_start_date):
for serial_no in serial_nos:
- sr_details = frappe.db.get_value("Serial No", serial_no,
- ["warranty_expiry_date", "amc_expiry_date", "warehouse", "delivery_date", "item_code"], as_dict=1)
+ sr_details = frappe.db.get_value(
+ "Serial No",
+ serial_no,
+ ["warranty_expiry_date", "amc_expiry_date", "warehouse", "delivery_date", "item_code"],
+ as_dict=1,
+ )
if not sr_details:
frappe.throw(_("Serial No {0} not found").format(serial_no))
if sr_details.get("item_code") != item_code:
- frappe.throw(_("Serial No {0} does not belong to Item {1}")
- .format(frappe.bold(serial_no), frappe.bold(item_code)), title="Invalid")
+ frappe.throw(
+ _("Serial No {0} does not belong to Item {1}").format(
+ frappe.bold(serial_no), frappe.bold(item_code)
+ ),
+ title="Invalid",
+ )
- if sr_details.warranty_expiry_date \
- and getdate(sr_details.warranty_expiry_date) >= getdate(amc_start_date):
- throw(_("Serial No {0} is under warranty upto {1}")
- .format(serial_no, sr_details.warranty_expiry_date))
+ if sr_details.warranty_expiry_date and getdate(sr_details.warranty_expiry_date) >= getdate(
+ amc_start_date
+ ):
+ throw(
+ _("Serial No {0} is under warranty upto {1}").format(
+ serial_no, sr_details.warranty_expiry_date
+ )
+ )
- if sr_details.amc_expiry_date and getdate(sr_details.amc_expiry_date) >= getdate(amc_start_date):
- throw(_("Serial No {0} is under maintenance contract upto {1}")
- .format(serial_no, sr_details.amc_expiry_date))
+ if sr_details.amc_expiry_date and getdate(sr_details.amc_expiry_date) >= getdate(
+ amc_start_date
+ ):
+ throw(
+ _("Serial No {0} is under maintenance contract upto {1}").format(
+ serial_no, sr_details.amc_expiry_date
+ )
+ )
- if not sr_details.warehouse and sr_details.delivery_date and \
- getdate(sr_details.delivery_date) >= getdate(amc_start_date):
- throw(_("Maintenance start date can not be before delivery date for Serial No {0}")
- .format(serial_no))
+ if (
+ not sr_details.warehouse
+ and sr_details.delivery_date
+ and getdate(sr_details.delivery_date) >= getdate(amc_start_date)
+ ):
+ throw(
+ _("Maintenance start date can not be before delivery date for Serial No {0}").format(
+ serial_no
+ )
+ )
def validate_schedule(self):
- item_lst1 =[]
- item_lst2 =[]
- for d in self.get('items'):
+ item_lst1 = []
+ item_lst2 = []
+ for d in self.get("items"):
if d.item_code not in item_lst1:
item_lst1.append(d.item_code)
- for m in self.get('schedules'):
+ for m in self.get("schedules"):
if m.item_code not in item_lst2:
item_lst2.append(m.item_code)
if len(item_lst1) != len(item_lst2):
- throw(_("Maintenance Schedule is not generated for all the items. Please click on 'Generate Schedule'"))
+ throw(
+ _(
+ "Maintenance Schedule is not generated for all the items. Please click on 'Generate Schedule'"
+ )
+ )
else:
for x in item_lst1:
if x not in item_lst2:
throw(_("Please click on 'Generate Schedule'"))
def check_serial_no_added(self):
- serial_present =[]
- for d in self.get('items'):
+ serial_present = []
+ for d in self.get("items"):
if d.serial_no:
serial_present.append(d.item_code)
- for m in self.get('schedules'):
+ for m in self.get("schedules"):
if serial_present:
if m.item_code in serial_present and not m.serial_no:
- throw(_("Please click on 'Generate Schedule' to fetch Serial No added for Item {0}").format(m.item_code))
+ throw(
+ _("Please click on 'Generate Schedule' to fetch Serial No added for Item {0}").format(
+ m.item_code
+ )
+ )
def on_cancel(self):
- for d in self.get('items'):
+ for d in self.get("items"):
if d.serial_no:
serial_nos = get_valid_serial_nos(d.serial_no)
self.update_amc_date(serial_nos)
- frappe.db.set(self, 'status', 'Cancelled')
+ frappe.db.set(self, "status", "Cancelled")
delete_events(self.doctype, self.name)
def on_trash(self):
@@ -301,23 +348,26 @@ class MaintenanceSchedule(TransactionBase):
return items
elif data_type == "id":
for schedule in self.schedules:
- if schedule.item_name == item_name and s_date == formatdate(schedule.scheduled_date, "dd-mm-yyyy"):
+ if schedule.item_name == item_name and s_date == formatdate(
+ schedule.scheduled_date, "dd-mm-yyyy"
+ ):
return schedule.name
+
@frappe.whitelist()
def get_serial_nos_from_schedule(item_code, schedule=None):
serial_nos = []
if schedule:
- serial_nos = frappe.db.get_value('Maintenance Schedule Item', {
- 'parent': schedule,
- 'item_code': item_code
- }, 'serial_no')
+ serial_nos = frappe.db.get_value(
+ "Maintenance Schedule Item", {"parent": schedule, "item_code": item_code}, "serial_no"
+ )
if serial_nos:
serial_nos = get_serial_nos(serial_nos)
return serial_nos
+
@frappe.whitelist()
def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None):
from frappe.model.mapper import get_mapped_doc
@@ -331,27 +381,26 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
if len(serial_nos) == 1:
target.serial_no = serial_nos[0]
else:
- target.serial_no = ''
+ target.serial_no = ""
- doclist = get_mapped_doc("Maintenance Schedule", source_name, {
- "Maintenance Schedule": {
- "doctype": "Maintenance Visit",
- "field_map": {
- "name": "maintenance_schedule"
+ doclist = get_mapped_doc(
+ "Maintenance Schedule",
+ source_name,
+ {
+ "Maintenance Schedule": {
+ "doctype": "Maintenance Visit",
+ "field_map": {"name": "maintenance_schedule"},
+ "validation": {"docstatus": ["=", 1]},
+ "postprocess": update_status_and_detail,
},
- "validation": {
- "docstatus": ["=", 1]
+ "Maintenance Schedule Item": {
+ "doctype": "Maintenance Visit Purpose",
+ "condition": lambda doc: doc.item_name == item_name,
+ "field_map": {"sales_person": "service_person"},
+ "postprocess": update_serial,
},
- "postprocess": update_status_and_detail
},
- "Maintenance Schedule Item": {
- "doctype": "Maintenance Visit Purpose",
- "condition": lambda doc: doc.item_name == item_name,
- "field_map": {
- "sales_person": "service_person"
- },
- "postprocess": update_serial
- }
- }, target_doc)
+ target_doc,
+ )
return doclist
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
index 6e727e53efd..a98cd10e320 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
@@ -16,6 +16,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_i
# test_records = frappe.get_test_records('Maintenance Schedule')
+
class TestMaintenanceSchedule(unittest.TestCase):
def test_events_should_be_created_and_deleted(self):
ms = make_maintenance_schedule()
@@ -42,15 +43,15 @@ class TestMaintenanceSchedule(unittest.TestCase):
expected_end_date = add_days(i.start_date, i.no_of_visits * 7)
self.assertEqual(i.end_date, expected_end_date)
- items = ms.get_pending_data(data_type = "items")
- items = items.split('\n')
+ items = ms.get_pending_data(data_type="items")
+ items = items.split("\n")
items.pop(0)
- expected_items = ['_Test Item']
+ expected_items = ["_Test Item"]
self.assertTrue(items, expected_items)
# "dates" contains all generated schedule dates
- dates = ms.get_pending_data(data_type = "date", item_name = i.item_name)
- dates = dates.split('\n')
+ dates = ms.get_pending_data(data_type="date", item_name=i.item_name)
+ dates = dates.split("\n")
dates.pop(0)
expected_dates.append(formatdate(add_days(i.start_date, 7), "dd-MM-yyyy"))
expected_dates.append(formatdate(add_days(i.start_date, 14), "dd-MM-yyyy"))
@@ -59,33 +60,38 @@ class TestMaintenanceSchedule(unittest.TestCase):
self.assertEqual(dates, expected_dates)
ms.submit()
- s_id = ms.get_pending_data(data_type = "id", item_name = i.item_name, s_date = expected_dates[1])
+ s_id = ms.get_pending_data(data_type="id", item_name=i.item_name, s_date=expected_dates[1])
# Check if item is mapped in visit.
- test_map_visit = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id)
+ test_map_visit = make_maintenance_visit(source_name=ms.name, item_name="_Test Item", s_id=s_id)
self.assertEqual(len(test_map_visit.purposes), 1)
self.assertEqual(test_map_visit.purposes[0].item_name, "_Test Item")
- visit = frappe.new_doc('Maintenance Visit')
+ visit = frappe.new_doc("Maintenance Visit")
visit = test_map_visit
visit.maintenance_schedule = ms.name
visit.maintenance_schedule_detail = s_id
visit.completion_status = "Partially Completed"
- visit.set('purposes', [{
- 'item_code': i.item_code,
- 'description': "test",
- 'work_done': "test",
- 'service_person': "Sales Team",
- }])
+ visit.set(
+ "purposes",
+ [
+ {
+ "item_code": i.item_code,
+ "description": "test",
+ "work_done": "test",
+ "service_person": "Sales Team",
+ }
+ ],
+ )
visit.save()
visit.submit()
- ms = frappe.get_doc('Maintenance Schedule', ms.name)
+ ms = frappe.get_doc("Maintenance Schedule", ms.name)
- #checks if visit status is back updated in schedule
+ # checks if visit status is back updated in schedule
self.assertTrue(ms.schedules[1].completion_status, "Partially Completed")
self.assertEqual(format_date(visit.mntc_date), format_date(ms.schedules[1].actual_date))
- #checks if visit status is updated on cancel
+ # checks if visit status is updated on cancel
visit.cancel()
ms.reload()
self.assertTrue(ms.schedules[1].completion_status, "Pending")
@@ -117,22 +123,24 @@ class TestMaintenanceSchedule(unittest.TestCase):
frappe.db.rollback()
+
def make_serial_item_with_serial(item_code):
serial_item_doc = create_item(item_code, is_stock_item=1)
if not serial_item_doc.has_serial_no or not serial_item_doc.serial_no_series:
serial_item_doc.has_serial_no = 1
serial_item_doc.serial_no_series = "TEST.###"
serial_item_doc.save(ignore_permissions=True)
- active_serials = frappe.db.get_all('Serial No', {"status": "Active", "item_code": item_code})
+ active_serials = frappe.db.get_all("Serial No", {"status": "Active", "item_code": item_code})
if len(active_serials) < 2:
make_serialized_item(item_code=item_code)
+
def get_events(ms):
- return frappe.get_all("Event Participants", filters={
- "reference_doctype": ms.doctype,
- "reference_docname": ms.name,
- "parenttype": "Event"
- })
+ return frappe.get_all(
+ "Event Participants",
+ filters={"reference_doctype": ms.doctype, "reference_docname": ms.name, "parenttype": "Event"},
+ )
+
def make_maintenance_schedule(**args):
ms = frappe.new_doc("Maintenance Schedule")
@@ -140,14 +148,17 @@ def make_maintenance_schedule(**args):
ms.customer = "_Test Customer"
ms.transaction_date = today()
- ms.append("items", {
- "item_code": args.get("item_code") or "_Test Item",
- "start_date": today(),
- "periodicity": "Weekly",
- "no_of_visits": 4,
- "serial_no": args.get("serial_no"),
- "sales_person": "Sales Team",
- })
+ ms.append(
+ "items",
+ {
+ "item_code": args.get("item_code") or "_Test Item",
+ "start_date": today(),
+ "periodicity": "Weekly",
+ "no_of_visits": 4,
+ "serial_no": args.get("serial_no"),
+ "sales_person": "Sales Team",
+ },
+ )
ms.insert(ignore_permissions=True)
return ms
diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
index f4a0d4d399c..939df5c07c5 100644
--- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
+++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
@@ -12,6 +12,9 @@ frappe.ui.form.on('Maintenance Visit', {
// filters for serial no based on item code
if (frm.doc.maintenance_type === "Scheduled") {
let item_code = frm.doc.purposes[0].item_code;
+ if (!item_code) {
+ return;
+ }
frappe.call({
method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.get_serial_nos_from_schedule",
args: {
diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
index 6fe2466be22..29a17849fd9 100644
--- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
+++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
@@ -14,7 +14,7 @@ class MaintenanceVisit(TransactionBase):
return _("To {0}").format(self.customer_name)
def validate_serial_no(self):
- for d in self.get('purposes'):
+ for d in self.get("purposes"):
if d.serial_no and not frappe.db.exists("Serial No", d.serial_no):
frappe.throw(_("Serial No {0} does not exist").format(d.serial_no))
@@ -24,13 +24,19 @@ class MaintenanceVisit(TransactionBase):
def validate_maintenance_date(self):
if self.maintenance_type == "Scheduled" and self.maintenance_schedule_detail:
- item_ref = frappe.db.get_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'item_reference')
+ item_ref = frappe.db.get_value(
+ "Maintenance Schedule Detail", self.maintenance_schedule_detail, "item_reference"
+ )
if item_ref:
- start_date, end_date = frappe.db.get_value('Maintenance Schedule Item', item_ref, ['start_date', 'end_date'])
- if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(self.mntc_date) > get_datetime(end_date):
- frappe.throw(_("Date must be between {0} and {1}")
- .format(format_date(start_date), format_date(end_date)))
-
+ start_date, end_date = frappe.db.get_value(
+ "Maintenance Schedule Item", item_ref, ["start_date", "end_date"]
+ )
+ if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(
+ self.mntc_date
+ ) > get_datetime(end_date):
+ frappe.throw(
+ _("Date must be between {0} and {1}").format(format_date(start_date), format_date(end_date))
+ )
def validate(self):
self.validate_serial_no()
@@ -44,73 +50,87 @@ class MaintenanceVisit(TransactionBase):
status = self.completion_status
actual_date = self.mntc_date
if self.maintenance_schedule_detail:
- frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', status)
- frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', actual_date)
+ frappe.db.set_value(
+ "Maintenance Schedule Detail", self.maintenance_schedule_detail, "completion_status", status
+ )
+ frappe.db.set_value(
+ "Maintenance Schedule Detail", self.maintenance_schedule_detail, "actual_date", actual_date
+ )
def update_customer_issue(self, flag):
if not self.maintenance_schedule:
- for d in self.get('purposes'):
- if d.prevdoc_docname and d.prevdoc_doctype == 'Warranty Claim' :
- if flag==1:
+ for d in self.get("purposes"):
+ if d.prevdoc_docname and d.prevdoc_doctype == "Warranty Claim":
+ if flag == 1:
mntc_date = self.mntc_date
service_person = d.service_person
work_done = d.work_done
status = "Open"
- if self.completion_status == 'Fully Completed':
- status = 'Closed'
- elif self.completion_status == 'Partially Completed':
- status = 'Work In Progress'
+ if self.completion_status == "Fully Completed":
+ status = "Closed"
+ elif self.completion_status == "Partially Completed":
+ status = "Work In Progress"
else:
- nm = frappe.db.sql("select t1.name, t1.mntc_date, t2.service_person, t2.work_done from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t1.completion_status = 'Partially Completed' and t2.prevdoc_docname = %s and t1.name!=%s and t1.docstatus = 1 order by t1.name desc limit 1", (d.prevdoc_docname, self.name))
+ nm = frappe.db.sql(
+ "select t1.name, t1.mntc_date, t2.service_person, t2.work_done from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t1.completion_status = 'Partially Completed' and t2.prevdoc_docname = %s and t1.name!=%s and t1.docstatus = 1 order by t1.name desc limit 1",
+ (d.prevdoc_docname, self.name),
+ )
if nm:
- status = 'Work In Progress'
- mntc_date = nm and nm[0][1] or ''
- service_person = nm and nm[0][2] or ''
- work_done = nm and nm[0][3] or ''
+ status = "Work In Progress"
+ mntc_date = nm and nm[0][1] or ""
+ service_person = nm and nm[0][2] or ""
+ work_done = nm and nm[0][3] or ""
else:
- status = 'Open'
+ status = "Open"
mntc_date = None
service_person = None
work_done = None
- wc_doc = frappe.get_doc('Warranty Claim', d.prevdoc_docname)
- wc_doc.update({
- 'resolution_date': mntc_date,
- 'resolved_by': service_person,
- 'resolution_details': work_done,
- 'status': status
- })
+ wc_doc = frappe.get_doc("Warranty Claim", d.prevdoc_docname)
+ wc_doc.update(
+ {
+ "resolution_date": mntc_date,
+ "resolved_by": service_person,
+ "resolution_details": work_done,
+ "status": status,
+ }
+ )
wc_doc.db_update()
def check_if_last_visit(self):
"""check if last maintenance visit against same sales order/ Warranty Claim"""
check_for_docname = None
- for d in self.get('purposes'):
+ for d in self.get("purposes"):
if d.prevdoc_docname:
check_for_docname = d.prevdoc_docname
- #check_for_doctype = d.prevdoc_doctype
+ # check_for_doctype = d.prevdoc_doctype
if check_for_docname:
- check = frappe.db.sql("select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t1.name!=%s and t2.prevdoc_docname=%s and t1.docstatus = 1 and (t1.mntc_date > %s or (t1.mntc_date = %s and t1.mntc_time > %s))", (self.name, check_for_docname, self.mntc_date, self.mntc_date, self.mntc_time))
+ check = frappe.db.sql(
+ "select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t1.name!=%s and t2.prevdoc_docname=%s and t1.docstatus = 1 and (t1.mntc_date > %s or (t1.mntc_date = %s and t1.mntc_time > %s))",
+ (self.name, check_for_docname, self.mntc_date, self.mntc_date, self.mntc_time),
+ )
if check:
check_lst = [x[0] for x in check]
- check_lst =','.join(check_lst)
- frappe.throw(_("Cancel Material Visits {0} before cancelling this Maintenance Visit").format(check_lst))
+ check_lst = ",".join(check_lst)
+ frappe.throw(
+ _("Cancel Material Visits {0} before cancelling this Maintenance Visit").format(check_lst)
+ )
raise Exception
else:
self.update_customer_issue(0)
def on_submit(self):
self.update_customer_issue(1)
- frappe.db.set(self, 'status', 'Submitted')
+ frappe.db.set(self, "status", "Submitted")
self.update_status_and_actual_date()
def on_cancel(self):
self.check_if_last_visit()
- frappe.db.set(self, 'status', 'Cancelled')
+ frappe.db.set(self, "status", "Cancelled")
self.update_status_and_actual_date(cancel=True)
def on_update(self):
diff --git a/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py
index a0c1a338e49..bdbed7c0965 100644
--- a/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py
+++ b/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Maintenance Visit')
+
class TestMaintenanceVisit(unittest.TestCase):
pass
diff --git a/erpnext/manufacturing/dashboard_fixtures.py b/erpnext/manufacturing/dashboard_fixtures.py
index 1bc12ff35eb..9e64f4dc196 100644
--- a/erpnext/manufacturing/dashboard_fixtures.py
+++ b/erpnext/manufacturing/dashboard_fixtures.py
@@ -11,33 +11,39 @@ import erpnext
def get_data():
- return frappe._dict({
- "dashboards": get_dashboards(),
- "charts": get_charts(),
- "number_cards": get_number_cards(),
- })
+ return frappe._dict(
+ {
+ "dashboards": get_dashboards(),
+ "charts": get_charts(),
+ "number_cards": get_number_cards(),
+ }
+ )
+
def get_dashboards():
- return [{
- "name": "Manufacturing",
- "dashboard_name": "Manufacturing",
- "charts": [
- { "chart": "Produced Quantity", "width": "Half" },
- { "chart": "Completed Operation", "width": "Half" },
- { "chart": "Work Order Analysis", "width": "Half" },
- { "chart": "Quality Inspection Analysis", "width": "Half" },
- { "chart": "Pending Work Order", "width": "Half" },
- { "chart": "Last Month Downtime Analysis", "width": "Half" },
- { "chart": "Work Order Qty Analysis", "width": "Full" },
- { "chart": "Job Card Analysis", "width": "Full" }
- ],
- "cards": [
- { "card": "Monthly Total Work Order" },
- { "card": "Monthly Completed Work Order" },
- { "card": "Ongoing Job Card" },
- { "card": "Monthly Quality Inspection"}
- ]
- }]
+ return [
+ {
+ "name": "Manufacturing",
+ "dashboard_name": "Manufacturing",
+ "charts": [
+ {"chart": "Produced Quantity", "width": "Half"},
+ {"chart": "Completed Operation", "width": "Half"},
+ {"chart": "Work Order Analysis", "width": "Half"},
+ {"chart": "Quality Inspection Analysis", "width": "Half"},
+ {"chart": "Pending Work Order", "width": "Half"},
+ {"chart": "Last Month Downtime Analysis", "width": "Half"},
+ {"chart": "Work Order Qty Analysis", "width": "Full"},
+ {"chart": "Job Card Analysis", "width": "Full"},
+ ],
+ "cards": [
+ {"card": "Monthly Total Work Order"},
+ {"card": "Monthly Completed Work Order"},
+ {"card": "Ongoing Job Card"},
+ {"card": "Monthly Quality Inspection"},
+ ],
+ }
+ ]
+
def get_charts():
company = erpnext.get_default_company()
@@ -45,200 +51,198 @@ def get_charts():
if not company:
company = frappe.db.get_value("Company", {"is_group": 0}, "name")
- return [{
- "doctype": "Dashboard Chart",
- "based_on": "modified",
- "chart_type": "Sum",
- "chart_name": _("Produced Quantity"),
- "name": "Produced Quantity",
- "document_type": "Work Order",
- "filters_json": json.dumps([['Work Order', 'docstatus', '=', 1, False]]),
- "group_by_type": "Count",
- "time_interval": "Monthly",
- "timespan": "Last Year",
- "owner": "Administrator",
- "type": "Line",
- "value_based_on": "produced_qty",
- "is_public": 1,
- "timeseries": 1
- }, {
- "doctype": "Dashboard Chart",
- "based_on": "creation",
- "chart_type": "Sum",
- "chart_name": _("Completed Operation"),
- "name": "Completed Operation",
- "document_type": "Work Order Operation",
- "filters_json": json.dumps([['Work Order Operation', 'docstatus', '=', 1, False]]),
- "group_by_type": "Count",
- "time_interval": "Quarterly",
- "timespan": "Last Year",
- "owner": "Administrator",
- "type": "Line",
- "value_based_on": "completed_qty",
- "is_public": 1,
- "timeseries": 1
- }, {
- "doctype": "Dashboard Chart",
- "time_interval": "Yearly",
- "chart_type": "Report",
- "chart_name": _("Work Order Analysis"),
- "name": "Work Order Analysis",
- "timespan": "Last Year",
- "report_name": "Work Order Summary",
- "owner": "Administrator",
- "filters_json": json.dumps({"company": company, "charts_based_on": "Status"}),
- "type": "Donut",
- "is_public": 1,
- "is_custom": 1,
- "custom_options": json.dumps({
- "axisOptions": {
- "shortenYAxisNumbers": 1
- },
- "height": 300
- }),
- }, {
- "doctype": "Dashboard Chart",
- "time_interval": "Yearly",
- "chart_type": "Report",
- "chart_name": _("Quality Inspection Analysis"),
- "name": "Quality Inspection Analysis",
- "timespan": "Last Year",
- "report_name": "Quality Inspection Summary",
- "owner": "Administrator",
- "filters_json": json.dumps({}),
- "type": "Donut",
- "is_public": 1,
- "is_custom": 1,
- "custom_options": json.dumps({
- "axisOptions": {
- "shortenYAxisNumbers": 1
- },
- "height": 300
- }),
- }, {
- "doctype": "Dashboard Chart",
- "time_interval": "Yearly",
- "chart_type": "Report",
- "chart_name": _("Pending Work Order"),
- "name": "Pending Work Order",
- "timespan": "Last Year",
- "report_name": "Work Order Summary",
- "filters_json": json.dumps({"company": company, "charts_based_on": "Age"}),
- "owner": "Administrator",
- "type": "Donut",
- "is_public": 1,
- "is_custom": 1,
- "custom_options": json.dumps({
- "axisOptions": {
- "shortenYAxisNumbers": 1
- },
- "height": 300
- }),
- }, {
- "doctype": "Dashboard Chart",
- "time_interval": "Yearly",
- "chart_type": "Report",
- "chart_name": _("Last Month Downtime Analysis"),
- "name": "Last Month Downtime Analysis",
- "timespan": "Last Year",
- "filters_json": json.dumps({}),
- "report_name": "Downtime Analysis",
- "owner": "Administrator",
- "is_public": 1,
- "is_custom": 1,
- "type": "Bar"
- }, {
- "doctype": "Dashboard Chart",
- "time_interval": "Yearly",
- "chart_type": "Report",
- "chart_name": _("Work Order Qty Analysis"),
- "name": "Work Order Qty Analysis",
- "timespan": "Last Year",
- "report_name": "Work Order Summary",
- "filters_json": json.dumps({"company": company, "charts_based_on": "Quantity"}),
- "owner": "Administrator",
- "type": "Bar",
- "is_public": 1,
- "is_custom": 1,
- "custom_options": json.dumps({
- "barOptions": { "stacked": 1 }
- }),
- }, {
- "doctype": "Dashboard Chart",
- "time_interval": "Yearly",
- "chart_type": "Report",
- "chart_name": _("Job Card Analysis"),
- "name": "Job Card Analysis",
- "timespan": "Last Year",
- "report_name": "Job Card Summary",
- "owner": "Administrator",
- "is_public": 1,
- "is_custom": 1,
- "filters_json": json.dumps({"company": company, "docstatus": 1, "range":"Monthly"}),
- "custom_options": json.dumps({
- "barOptions": { "stacked": 1 }
- }),
- "type": "Bar"
- }]
+ return [
+ {
+ "doctype": "Dashboard Chart",
+ "based_on": "modified",
+ "chart_type": "Sum",
+ "chart_name": _("Produced Quantity"),
+ "name": "Produced Quantity",
+ "document_type": "Work Order",
+ "filters_json": json.dumps([["Work Order", "docstatus", "=", 1, False]]),
+ "group_by_type": "Count",
+ "time_interval": "Monthly",
+ "timespan": "Last Year",
+ "owner": "Administrator",
+ "type": "Line",
+ "value_based_on": "produced_qty",
+ "is_public": 1,
+ "timeseries": 1,
+ },
+ {
+ "doctype": "Dashboard Chart",
+ "based_on": "creation",
+ "chart_type": "Sum",
+ "chart_name": _("Completed Operation"),
+ "name": "Completed Operation",
+ "document_type": "Work Order Operation",
+ "filters_json": json.dumps([["Work Order Operation", "docstatus", "=", 1, False]]),
+ "group_by_type": "Count",
+ "time_interval": "Quarterly",
+ "timespan": "Last Year",
+ "owner": "Administrator",
+ "type": "Line",
+ "value_based_on": "completed_qty",
+ "is_public": 1,
+ "timeseries": 1,
+ },
+ {
+ "doctype": "Dashboard Chart",
+ "time_interval": "Yearly",
+ "chart_type": "Report",
+ "chart_name": _("Work Order Analysis"),
+ "name": "Work Order Analysis",
+ "timespan": "Last Year",
+ "report_name": "Work Order Summary",
+ "owner": "Administrator",
+ "filters_json": json.dumps({"company": company, "charts_based_on": "Status"}),
+ "type": "Donut",
+ "is_public": 1,
+ "is_custom": 1,
+ "custom_options": json.dumps({"axisOptions": {"shortenYAxisNumbers": 1}, "height": 300}),
+ },
+ {
+ "doctype": "Dashboard Chart",
+ "time_interval": "Yearly",
+ "chart_type": "Report",
+ "chart_name": _("Quality Inspection Analysis"),
+ "name": "Quality Inspection Analysis",
+ "timespan": "Last Year",
+ "report_name": "Quality Inspection Summary",
+ "owner": "Administrator",
+ "filters_json": json.dumps({}),
+ "type": "Donut",
+ "is_public": 1,
+ "is_custom": 1,
+ "custom_options": json.dumps({"axisOptions": {"shortenYAxisNumbers": 1}, "height": 300}),
+ },
+ {
+ "doctype": "Dashboard Chart",
+ "time_interval": "Yearly",
+ "chart_type": "Report",
+ "chart_name": _("Pending Work Order"),
+ "name": "Pending Work Order",
+ "timespan": "Last Year",
+ "report_name": "Work Order Summary",
+ "filters_json": json.dumps({"company": company, "charts_based_on": "Age"}),
+ "owner": "Administrator",
+ "type": "Donut",
+ "is_public": 1,
+ "is_custom": 1,
+ "custom_options": json.dumps({"axisOptions": {"shortenYAxisNumbers": 1}, "height": 300}),
+ },
+ {
+ "doctype": "Dashboard Chart",
+ "time_interval": "Yearly",
+ "chart_type": "Report",
+ "chart_name": _("Last Month Downtime Analysis"),
+ "name": "Last Month Downtime Analysis",
+ "timespan": "Last Year",
+ "filters_json": json.dumps({}),
+ "report_name": "Downtime Analysis",
+ "owner": "Administrator",
+ "is_public": 1,
+ "is_custom": 1,
+ "type": "Bar",
+ },
+ {
+ "doctype": "Dashboard Chart",
+ "time_interval": "Yearly",
+ "chart_type": "Report",
+ "chart_name": _("Work Order Qty Analysis"),
+ "name": "Work Order Qty Analysis",
+ "timespan": "Last Year",
+ "report_name": "Work Order Summary",
+ "filters_json": json.dumps({"company": company, "charts_based_on": "Quantity"}),
+ "owner": "Administrator",
+ "type": "Bar",
+ "is_public": 1,
+ "is_custom": 1,
+ "custom_options": json.dumps({"barOptions": {"stacked": 1}}),
+ },
+ {
+ "doctype": "Dashboard Chart",
+ "time_interval": "Yearly",
+ "chart_type": "Report",
+ "chart_name": _("Job Card Analysis"),
+ "name": "Job Card Analysis",
+ "timespan": "Last Year",
+ "report_name": "Job Card Summary",
+ "owner": "Administrator",
+ "is_public": 1,
+ "is_custom": 1,
+ "filters_json": json.dumps({"company": company, "docstatus": 1, "range": "Monthly"}),
+ "custom_options": json.dumps({"barOptions": {"stacked": 1}}),
+ "type": "Bar",
+ },
+ ]
+
def get_number_cards():
start_date = add_months(nowdate(), -1)
end_date = nowdate()
- return [{
- "doctype": "Number Card",
- "document_type": "Work Order",
- "name": "Monthly Total Work Order",
- "filters_json": json.dumps([
- ['Work Order', 'docstatus', '=', 1],
- ['Work Order', 'creation', 'between', [start_date, end_date]]
- ]),
- "function": "Count",
- "is_public": 1,
- "label": _("Monthly Total Work Orders"),
- "show_percentage_stats": 1,
- "stats_time_interval": "Weekly"
- },
- {
- "doctype": "Number Card",
- "document_type": "Work Order",
- "name": "Monthly Completed Work Order",
- "filters_json": json.dumps([
- ['Work Order', 'status', '=', 'Completed'],
- ['Work Order', 'docstatus', '=', 1],
- ['Work Order', 'creation', 'between', [start_date, end_date]]
- ]),
- "function": "Count",
- "is_public": 1,
- "label": _("Monthly Completed Work Orders"),
- "show_percentage_stats": 1,
- "stats_time_interval": "Weekly"
- },
- {
- "doctype": "Number Card",
- "document_type": "Job Card",
- "name": "Ongoing Job Card",
- "filters_json": json.dumps([
- ['Job Card', 'status','!=','Completed'],
- ['Job Card', 'docstatus', '=', 1]
- ]),
- "function": "Count",
- "is_public": 1,
- "label": _("Ongoing Job Cards"),
- "show_percentage_stats": 1,
- "stats_time_interval": "Weekly"
- },
- {
- "doctype": "Number Card",
- "document_type": "Quality Inspection",
- "name": "Monthly Quality Inspection",
- "filters_json": json.dumps([
- ['Quality Inspection', 'docstatus', '=', 1],
- ['Quality Inspection', 'creation', 'between', [start_date, end_date]]
- ]),
- "function": "Count",
- "is_public": 1,
- "label": _("Monthly Quality Inspections"),
- "show_percentage_stats": 1,
- "stats_time_interval": "Weekly"
- }]
+ return [
+ {
+ "doctype": "Number Card",
+ "document_type": "Work Order",
+ "name": "Monthly Total Work Order",
+ "filters_json": json.dumps(
+ [
+ ["Work Order", "docstatus", "=", 1],
+ ["Work Order", "creation", "between", [start_date, end_date]],
+ ]
+ ),
+ "function": "Count",
+ "is_public": 1,
+ "label": _("Monthly Total Work Orders"),
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Weekly",
+ },
+ {
+ "doctype": "Number Card",
+ "document_type": "Work Order",
+ "name": "Monthly Completed Work Order",
+ "filters_json": json.dumps(
+ [
+ ["Work Order", "status", "=", "Completed"],
+ ["Work Order", "docstatus", "=", 1],
+ ["Work Order", "creation", "between", [start_date, end_date]],
+ ]
+ ),
+ "function": "Count",
+ "is_public": 1,
+ "label": _("Monthly Completed Work Orders"),
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Weekly",
+ },
+ {
+ "doctype": "Number Card",
+ "document_type": "Job Card",
+ "name": "Ongoing Job Card",
+ "filters_json": json.dumps(
+ [["Job Card", "status", "!=", "Completed"], ["Job Card", "docstatus", "=", 1]]
+ ),
+ "function": "Count",
+ "is_public": 1,
+ "label": _("Ongoing Job Cards"),
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Weekly",
+ },
+ {
+ "doctype": "Number Card",
+ "document_type": "Quality Inspection",
+ "name": "Monthly Quality Inspection",
+ "filters_json": json.dumps(
+ [
+ ["Quality Inspection", "docstatus", "=", 1],
+ ["Quality Inspection", "creation", "between", [start_date, end_date]],
+ ]
+ ),
+ "function": "Count",
+ "is_public": 1,
+ "label": _("Monthly Quality Inspections"),
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Weekly",
+ },
+ ]
diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
index 5340c51131f..ff2140199de 100644
--- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
+++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
@@ -29,7 +29,9 @@ class BlanketOrder(Document):
def update_ordered_qty(self):
ref_doctype = "Sales Order" if self.blanket_order_type == "Selling" else "Purchase Order"
- item_ordered_qty = frappe._dict(frappe.db.sql("""
+ item_ordered_qty = frappe._dict(
+ frappe.db.sql(
+ """
select trans_item.item_code, sum(trans_item.stock_qty) as qty
from `tab{0} Item` trans_item, `tab{0}` trans
where trans.name = trans_item.parent
@@ -37,18 +39,24 @@ class BlanketOrder(Document):
and trans.docstatus=1
and trans.status not in ('Closed', 'Stopped')
group by trans_item.item_code
- """.format(ref_doctype), self.name))
+ """.format(
+ ref_doctype
+ ),
+ self.name,
+ )
+ )
for d in self.items:
d.db_set("ordered_qty", item_ordered_qty.get(d.item_code, 0))
+
@frappe.whitelist()
def make_order(source_name):
doctype = frappe.flags.args.doctype
def update_doc(source_doc, target_doc, source_parent):
- if doctype == 'Quotation':
- target_doc.quotation_to = 'Customer'
+ if doctype == "Quotation":
+ target_doc.quotation_to = "Customer"
target_doc.party_name = source_doc.customer
def update_item(source, target, source_parent):
@@ -62,18 +70,16 @@ def make_order(source_name):
target.against_blanket_order = 1
target.blanket_order = source_name
- target_doc = get_mapped_doc("Blanket Order", source_name, {
- "Blanket Order": {
- "doctype": doctype,
- "postprocess": update_doc
- },
- "Blanket Order Item": {
- "doctype": doctype + " Item",
- "field_map": {
- "rate": "blanket_order_rate",
- "parent": "blanket_order"
+ target_doc = get_mapped_doc(
+ "Blanket Order",
+ source_name,
+ {
+ "Blanket Order": {"doctype": doctype, "postprocess": update_doc},
+ "Blanket Order Item": {
+ "doctype": doctype + " Item",
+ "field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"},
+ "postprocess": update_item,
},
- "postprocess": update_item
- }
- })
+ },
+ )
return target_doc
diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order_dashboard.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order_dashboard.py
index 2556f2f163d..31062342d03 100644
--- a/erpnext/manufacturing/doctype/blanket_order/blanket_order_dashboard.py
+++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order_dashboard.py
@@ -1,11 +1,5 @@
-
-
def get_data():
return {
- 'fieldname': 'blanket_order',
- 'transactions': [
- {
- 'items': ['Purchase Order', 'Sales Order', 'Quotation']
- }
- ]
+ "fieldname": "blanket_order",
+ "transactions": [{"items": ["Purchase Order", "Sales Order", "Quotation"]}],
}
diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
index eff2344e85c..2f1f3ae0f52 100644
--- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
+++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
@@ -1,22 +1,22 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_months, today
from erpnext import get_company_currency
-from erpnext.tests.utils import ERPNextTestCase
from .blanket_order import make_order
-class TestBlanketOrder(ERPNextTestCase):
+class TestBlanketOrder(FrappeTestCase):
def setUp(self):
frappe.flags.args = frappe._dict()
def test_sales_order_creation(self):
bo = make_blanket_order(blanket_order_type="Selling")
- frappe.flags.args.doctype = 'Sales Order'
+ frappe.flags.args.doctype = "Sales Order"
so = make_order(bo.name)
so.currency = get_company_currency(so.company)
so.delivery_date = today()
@@ -33,16 +33,15 @@ class TestBlanketOrder(ERPNextTestCase):
self.assertEqual(so.items[0].qty, bo.items[0].ordered_qty)
# test the quantity
- frappe.flags.args.doctype = 'Sales Order'
+ frappe.flags.args.doctype = "Sales Order"
so1 = make_order(bo.name)
so1.currency = get_company_currency(so1.company)
- self.assertEqual(so1.items[0].qty, (bo.items[0].qty-bo.items[0].ordered_qty))
-
+ self.assertEqual(so1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty))
def test_purchase_order_creation(self):
bo = make_blanket_order(blanket_order_type="Purchasing")
- frappe.flags.args.doctype = 'Purchase Order'
+ frappe.flags.args.doctype = "Purchase Order"
po = make_order(bo.name)
po.currency = get_company_currency(po.company)
po.schedule_date = today()
@@ -59,11 +58,10 @@ class TestBlanketOrder(ERPNextTestCase):
self.assertEqual(po.items[0].qty, bo.items[0].ordered_qty)
# test the quantity
- frappe.flags.args.doctype = 'Purchase Order'
+ frappe.flags.args.doctype = "Purchase Order"
po1 = make_order(bo.name)
po1.currency = get_company_currency(po1.company)
- self.assertEqual(po1.items[0].qty, (bo.items[0].qty-bo.items[0].ordered_qty))
-
+ self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty))
def make_blanket_order(**args):
@@ -80,11 +78,14 @@ def make_blanket_order(**args):
bo.from_date = today()
bo.to_date = add_months(bo.from_date, months=12)
- bo.append("items", {
- "item_code": args.item_code or "_Test Item",
- "qty": args.quantity or 1000,
- "rate": args.rate or 100
- })
+ bo.append(
+ "items",
+ {
+ "item_code": args.item_code or "_Test Item",
+ "qty": args.quantity or 1000,
+ "rate": args.rate or 100,
+ },
+ )
bo.insert()
bo.submit()
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index b97dcab632f..f8fcd073951 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import functools
+import re
from collections import deque
from operator import itemgetter
from typing import List
@@ -18,9 +19,7 @@ from erpnext.setup.utils import get_exchange_rate
from erpnext.stock.doctype.item.item import get_item_details
from erpnext.stock.get_item_details import get_conversion_factor, get_price_list_rate
-form_grid_templates = {
- "items": "templates/form_grid/item_grid.html"
-}
+form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
class BOMTree:
@@ -30,10 +29,12 @@ class BOMTree:
# ref: https://docs.python.org/3/reference/datamodel.html#slots
__slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"]
- def __init__(self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1) -> None:
+ def __init__(
+ self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1
+ ) -> None:
self.name = name # name of node, BOM number if is_bom else item_code
self.child_items: List["BOMTree"] = [] # list of child items
- self.is_bom = is_bom # true if the node is a BOM and not a leaf item
+ self.is_bom = is_bom # true if the node is a BOM and not a leaf item
self.item_code: str = None # item_code associated with node
self.qty = qty # required unit quantity to make one unit of parent item.
self.exploded_qty = exploded_qty # total exploded qty required for making root of tree.
@@ -61,12 +62,12 @@ class BOMTree:
"""Get level order traversal of tree.
E.g. for following tree the traversal will return list of nodes in order from top to bottom.
BOM:
- - SubAssy1
- - item1
- - item2
- - SubAssy2
- - item3
- - item4
+ - SubAssy1
+ - item1
+ - item2
+ - SubAssy2
+ - item3
+ - item4
returns = [SubAssy1, item1, item2, SubAssy2, item3, item4]
"""
@@ -95,52 +96,84 @@ class BOMTree:
rep += child.__repr__(level=level + 1)
return rep
+
class BOM(WebsiteGenerator):
website = frappe._dict(
# page_title_field = "item_name",
- condition_field = "show_in_website",
- template = "templates/generators/bom.html"
+ condition_field="show_in_website",
+ template="templates/generators/bom.html",
)
def autoname(self):
- names = frappe.db.sql_list("""select name from `tabBOM` where item=%s""", self.item)
+ # ignore amended documents while calculating current index
+ existing_boms = frappe.get_all(
+ "BOM", filters={"item": self.item, "amended_from": ["is", "not set"]}, pluck="name"
+ )
- if names:
- # name can be BOM/ITEM/001, BOM/ITEM/001-1, BOM-ITEM-001, BOM-ITEM-001-1
-
- # split by item
- names = [name.split(self.item, 1) for name in names]
- names = [d[-1][1:] for d in filter(lambda x: len(x) > 1 and x[-1], names)]
-
- # split by (-) if cancelled
- if names:
- names = [cint(name.split('-')[-1]) for name in names]
- idx = max(names) + 1
- else:
- idx = 1
+ if existing_boms:
+ index = self.get_next_version_index(existing_boms)
else:
- idx = 1
+ index = 1
+
+ prefix = self.doctype
+ suffix = "%.3i" % index # convert index to string (1 -> "001")
+ bom_name = f"{prefix}-{self.item}-{suffix}"
+
+ if len(bom_name) <= 140:
+ name = bom_name
+ else:
+ # since max characters for name is 140, remove enough characters from the
+ # item name to fit the prefix, suffix and the separators
+ truncated_length = 140 - (len(prefix) + len(suffix) + 2)
+ truncated_item_name = self.item[:truncated_length]
+ # if a partial word is found after truncate, remove the extra characters
+ truncated_item_name = truncated_item_name.rsplit(" ", 1)[0]
+ name = f"{prefix}-{truncated_item_name}-{suffix}"
- name = 'BOM-' + self.item + ('-%.3i' % idx)
if frappe.db.exists("BOM", name):
conflicting_bom = frappe.get_doc("BOM", name)
if conflicting_bom.item != self.item:
- msg = (_("A BOM with name {0} already exists for item {1}.")
- .format(frappe.bold(name), frappe.bold(conflicting_bom.item)))
+ msg = _("A BOM with name {0} already exists for item {1}.").format(
+ frappe.bold(name), frappe.bold(conflicting_bom.item)
+ )
- frappe.throw(_("{0}{1} Did you rename the item? Please contact Administrator / Tech support")
- .format(msg, " "))
+ frappe.throw(
+ _("{0}{1} Did you rename the item? Please contact Administrator / Tech support").format(
+ msg, " "
+ )
+ )
self.name = name
+ @staticmethod
+ def get_next_version_index(existing_boms: List[str]) -> int:
+ # split by "/" and "-"
+ delimiters = ["/", "-"]
+ pattern = "|".join(map(re.escape, delimiters))
+ bom_parts = [re.split(pattern, bom_name) for bom_name in existing_boms]
+
+ # filter out BOMs that do not follow the following formats: BOM/ITEM/001, BOM-ITEM-001
+ valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts))
+
+ # extract the current index from the BOM parts
+ if valid_bom_parts:
+ # handle cancelled and submitted documents
+ indexes = [cint(part[-1]) for part in valid_bom_parts]
+ index = max(indexes) + 1
+ else:
+ index = 1
+
+ return index
+
def validate(self):
- self.route = frappe.scrub(self.name).replace('_', '-')
+ self.route = frappe.scrub(self.name).replace("_", "-")
if not self.company:
frappe.throw(_("Please select a Company first."), title=_("Mandatory"))
self.clear_operations()
+ self.clear_inspection()
self.validate_main_item()
self.validate_currency()
self.set_conversion_rate()
@@ -155,13 +188,13 @@ class BOM(WebsiteGenerator):
self.calculate_cost()
self.update_stock_qty()
self.validate_scrap_items()
- self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
+ self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False)
def get_context(self, context):
- context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
+ context.parents = [{"name": "boms", "title": _("All BOMs")}]
def on_update(self):
- frappe.cache().hdel('bom_children', self.name)
+ frappe.cache().hdel("bom_children", self.name)
self.check_recursion()
def on_submit(self):
@@ -191,31 +224,47 @@ class BOM(WebsiteGenerator):
def get_routing(self):
if self.routing:
self.set("operations", [])
- fields = ["sequence_id", "operation", "workstation", "description",
- "time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate"]
+ fields = [
+ "sequence_id",
+ "operation",
+ "workstation",
+ "description",
+ "time_in_mins",
+ "batch_size",
+ "operating_cost",
+ "idx",
+ "hour_rate",
+ "set_cost_based_on_bom_qty",
+ ]
- for row in frappe.get_all("BOM Operation", fields = fields,
- filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="sequence_id, idx"):
- child = self.append('operations', row)
- child.hour_rate = flt(row.hour_rate / self.conversion_rate, 2)
+ for row in frappe.get_all(
+ "BOM Operation",
+ fields=fields,
+ filters={"parenttype": "Routing", "parent": self.routing},
+ order_by="sequence_id, idx",
+ ):
+ child = self.append("operations", row)
+ child.hour_rate = flt(row.hour_rate / self.conversion_rate, child.precision("hour_rate"))
def set_bom_material_details(self):
for item in self.get("items"):
self.validate_bom_currency(item)
- ret = self.get_bom_material_detail({
- "company": self.company,
- "item_code": item.item_code,
- "item_name": item.item_name,
- "bom_no": item.bom_no,
- "stock_qty": item.stock_qty,
- "include_item_in_manufacturing": item.include_item_in_manufacturing,
- "qty": item.qty,
- "uom": item.uom,
- "stock_uom": item.stock_uom,
- "conversion_factor": item.conversion_factor,
- "sourced_by_supplier": item.sourced_by_supplier
- })
+ ret = self.get_bom_material_detail(
+ {
+ "company": self.company,
+ "item_code": item.item_code,
+ "item_name": item.item_name,
+ "bom_no": item.bom_no,
+ "stock_qty": item.stock_qty,
+ "include_item_in_manufacturing": item.include_item_in_manufacturing,
+ "qty": item.qty,
+ "uom": item.uom,
+ "stock_uom": item.stock_uom,
+ "conversion_factor": item.conversion_factor,
+ "sourced_by_supplier": item.sourced_by_supplier,
+ }
+ )
for r in ret:
if not item.get(r):
item.set(r, ret[r])
@@ -226,7 +275,7 @@ class BOM(WebsiteGenerator):
"item_code": item.item_code,
"company": self.company,
"scrap_items": True,
- "bom_no": '',
+ "bom_no": "",
}
ret = self.get_bom_material_detail(args)
for key, value in ret.items():
@@ -235,72 +284,90 @@ class BOM(WebsiteGenerator):
@frappe.whitelist()
def get_bom_material_detail(self, args=None):
- """ Get raw material details like uom, desc and rate"""
+ """Get raw material details like uom, desc and rate"""
if not args:
- args = frappe.form_dict.get('args')
+ args = frappe.form_dict.get("args")
if isinstance(args, str):
import json
+
args = json.loads(args)
- item = self.get_item_det(args['item_code'])
+ item = self.get_item_det(args["item_code"])
- args['bom_no'] = args['bom_no'] or item and cstr(item['default_bom']) or ''
- args['transfer_for_manufacture'] = (cstr(args.get('include_item_in_manufacturing', '')) or
- item and item.include_item_in_manufacturing or 0)
+ args["bom_no"] = args["bom_no"] or item and cstr(item["default_bom"]) or ""
+ args["transfer_for_manufacture"] = (
+ cstr(args.get("include_item_in_manufacturing", ""))
+ or item
+ and item.include_item_in_manufacturing
+ or 0
+ )
args.update(item)
rate = self.get_rm_rate(args)
ret_item = {
- 'item_name' : item and args['item_name'] or '',
- 'description' : item and args['description'] or '',
- 'image' : item and args['image'] or '',
- 'stock_uom' : item and args['stock_uom'] or '',
- 'uom' : item and args['stock_uom'] or '',
- 'conversion_factor': 1,
- 'bom_no' : args['bom_no'],
- 'rate' : rate,
- 'qty' : args.get("qty") or args.get("stock_qty") or 1,
- 'stock_qty' : args.get("qty") or args.get("stock_qty") or 1,
- 'base_rate' : flt(rate) * (flt(self.conversion_rate) or 1),
- 'include_item_in_manufacturing': cint(args.get('transfer_for_manufacture')),
- 'sourced_by_supplier' : args.get('sourced_by_supplier', 0)
+ "item_name": item and args["item_name"] or "",
+ "description": item and args["description"] or "",
+ "image": item and args["image"] or "",
+ "stock_uom": item and args["stock_uom"] or "",
+ "uom": item and args["stock_uom"] or "",
+ "conversion_factor": 1,
+ "bom_no": args["bom_no"],
+ "rate": rate,
+ "qty": args.get("qty") or args.get("stock_qty") or 1,
+ "stock_qty": args.get("qty") or args.get("stock_qty") or 1,
+ "base_rate": flt(rate) * (flt(self.conversion_rate) or 1),
+ "include_item_in_manufacturing": cint(args.get("transfer_for_manufacture")),
+ "sourced_by_supplier": args.get("sourced_by_supplier", 0),
}
return ret_item
def validate_bom_currency(self, item):
- if item.get('bom_no') and frappe.db.get_value('BOM', item.get('bom_no'), 'currency') != self.currency:
- frappe.throw(_("Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2}")
- .format(item.idx, item.bom_no, self.currency))
+ if (
+ item.get("bom_no")
+ and frappe.db.get_value("BOM", item.get("bom_no"), "currency") != self.currency
+ ):
+ frappe.throw(
+ _("Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2}").format(
+ item.idx, item.bom_no, self.currency
+ )
+ )
def get_rm_rate(self, arg):
- """ Get raw material rate as per selected method, if bom exists takes bom cost """
+ """Get raw material rate as per selected method, if bom exists takes bom cost"""
rate = 0
if not self.rm_cost_as_per:
self.rm_cost_as_per = "Valuation Rate"
- if arg.get('scrap_items'):
+ if arg.get("scrap_items"):
rate = get_valuation_rate(arg)
elif arg:
- #Customer Provided parts and Supplier sourced parts will have zero rate
- if not frappe.db.get_value('Item', arg["item_code"], 'is_customer_provided_item') and not arg.get('sourced_by_supplier'):
- if arg.get('bom_no') and self.set_rate_of_sub_assembly_item_based_on_bom:
- rate = flt(self.get_bom_unitcost(arg['bom_no'])) * (arg.get("conversion_factor") or 1)
+ # Customer Provided parts and Supplier sourced parts will have zero rate
+ if not frappe.db.get_value(
+ "Item", arg["item_code"], "is_customer_provided_item"
+ ) and not arg.get("sourced_by_supplier"):
+ if arg.get("bom_no") and self.set_rate_of_sub_assembly_item_based_on_bom:
+ rate = flt(self.get_bom_unitcost(arg["bom_no"])) * (arg.get("conversion_factor") or 1)
else:
rate = get_bom_item_rate(arg, self)
if not rate:
if self.rm_cost_as_per == "Price List":
- frappe.msgprint(_("Price not found for item {0} in price list {1}")
- .format(arg["item_code"], self.buying_price_list), alert=True)
+ frappe.msgprint(
+ _("Price not found for item {0} in price list {1}").format(
+ arg["item_code"], self.buying_price_list
+ ),
+ alert=True,
+ )
else:
- frappe.msgprint(_("{0} not found for item {1}")
- .format(self.rm_cost_as_per, arg["item_code"]), alert=True)
+ frappe.msgprint(
+ _("{0} not found for item {1}").format(self.rm_cost_as_per, arg["item_code"]), alert=True
+ )
return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1)
@frappe.whitelist()
- def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate = True, save=True):
+ def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate=True, save=True):
if self.docstatus == 2:
return
@@ -310,16 +377,18 @@ class BOM(WebsiteGenerator):
if not d.item_code:
continue
- rate = self.get_rm_rate({
- "company": self.company,
- "item_code": d.item_code,
- "bom_no": d.bom_no,
- "qty": d.qty,
- "uom": d.uom,
- "stock_uom": d.stock_uom,
- "conversion_factor": d.conversion_factor,
- "sourced_by_supplier": d.sourced_by_supplier
- })
+ rate = self.get_rm_rate(
+ {
+ "company": self.company,
+ "item_code": d.item_code,
+ "bom_no": d.bom_no,
+ "qty": d.qty,
+ "uom": d.uom,
+ "stock_uom": d.stock_uom,
+ "conversion_factor": d.conversion_factor,
+ "sourced_by_supplier": d.sourced_by_supplier,
+ }
+ )
if rate:
d.rate = rate
@@ -340,8 +409,11 @@ class BOM(WebsiteGenerator):
# update parent BOMs
if self.total_cost != existing_bom_cost and update_parent:
- parent_boms = frappe.db.sql_list("""select distinct parent from `tabBOM Item`
- where bom_no = %s and docstatus=1 and parenttype='BOM'""", self.name)
+ parent_boms = frappe.db.sql_list(
+ """select distinct parent from `tabBOM Item`
+ where bom_no = %s and docstatus=1 and parenttype='BOM'""",
+ self.name,
+ )
for bom in parent_boms:
frappe.get_doc("BOM", bom).update_cost(from_child_bom=True)
@@ -353,41 +425,54 @@ class BOM(WebsiteGenerator):
if self.total_cost:
cost = self.total_cost / self.quantity
- frappe.db.sql("""update `tabBOM Item` set rate=%s, amount=stock_qty*%s
+ frappe.db.sql(
+ """update `tabBOM Item` set rate=%s, amount=stock_qty*%s
where bom_no = %s and docstatus < 2 and parenttype='BOM'""",
- (cost, cost, self.name))
+ (cost, cost, self.name),
+ )
def get_bom_unitcost(self, bom_no):
- bom = frappe.db.sql("""select name, base_total_cost/quantity as unit_cost from `tabBOM`
- where is_active = 1 and name = %s""", bom_no, as_dict=1)
- return bom and bom[0]['unit_cost'] or 0
+ bom = frappe.db.sql(
+ """select name, base_total_cost/quantity as unit_cost from `tabBOM`
+ where is_active = 1 and name = %s""",
+ bom_no,
+ as_dict=1,
+ )
+ return bom and bom[0]["unit_cost"] or 0
def manage_default_bom(self):
- """ Uncheck others if current one is selected as default or
- check the current one as default if it the only bom for the selected item,
- update default bom in item master
+ """Uncheck others if current one is selected as default or
+ check the current one as default if it the only bom for the selected item,
+ update default bom in item master
"""
if self.is_default and self.is_active:
from frappe.model.utils import set_default
+
set_default(self, "item")
item = frappe.get_doc("Item", self.item)
if item.default_bom != self.name:
- frappe.db.set_value('Item', self.item, 'default_bom', self.name)
- elif not frappe.db.exists(dict(doctype='BOM', docstatus=1, item=self.item, is_default=1)) \
- and self.is_active:
+ frappe.db.set_value("Item", self.item, "default_bom", self.name)
+ elif (
+ not frappe.db.exists(dict(doctype="BOM", docstatus=1, item=self.item, is_default=1))
+ and self.is_active
+ ):
frappe.db.set(self, "is_default", 1)
else:
frappe.db.set(self, "is_default", 0)
item = frappe.get_doc("Item", self.item)
if item.default_bom == self.name:
- frappe.db.set_value('Item', self.item, 'default_bom', None)
+ frappe.db.set_value("Item", self.item, "default_bom", None)
def clear_operations(self):
if not self.with_operations:
- self.set('operations', [])
+ self.set("operations", [])
+
+ def clear_inspection(self):
+ if not self.inspection_required:
+ self.quality_inspection_template = None
def validate_main_item(self):
- """ Validate main FG item"""
+ """Validate main FG item"""
item = self.get_item_det(self.item)
if not item:
frappe.throw(_("Item {0} does not exist in the system or has expired").format(self.item))
@@ -395,30 +480,34 @@ class BOM(WebsiteGenerator):
ret = frappe.db.get_value("Item", self.item, ["description", "stock_uom", "item_name"])
self.description = ret[0]
self.uom = ret[1]
- self.item_name= ret[2]
+ self.item_name = ret[2]
if not self.quantity:
frappe.throw(_("Quantity should be greater than 0"))
def validate_currency(self):
- if self.rm_cost_as_per == 'Price List':
- price_list_currency = frappe.db.get_value('Price List', self.buying_price_list, 'currency')
+ if self.rm_cost_as_per == "Price List":
+ price_list_currency = frappe.db.get_value("Price List", self.buying_price_list, "currency")
if price_list_currency not in (self.currency, self.company_currency()):
- frappe.throw(_("Currency of the price list {0} must be {1} or {2}")
- .format(self.buying_price_list, self.currency, self.company_currency()))
+ frappe.throw(
+ _("Currency of the price list {0} must be {1} or {2}").format(
+ self.buying_price_list, self.currency, self.company_currency()
+ )
+ )
def update_stock_qty(self):
- for m in self.get('items'):
+ for m in self.get("items"):
if not m.conversion_factor:
- m.conversion_factor = flt(get_conversion_factor(m.item_code, m.uom)['conversion_factor'])
+ m.conversion_factor = flt(get_conversion_factor(m.item_code, m.uom)["conversion_factor"])
if m.uom and m.qty:
- m.stock_qty = flt(m.conversion_factor)*flt(m.qty)
+ m.stock_qty = flt(m.conversion_factor) * flt(m.qty)
if not m.uom and m.stock_uom:
m.uom = m.stock_uom
m.qty = m.stock_qty
def validate_uom_is_interger(self):
from erpnext.utilities.transaction_base import validate_uom_is_integer
+
validate_uom_is_integer(self, "uom", "qty", "BOM Item")
validate_uom_is_integer(self, "stock_uom", "stock_qty", "BOM Item")
@@ -426,23 +515,26 @@ class BOM(WebsiteGenerator):
if self.currency == self.company_currency():
self.conversion_rate = 1
elif self.conversion_rate == 1 or flt(self.conversion_rate) <= 0:
- self.conversion_rate = get_exchange_rate(self.currency, self.company_currency(), args="for_buying")
+ self.conversion_rate = get_exchange_rate(
+ self.currency, self.company_currency(), args="for_buying"
+ )
def set_plc_conversion_rate(self):
if self.rm_cost_as_per in ["Valuation Rate", "Last Purchase Rate"]:
self.plc_conversion_rate = 1
elif not self.plc_conversion_rate and self.price_list_currency:
- self.plc_conversion_rate = get_exchange_rate(self.price_list_currency,
- self.company_currency(), args="for_buying")
+ self.plc_conversion_rate = get_exchange_rate(
+ self.price_list_currency, self.company_currency(), args="for_buying"
+ )
def validate_materials(self):
- """ Validate raw material entries """
+ """Validate raw material entries"""
- if not self.get('items'):
+ if not self.get("items"):
frappe.throw(_("Raw Materials cannot be blank."))
check_list = []
- for m in self.get('items'):
+ for m in self.get("items"):
if m.bom_no:
validate_bom_no(m.item_code, m.bom_no)
if flt(m.qty) <= 0:
@@ -450,13 +542,20 @@ class BOM(WebsiteGenerator):
check_list.append(m)
def check_recursion(self, bom_list=None):
- """ Check whether recursion occurs in any bom"""
+ """Check whether recursion occurs in any bom"""
+
def _throw_error(bom_name):
frappe.throw(_("BOM recursion: {0} cannot be parent or child of {0}").format(bom_name))
bom_list = self.traverse_tree()
- child_items = frappe.get_all('BOM Item', fields=["bom_no", "item_code"],
- filters={'parent': ('in', bom_list), 'parenttype': 'BOM'}) or []
+ child_items = (
+ frappe.get_all(
+ "BOM Item",
+ fields=["bom_no", "item_code"],
+ filters={"parent": ("in", bom_list), "parenttype": "BOM"},
+ )
+ or []
+ )
child_bom = {d.bom_no for d in child_items}
child_items_codes = {d.item_code for d in child_items}
@@ -467,19 +566,26 @@ class BOM(WebsiteGenerator):
if self.item in child_items_codes:
_throw_error(self.item)
- bom_nos = frappe.get_all('BOM Item', fields=["parent"],
- filters={'bom_no': self.name, 'parenttype': 'BOM'}) or []
+ bom_nos = (
+ frappe.get_all(
+ "BOM Item", fields=["parent"], filters={"bom_no": self.name, "parenttype": "BOM"}
+ )
+ or []
+ )
if self.name in {d.parent for d in bom_nos}:
_throw_error(self.name)
def traverse_tree(self, bom_list=None):
def _get_children(bom_no):
- children = frappe.cache().hget('bom_children', bom_no)
+ children = frappe.cache().hget("bom_children", bom_no)
if children is None:
- children = frappe.db.sql_list("""SELECT `bom_no` FROM `tabBOM Item`
- WHERE `parent`=%s AND `bom_no`!='' AND `parenttype`='BOM'""", bom_no)
- frappe.cache().hset('bom_children', bom_no, children)
+ children = frappe.db.sql_list(
+ """SELECT `bom_no` FROM `tabBOM Item`
+ WHERE `parent`=%s AND `bom_no`!='' AND `parenttype`='BOM'""",
+ bom_no,
+ )
+ frappe.cache().hset("bom_children", bom_no, children)
return children
count = 0
@@ -489,7 +595,7 @@ class BOM(WebsiteGenerator):
if self.name not in bom_list:
bom_list.append(self.name)
- while(count < len(bom_list)):
+ while count < len(bom_list):
for child_bom in _get_children(bom_list[count]):
if child_bom not in bom_list:
bom_list.append(child_bom)
@@ -497,19 +603,21 @@ class BOM(WebsiteGenerator):
bom_list.reverse()
return bom_list
- def calculate_cost(self, update_hour_rate = False):
+ def calculate_cost(self, update_hour_rate=False):
"""Calculate bom totals"""
self.calculate_op_cost(update_hour_rate)
self.calculate_rm_cost()
self.calculate_sm_cost()
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
- self.base_total_cost = self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
+ self.base_total_cost = (
+ self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
+ )
- def calculate_op_cost(self, update_hour_rate = False):
+ def calculate_op_cost(self, update_hour_rate=False):
"""Update workstation rate and calculates totals"""
self.operating_cost = 0
self.base_operating_cost = 0
- for d in self.get('operations'):
+ for d in self.get("operations"):
if d.workstation:
self.update_rate_and_time(d, update_hour_rate)
@@ -522,13 +630,14 @@ class BOM(WebsiteGenerator):
self.operating_cost += flt(operating_cost)
self.base_operating_cost += flt(base_operating_cost)
- def update_rate_and_time(self, row, update_hour_rate = False):
+ def update_rate_and_time(self, row, update_hour_rate=False):
if not row.hour_rate or update_hour_rate:
hour_rate = flt(frappe.get_cached_value("Workstation", row.workstation, "hour_rate"))
if hour_rate:
- row.hour_rate = (hour_rate / flt(self.conversion_rate)
- if self.conversion_rate and hour_rate else hour_rate)
+ row.hour_rate = (
+ hour_rate / flt(self.conversion_rate) if self.conversion_rate and hour_rate else hour_rate
+ )
if row.hour_rate and row.time_in_mins:
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
@@ -545,12 +654,13 @@ class BOM(WebsiteGenerator):
total_rm_cost = 0
base_total_rm_cost = 0
- for d in self.get('items'):
+ for d in self.get("items"):
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
d.base_amount = d.amount * flt(self.conversion_rate)
- d.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) \
- / flt(self.quantity, self.precision("quantity"))
+ d.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) / flt(
+ self.quantity, self.precision("quantity")
+ )
total_rm_cost += d.amount
base_total_rm_cost += d.base_amount
@@ -563,49 +673,49 @@ class BOM(WebsiteGenerator):
total_sm_cost = 0
base_total_sm_cost = 0
- for d in self.get('scrap_items'):
- d.base_rate = flt(d.rate, d.precision("rate")) * flt(self.conversion_rate, self.precision("conversion_rate"))
+ for d in self.get("scrap_items"):
+ d.base_rate = flt(d.rate, d.precision("rate")) * flt(
+ self.conversion_rate, self.precision("conversion_rate")
+ )
d.amount = flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty"))
- d.base_amount = flt(d.amount, d.precision("amount")) * flt(self.conversion_rate, self.precision("conversion_rate"))
+ d.base_amount = flt(d.amount, d.precision("amount")) * flt(
+ self.conversion_rate, self.precision("conversion_rate")
+ )
total_sm_cost += d.amount
base_total_sm_cost += d.base_amount
self.scrap_material_cost = total_sm_cost
self.base_scrap_material_cost = base_total_sm_cost
- def update_new_bom(self, old_bom, new_bom, rate):
- for d in self.get("items"):
- if d.bom_no != old_bom: continue
-
- d.bom_no = new_bom
- d.rate = rate
- d.amount = (d.stock_qty or d.qty) * rate
-
def update_exploded_items(self, save=True):
- """ Update Flat BOM, following will be correct data"""
+ """Update Flat BOM, following will be correct data"""
self.get_exploded_items()
self.add_exploded_items(save=save)
def get_exploded_items(self):
- """ Get all raw materials including items from child bom"""
+ """Get all raw materials including items from child bom"""
self.cur_exploded_items = {}
- for d in self.get('items'):
+ for d in self.get("items"):
if d.bom_no:
self.get_child_exploded_items(d.bom_no, d.stock_qty)
elif d.item_code:
- self.add_to_cur_exploded_items(frappe._dict({
- 'item_code' : d.item_code,
- 'item_name' : d.item_name,
- 'operation' : d.operation,
- 'source_warehouse': d.source_warehouse,
- 'description' : d.description,
- 'image' : d.image,
- 'stock_uom' : d.stock_uom,
- 'stock_qty' : flt(d.stock_qty),
- 'rate' : flt(d.base_rate) / (flt(d.conversion_factor) or 1.0),
- 'include_item_in_manufacturing': d.include_item_in_manufacturing,
- 'sourced_by_supplier': d.sourced_by_supplier
- }))
+ self.add_to_cur_exploded_items(
+ frappe._dict(
+ {
+ "item_code": d.item_code,
+ "item_name": d.item_name,
+ "operation": d.operation,
+ "source_warehouse": d.source_warehouse,
+ "description": d.description,
+ "image": d.image,
+ "stock_uom": d.stock_uom,
+ "stock_qty": flt(d.stock_qty),
+ "rate": flt(d.base_rate) / (flt(d.conversion_factor) or 1.0),
+ "include_item_in_manufacturing": d.include_item_in_manufacturing,
+ "sourced_by_supplier": d.sourced_by_supplier,
+ }
+ )
+ )
def company_currency(self):
return erpnext.get_company_currency(self.company)
@@ -617,9 +727,10 @@ class BOM(WebsiteGenerator):
self.cur_exploded_items[args.item_code] = args
def get_child_exploded_items(self, bom_no, stock_qty):
- """ Add all items from Flat BOM of child BOM"""
+ """Add all items from Flat BOM of child BOM"""
# Did not use qty_consumed_per_unit in the query, as it leads to rounding loss
- child_fb_items = frappe.db.sql("""
+ child_fb_items = frappe.db.sql(
+ """
SELECT
bom_item.item_code,
bom_item.item_name,
@@ -637,31 +748,38 @@ class BOM(WebsiteGenerator):
bom_item.parent = bom.name
AND bom.name = %s
AND bom.docstatus = 1
- """, bom_no, as_dict = 1)
+ """,
+ bom_no,
+ as_dict=1,
+ )
for d in child_fb_items:
- self.add_to_cur_exploded_items(frappe._dict({
- 'item_code' : d['item_code'],
- 'item_name' : d['item_name'],
- 'source_warehouse' : d['source_warehouse'],
- 'operation' : d['operation'],
- 'description' : d['description'],
- 'stock_uom' : d['stock_uom'],
- 'stock_qty' : d['qty_consumed_per_unit'] * stock_qty,
- 'rate' : flt(d['rate']),
- 'include_item_in_manufacturing': d.get('include_item_in_manufacturing', 0),
- 'sourced_by_supplier': d.get('sourced_by_supplier', 0)
- }))
+ self.add_to_cur_exploded_items(
+ frappe._dict(
+ {
+ "item_code": d["item_code"],
+ "item_name": d["item_name"],
+ "source_warehouse": d["source_warehouse"],
+ "operation": d["operation"],
+ "description": d["description"],
+ "stock_uom": d["stock_uom"],
+ "stock_qty": d["qty_consumed_per_unit"] * stock_qty,
+ "rate": flt(d["rate"]),
+ "include_item_in_manufacturing": d.get("include_item_in_manufacturing", 0),
+ "sourced_by_supplier": d.get("sourced_by_supplier", 0),
+ }
+ )
+ )
def add_exploded_items(self, save=True):
"Add items to Flat BOM table"
- self.set('exploded_items', [])
+ self.set("exploded_items", [])
if save:
frappe.db.sql("""delete from `tabBOM Explosion Item` where parent=%s""", self.name)
for d in sorted(self.cur_exploded_items, key=itemgetter(0)):
- ch = self.append('exploded_items', {})
+ ch = self.append("exploded_items", {})
for i in self.cur_exploded_items[d].keys():
ch.set(i, self.cur_exploded_items[d][i])
ch.amount = flt(ch.stock_qty) * flt(ch.rate)
@@ -673,10 +791,13 @@ class BOM(WebsiteGenerator):
def validate_bom_links(self):
if not self.is_active:
- act_pbom = frappe.db.sql("""select distinct bom_item.parent from `tabBOM Item` bom_item
+ act_pbom = frappe.db.sql(
+ """select distinct bom_item.parent from `tabBOM Item` bom_item
where bom_item.bom_no = %s and bom_item.docstatus = 1 and bom_item.parenttype='BOM'
and exists (select * from `tabBOM` where name = bom_item.parent
- and docstatus = 1 and is_active = 1)""", self.name)
+ and docstatus = 1 and is_active = 1)""",
+ self.name,
+ )
if act_pbom and act_pbom[0][0]:
frappe.throw(_("Cannot deactivate or cancel BOM as it is linked with other BOMs"))
@@ -685,20 +806,23 @@ class BOM(WebsiteGenerator):
if not self.with_operations:
self.transfer_material_against = "Work Order"
if not self.transfer_material_against and not self.is_new():
- frappe.throw(_("Setting {} is required").format(self.meta.get_label("transfer_material_against")), title=_("Missing value"))
+ frappe.throw(
+ _("Setting {} is required").format(self.meta.get_label("transfer_material_against")),
+ title=_("Missing value"),
+ )
def set_routing_operations(self):
if self.routing and self.with_operations and not self.operations:
self.get_routing()
def validate_operations(self):
- if self.with_operations and not self.get('operations') and self.docstatus == 1:
+ if self.with_operations and not self.get("operations") and self.docstatus == 1:
frappe.throw(_("Operations cannot be left blank"))
if self.with_operations:
for d in self.operations:
if not d.description:
- d.description = frappe.db.get_value('Operation', d.operation, 'description')
+ d.description = frappe.db.get_value("Operation", d.operation, "description")
if not d.batch_size or d.batch_size <= 0:
d.batch_size = 1
@@ -706,24 +830,29 @@ class BOM(WebsiteGenerator):
for item in self.scrap_items:
msg = ""
if item.item_code == self.item and not item.is_process_loss:
- msg = _('Scrap/Loss Item: {0} should have Is Process Loss checked as it is the same as the item to be manufactured or repacked.') \
- .format(frappe.bold(item.item_code))
+ msg = _(
+ "Scrap/Loss Item: {0} should have Is Process Loss checked as it is the same as the item to be manufactured or repacked."
+ ).format(frappe.bold(item.item_code))
elif item.item_code != self.item and item.is_process_loss:
- msg = _('Scrap/Loss Item: {0} should not have Is Process Loss checked as it is different from the item to be manufactured or repacked') \
- .format(frappe.bold(item.item_code))
+ msg = _(
+ "Scrap/Loss Item: {0} should not have Is Process Loss checked as it is different from the item to be manufactured or repacked"
+ ).format(frappe.bold(item.item_code))
must_be_whole_number = frappe.get_value("UOM", item.stock_uom, "must_be_whole_number")
if item.is_process_loss and must_be_whole_number:
- msg = _("Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item as {1} is a whole UOM.") \
- .format(frappe.bold(item.item_code), frappe.bold(item.stock_uom))
+ msg = _(
+ "Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item as {1} is a whole UOM."
+ ).format(frappe.bold(item.item_code), frappe.bold(item.stock_uom))
if item.is_process_loss and (item.stock_qty >= self.quantity):
- msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity.") \
- .format(frappe.bold(item.item_code))
+ msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity.").format(
+ frappe.bold(item.item_code)
+ )
if item.is_process_loss and (item.rate > 0):
- msg = _("Scrap/Loss Item: {0} should have Rate set to 0 because Is Process Loss is checked.") \
- .format(frappe.bold(item.item_code))
+ msg = _(
+ "Scrap/Loss Item: {0} should have Rate set to 0 because Is Process Loss is checked."
+ ).format(frappe.bold(item.item_code))
if msg:
frappe.throw(msg, title=_("Note"))
@@ -732,42 +861,48 @@ class BOM(WebsiteGenerator):
"""Get a complete tree representation preserving order of child items."""
return BOMTree(self.name)
+
def get_bom_item_rate(args, bom_doc):
- if bom_doc.rm_cost_as_per == 'Valuation Rate':
+ if bom_doc.rm_cost_as_per == "Valuation Rate":
rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1)
- elif bom_doc.rm_cost_as_per == 'Last Purchase Rate':
- rate = ( flt(args.get('last_purchase_rate')) \
- or frappe.db.get_value("Item", args['item_code'], "last_purchase_rate")) \
- * (args.get("conversion_factor") or 1)
+ elif bom_doc.rm_cost_as_per == "Last Purchase Rate":
+ rate = (
+ flt(args.get("last_purchase_rate"))
+ or flt(frappe.db.get_value("Item", args["item_code"], "last_purchase_rate"))
+ ) * (args.get("conversion_factor") or 1)
elif bom_doc.rm_cost_as_per == "Price List":
if not bom_doc.buying_price_list:
frappe.throw(_("Please select Price List"))
- bom_args = frappe._dict({
- "doctype": "BOM",
- "price_list": bom_doc.buying_price_list,
- "qty": args.get("qty") or 1,
- "uom": args.get("uom") or args.get("stock_uom"),
- "stock_uom": args.get("stock_uom"),
- "transaction_type": "buying",
- "company": bom_doc.company,
- "currency": bom_doc.currency,
- "conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function
- "conversion_factor": args.get("conversion_factor") or 1,
- "plc_conversion_rate": 1,
- "ignore_party": True,
- "ignore_conversion_rate": True
- })
+ bom_args = frappe._dict(
+ {
+ "doctype": "BOM",
+ "price_list": bom_doc.buying_price_list,
+ "qty": args.get("qty") or 1,
+ "uom": args.get("uom") or args.get("stock_uom"),
+ "stock_uom": args.get("stock_uom"),
+ "transaction_type": "buying",
+ "company": bom_doc.company,
+ "currency": bom_doc.currency,
+ "conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function
+ "conversion_factor": args.get("conversion_factor") or 1,
+ "plc_conversion_rate": 1,
+ "ignore_party": True,
+ "ignore_conversion_rate": True,
+ }
+ )
item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
price_list_data = get_price_list_rate(bom_args, item_doc)
rate = price_list_data.price_list_rate
- return rate
+ return flt(rate)
+
def get_valuation_rate(args):
- """ Get weighted average of valuation rate from all warehouses """
+ """Get weighted average of valuation rate from all warehouses"""
total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0
- item_bins = frappe.db.sql("""
+ item_bins = frappe.db.sql(
+ """
select
bin.actual_qty, bin.stock_value
from
@@ -776,33 +911,48 @@ def get_valuation_rate(args):
bin.item_code=%(item)s
and bin.warehouse = warehouse.name
and warehouse.company=%(company)s""",
- {"item": args['item_code'], "company": args['company']}, as_dict=1)
+ {"item": args["item_code"], "company": args["company"]},
+ as_dict=1,
+ )
for d in item_bins:
total_qty += flt(d.actual_qty)
total_value += flt(d.stock_value)
if total_qty:
- valuation_rate = total_value / total_qty
+ valuation_rate = total_value / total_qty
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`
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
if not valuation_rate:
- valuation_rate = frappe.db.get_value("Item", args['item_code'], "valuation_rate")
+ valuation_rate = frappe.db.get_value("Item", args["item_code"], "valuation_rate")
return flt(valuation_rate)
+
def get_list_context(context):
context.title = _("Bill of Materials")
# context.introduction = _('Boms')
-def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_items=0, include_non_stock_items=False, fetch_qty_in_stock_uom=True):
+
+def get_bom_items_as_dict(
+ bom,
+ company,
+ qty=1,
+ fetch_exploded=1,
+ fetch_scrap_items=0,
+ include_non_stock_items=False,
+ fetch_qty_in_stock_uom=True,
+):
item_dict = {}
# Did not use qty_consumed_per_unit in the query, as it leads to rounding loss
@@ -839,30 +989,40 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite
is_stock_item = 0 if include_non_stock_items else 1
if cint(fetch_exploded):
- query = query.format(table="BOM Explosion Item",
+ query = query.format(
+ table="BOM Explosion Item",
where_conditions="",
is_stock_item=is_stock_item,
qty_field="stock_qty",
- select_columns = """, bom_item.source_warehouse, bom_item.operation,
+ select_columns=""", bom_item.source_warehouse, bom_item.operation,
bom_item.include_item_in_manufacturing, bom_item.description, bom_item.rate, bom_item.sourced_by_supplier,
- (Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""")
-
- items = frappe.db.sql(query, { "parent": bom, "qty": qty, "bom": bom, "company": company }, as_dict=True)
- elif fetch_scrap_items:
- query = query.format(
- table="BOM Scrap Item", where_conditions="",
- select_columns=", bom_item.idx, item.description, is_process_loss",
- is_stock_item=is_stock_item, qty_field="stock_qty"
+ (Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""",
)
- items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True)
+ items = frappe.db.sql(
+ query, {"parent": bom, "qty": qty, "bom": bom, "company": company}, as_dict=True
+ )
+ elif fetch_scrap_items:
+ query = query.format(
+ table="BOM Scrap Item",
+ where_conditions="",
+ select_columns=", item.description, is_process_loss",
+ is_stock_item=is_stock_item,
+ qty_field="stock_qty",
+ )
+
+ items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True)
else:
- query = query.format(table="BOM Item", where_conditions="", is_stock_item=is_stock_item,
+ query = query.format(
+ table="BOM Item",
+ where_conditions="",
+ is_stock_item=is_stock_item,
qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty",
- select_columns = """, bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse,
- bom_item.idx, bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier,
- bom_item.description, bom_item.base_rate as rate """)
- items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True)
+ select_columns=""", bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse,
+ bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier,
+ bom_item.description, bom_item.base_rate as rate """,
+ )
+ items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True)
for item in items:
if item.item_code in item_dict:
@@ -871,21 +1031,28 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite
item_dict[item.item_code] = item
for item, item_details in item_dict.items():
- for d in [["Account", "expense_account", "stock_adjustment_account"],
- ["Cost Center", "cost_center", "cost_center"], ["Warehouse", "default_warehouse", ""]]:
- company_in_record = frappe.db.get_value(d[0], item_details.get(d[1]), "company")
- if not item_details.get(d[1]) or (company_in_record and company != company_in_record):
- item_dict[item][d[1]] = frappe.get_cached_value('Company', company, d[2]) if d[2] else None
+ for d in [
+ ["Account", "expense_account", "stock_adjustment_account"],
+ ["Cost Center", "cost_center", "cost_center"],
+ ["Warehouse", "default_warehouse", ""],
+ ]:
+ company_in_record = frappe.db.get_value(d[0], item_details.get(d[1]), "company")
+ if not item_details.get(d[1]) or (company_in_record and company != company_in_record):
+ item_dict[item][d[1]] = frappe.get_cached_value("Company", company, d[2]) if d[2] else None
return item_dict
+
@frappe.whitelist()
def get_bom_items(bom, company, qty=1, fetch_exploded=1):
- items = get_bom_items_as_dict(bom, company, qty, fetch_exploded, include_non_stock_items=True).values()
+ items = get_bom_items_as_dict(
+ bom, company, qty, fetch_exploded, include_non_stock_items=True
+ ).values()
items = list(items)
- items.sort(key = functools.cmp_to_key(lambda a, b: a.item_code > b.item_code and 1 or -1))
+ items.sort(key=functools.cmp_to_key(lambda a, b: a.item_code > b.item_code and 1 or -1))
return items
+
def validate_bom_no(item, bom_no):
"""Validate BOM No of sub-contracted items"""
bom = frappe.get_doc("BOM", bom_no)
@@ -897,21 +1064,24 @@ def validate_bom_no(item, bom_no):
if item:
rm_item_exists = False
for d in bom.items:
- if (d.item_code.lower() == item.lower()):
+ if d.item_code.lower() == item.lower():
rm_item_exists = True
for d in bom.scrap_items:
- if (d.item_code.lower() == item.lower()):
+ if d.item_code.lower() == item.lower():
rm_item_exists = True
- if bom.item.lower() == item.lower() or \
- bom.item.lower() == cstr(frappe.db.get_value("Item", item, "variant_of")).lower():
- rm_item_exists = True
+ if (
+ bom.item.lower() == item.lower()
+ or bom.item.lower() == cstr(frappe.db.get_value("Item", item, "variant_of")).lower()
+ ):
+ rm_item_exists = True
if not rm_item_exists:
frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item))
+
@frappe.whitelist()
def get_children(doctype, parent=None, is_root=False, **filters):
- if not parent or parent=="BOM":
- frappe.msgprint(_('Please select a BOM'))
+ if not parent or parent == "BOM":
+ frappe.msgprint(_("Please select a BOM"))
return
if parent:
@@ -921,38 +1091,45 @@ def get_children(doctype, parent=None, is_root=False, **filters):
bom_doc = frappe.get_cached_doc("BOM", frappe.form_dict.parent)
frappe.has_permission("BOM", doc=bom_doc, throw=True)
- bom_items = frappe.get_all('BOM Item',
- fields=['item_code', 'bom_no as value', 'stock_qty'],
- filters=[['parent', '=', frappe.form_dict.parent]],
- order_by='idx')
+ bom_items = frappe.get_all(
+ "BOM Item",
+ fields=["item_code", "bom_no as value", "stock_qty"],
+ filters=[["parent", "=", frappe.form_dict.parent]],
+ order_by="idx",
+ )
- item_names = tuple(d.get('item_code') for d in bom_items)
+ item_names = tuple(d.get("item_code") for d in bom_items)
- items = frappe.get_list('Item',
- fields=['image', 'description', 'name', 'stock_uom', 'item_name', 'is_sub_contracted_item'],
- filters=[['name', 'in', item_names]]) # to get only required item dicts
+ items = frappe.get_list(
+ "Item",
+ fields=["image", "description", "name", "stock_uom", "item_name", "is_sub_contracted_item"],
+ filters=[["name", "in", item_names]],
+ ) # to get only required item dicts
for bom_item in bom_items:
# extend bom_item dict with respective item dict
bom_item.update(
# returns an item dict from items list which matches with item_code
- next(item for item in items if item.get('name')
- == bom_item.get('item_code'))
+ next(item for item in items if item.get("name") == bom_item.get("item_code"))
)
bom_item.parent_bom_qty = bom_doc.quantity
- bom_item.expandable = 0 if bom_item.value in ('', None) else 1
+ bom_item.expandable = 0 if bom_item.value in ("", None) else 1
bom_item.image = frappe.db.escape(bom_item.image)
return bom_items
+
def get_boms_in_bottom_up_order(bom_no=None):
def _get_parent(bom_no):
- return frappe.db.sql_list("""
+ return frappe.db.sql_list(
+ """
select distinct bom_item.parent from `tabBOM Item` bom_item
where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM'
and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1)
- """, bom_no)
+ """,
+ bom_no,
+ )
count = 0
bom_list = []
@@ -960,12 +1137,14 @@ def get_boms_in_bottom_up_order(bom_no=None):
bom_list.append(bom_no)
else:
# get all leaf BOMs
- bom_list = frappe.db.sql_list("""select name from `tabBOM` bom
+ bom_list = frappe.db.sql_list(
+ """select name from `tabBOM` bom
where docstatus=1 and is_active=1
and not exists(select bom_no from `tabBOM Item`
- where parent=bom.name and ifnull(bom_no, '')!='')""")
+ where parent=bom.name and ifnull(bom_no, '')!='')"""
+ )
- while(count < len(bom_list)):
+ while count < len(bom_list):
for child_bom in _get_parent(bom_list[count]):
if child_bom not in bom_list:
bom_list.append(child_bom)
@@ -973,69 +1152,92 @@ def get_boms_in_bottom_up_order(bom_no=None):
return bom_list
+
def add_additional_cost(stock_entry, work_order):
# Add non stock items cost in the additional cost
stock_entry.additional_costs = []
- expenses_included_in_valuation = frappe.get_cached_value("Company", work_order.company,
- "expenses_included_in_valuation")
+ expenses_included_in_valuation = frappe.get_cached_value(
+ "Company", work_order.company, "expenses_included_in_valuation"
+ )
add_non_stock_items_cost(stock_entry, work_order, expenses_included_in_valuation)
add_operations_cost(stock_entry, work_order, expenses_included_in_valuation)
+
def add_non_stock_items_cost(stock_entry, work_order, expense_account):
- bom = frappe.get_doc('BOM', work_order.bom_no)
- table = 'exploded_items' if work_order.get('use_multi_level_bom') else 'items'
+ bom = frappe.get_doc("BOM", work_order.bom_no)
+ table = "exploded_items" if work_order.get("use_multi_level_bom") else "items"
items = {}
for d in bom.get(table):
items.setdefault(d.item_code, d.amount)
- non_stock_items = frappe.get_all('Item',
- fields="name", filters={'name': ('in', list(items.keys())), 'ifnull(is_stock_item, 0)': 0}, as_list=1)
+ non_stock_items = frappe.get_all(
+ "Item",
+ fields="name",
+ filters={"name": ("in", list(items.keys())), "ifnull(is_stock_item, 0)": 0},
+ as_list=1,
+ )
non_stock_items_cost = 0.0
for name in non_stock_items:
- non_stock_items_cost += flt(items.get(name[0])) * flt(stock_entry.fg_completed_qty) / flt(bom.quantity)
+ non_stock_items_cost += (
+ flt(items.get(name[0])) * flt(stock_entry.fg_completed_qty) / flt(bom.quantity)
+ )
if non_stock_items_cost:
- stock_entry.append('additional_costs', {
- 'expense_account': expense_account,
- 'description': _("Non stock items"),
- 'amount': non_stock_items_cost
- })
+ stock_entry.append(
+ "additional_costs",
+ {
+ "expense_account": expense_account,
+ "description": _("Non stock items"),
+ "amount": non_stock_items_cost,
+ },
+ )
+
def add_operations_cost(stock_entry, work_order=None, expense_account=None):
from erpnext.stock.doctype.stock_entry.stock_entry import get_operating_cost_per_unit
+
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
if operating_cost_per_unit:
- stock_entry.append('additional_costs', {
- "expense_account": expense_account,
- "description": _("Operating Cost as per Work Order / BOM"),
- "amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty)
- })
+ stock_entry.append(
+ "additional_costs",
+ {
+ "expense_account": expense_account,
+ "description": _("Operating Cost as per Work Order / BOM"),
+ "amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
+ },
+ )
if work_order and work_order.additional_operating_cost and work_order.qty:
- additional_operating_cost_per_unit = \
- flt(work_order.additional_operating_cost) / flt(work_order.qty)
+ additional_operating_cost_per_unit = flt(work_order.additional_operating_cost) / flt(
+ work_order.qty
+ )
if additional_operating_cost_per_unit:
- stock_entry.append('additional_costs', {
- "expense_account": expense_account,
- "description": "Additional Operating Cost",
- "amount": additional_operating_cost_per_unit * flt(stock_entry.fg_completed_qty)
- })
+ stock_entry.append(
+ "additional_costs",
+ {
+ "expense_account": expense_account,
+ "description": "Additional Operating Cost",
+ "amount": additional_operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
+ },
+ )
+
@frappe.whitelist()
def get_bom_diff(bom1, bom2):
from frappe.model import table_fields
if bom1 == bom2:
- frappe.throw(_("BOM 1 {0} and BOM 2 {1} should not be same")
- .format(frappe.bold(bom1), frappe.bold(bom2)))
+ frappe.throw(
+ _("BOM 1 {0} and BOM 2 {1} should not be same").format(frappe.bold(bom1), frappe.bold(bom2))
+ )
- doc1 = frappe.get_doc('BOM', bom1)
- doc2 = frappe.get_doc('BOM', bom2)
+ doc1 = frappe.get_doc("BOM", bom1)
+ doc2 = frappe.get_doc("BOM", bom2)
out = get_diff(doc1, doc2)
out.row_changed = []
@@ -1045,10 +1247,10 @@ def get_bom_diff(bom1, bom2):
meta = doc1.meta
identifiers = {
- 'operations': 'operation',
- 'items': 'item_code',
- 'scrap_items': 'item_code',
- 'exploded_items': 'item_code'
+ "operations": "operation",
+ "items": "item_code",
+ "scrap_items": "item_code",
+ "exploded_items": "item_code",
}
for df in meta.fields:
@@ -1079,6 +1281,7 @@ def get_bom_diff(bom1, bom2):
return out
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def item_query(doctype, txt, searchfield, start, page_len, filters):
@@ -1088,25 +1291,28 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
order_by = "idx desc, name, item_name"
fields = ["name", "item_group", "item_name", "description"]
- fields.extend([field for field in searchfields
- if not field in ["name", "item_group", "description"]])
+ fields.extend(
+ [field for field in searchfields if not field in ["name", "item_group", "description"]]
+ )
- searchfields = searchfields + [field for field in [searchfield or "name", "item_code", "item_group", "item_name"]
- if not field in searchfields]
+ searchfields = searchfields + [
+ field
+ for field in [searchfield or "name", "item_code", "item_group", "item_name"]
+ if not field in searchfields
+ ]
- query_filters = {
- "disabled": 0,
- "ifnull(end_of_life, '5050-50-50')": (">", today())
- }
+ query_filters = {"disabled": 0, "ifnull(end_of_life, '5050-50-50')": (">", today())}
or_cond_filters = {}
if txt:
for s_field in searchfields:
or_cond_filters[s_field] = ("like", "%{0}%".format(txt))
- barcodes = frappe.get_all("Item Barcode",
+ barcodes = frappe.get_all(
+ "Item Barcode",
fields=["distinct parent as item_code"],
- filters = {"barcode": ("like", "%{0}%".format(txt))})
+ filters={"barcode": ("like", "%{0}%".format(txt))},
+ )
barcodes = [d.item_code for d in barcodes]
if barcodes:
@@ -1120,10 +1326,17 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if filters and filters.get("is_stock_item"):
query_filters["is_stock_item"] = 1
- return frappe.get_list("Item",
- fields = fields, filters=query_filters,
- or_filters = or_cond_filters, order_by=order_by,
- limit_start=start, limit_page_length=page_len, as_list=1)
+ return frappe.get_list(
+ "Item",
+ fields=fields,
+ filters=query_filters,
+ or_filters=or_cond_filters,
+ order_by=order_by,
+ limit_start=start,
+ limit_page_length=page_len,
+ as_list=1,
+ )
+
@frappe.whitelist()
def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None):
@@ -1134,28 +1347,31 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None):
doc.quantity = 1
item_data = get_item_details(item)
- doc.update({
- "item_name": item_data.item_name,
- "description": item_data.description,
- "uom": item_data.stock_uom,
- "allow_alternative_item": item_data.allow_alternative_item
- })
+ doc.update(
+ {
+ "item_name": item_data.item_name,
+ "description": item_data.description,
+ "uom": item_data.stock_uom,
+ "allow_alternative_item": item_data.allow_alternative_item,
+ }
+ )
add_variant_item(variant_items, doc, source_name)
- doc = get_mapped_doc('BOM', source_name, {
- 'BOM': {
- 'doctype': 'BOM',
- 'validation': {
- 'docstatus': ['=', 1]
- }
+ doc = get_mapped_doc(
+ "BOM",
+ source_name,
+ {
+ "BOM": {"doctype": "BOM", "validation": {"docstatus": ["=", 1]}},
+ "BOM Item": {
+ "doctype": "BOM Item",
+ # stop get_mapped_doc copying parent bom_no to children
+ "field_no_map": ["bom_no"],
+ "condition": lambda doc: doc.has_variants == 0,
+ },
},
- 'BOM Item': {
- 'doctype': 'BOM Item',
- # stop get_mapped_doc copying parent bom_no to children
- 'field_no_map': ['bom_no'],
- 'condition': lambda doc: doc.has_variants == 0
- },
- }, target_doc, postprocess)
+ target_doc,
+ postprocess,
+ )
return doc
diff --git a/erpnext/manufacturing/doctype/bom/bom_dashboard.py b/erpnext/manufacturing/doctype/bom/bom_dashboard.py
index 9b8f6bff095..d8a810bd0ad 100644
--- a/erpnext/manufacturing/doctype/bom/bom_dashboard.py
+++ b/erpnext/manufacturing/doctype/bom/bom_dashboard.py
@@ -1,30 +1,30 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'bom_no',
- 'non_standard_fieldnames': {
- 'Item': 'default_bom',
- 'Purchase Order': 'bom',
- 'Purchase Receipt': 'bom',
- 'Purchase Invoice': 'bom'
+ "fieldname": "bom_no",
+ "non_standard_fieldnames": {
+ "Item": "default_bom",
+ "Purchase Order": "bom",
+ "Purchase Receipt": "bom",
+ "Purchase Invoice": "bom",
},
- 'transactions': [
+ "transactions": [
+ {"label": _("Stock"), "items": ["Item", "Stock Entry", "Quality Inspection"]},
+ {"label": _("Manufacture"), "items": ["BOM", "Work Order", "Job Card"]},
{
- 'label': _('Stock'),
- 'items': ['Item', 'Stock Entry', 'Quality Inspection']
+ "label": _("Subcontract"),
+ "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"],
},
- {
- 'label': _('Manufacture'),
- 'items': ['BOM', 'Work Order', 'Job Card']
- },
- {
- 'label': _('Subcontract'),
- 'items': ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice']
- }
],
- 'disable_create_buttons': ["Item", "Purchase Order", "Purchase Receipt",
- "Purchase Invoice", "Job Card", "Stock Entry", "BOM"]
+ "disable_create_buttons": [
+ "Item",
+ "Purchase Order",
+ "Purchase Receipt",
+ "Purchase Invoice",
+ "Job Card",
+ "Stock Entry",
+ "BOM",
+ ],
}
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index bfafacdfb57..455e3f9d9c3 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -6,7 +6,7 @@ from collections import deque
from functools import partial
import frappe
-from frappe.test_runner import make_test_records
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import cstr, flt
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -17,27 +17,28 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
create_stock_reconciliation,
)
from erpnext.tests.test_subcontracting import set_backflush_based_on
-from erpnext.tests.utils import ERPNextTestCase
-test_records = frappe.get_test_records('BOM')
+test_records = frappe.get_test_records("BOM")
+test_dependencies = ["Item", "Quality Inspection Template"]
-class TestBOM(ERPNextTestCase):
- def setUp(self):
- if not frappe.get_value('Item', '_Test Item'):
- make_test_records('Item')
+class TestBOM(FrappeTestCase):
def test_get_items(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
- items_dict = get_bom_items_as_dict(bom=get_default_bom(),
- company="_Test Company", qty=1, fetch_exploded=0)
+
+ items_dict = get_bom_items_as_dict(
+ bom=get_default_bom(), company="_Test Company", qty=1, fetch_exploded=0
+ )
self.assertTrue(test_records[2]["items"][0]["item_code"] in items_dict)
self.assertTrue(test_records[2]["items"][1]["item_code"] in items_dict)
self.assertEqual(len(items_dict.values()), 2)
def test_get_items_exploded(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
- items_dict = get_bom_items_as_dict(bom=get_default_bom(),
- company="_Test Company", qty=1, fetch_exploded=1)
+
+ items_dict = get_bom_items_as_dict(
+ bom=get_default_bom(), company="_Test Company", qty=1, fetch_exploded=1
+ )
self.assertTrue(test_records[2]["items"][0]["item_code"] in items_dict)
self.assertFalse(test_records[2]["items"][1]["item_code"] in items_dict)
self.assertTrue(test_records[0]["items"][0]["item_code"] in items_dict)
@@ -46,13 +47,14 @@ class TestBOM(ERPNextTestCase):
def test_get_items_list(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items
+
self.assertEqual(len(get_bom_items(bom=get_default_bom(), company="_Test Company")), 3)
def test_default_bom(self):
def _get_default_bom_in_item():
return cstr(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom"))
- bom = frappe.get_doc("BOM", {"item":"_Test FG Item 2", "is_default": 1})
+ bom = frappe.get_doc("BOM", {"item": "_Test FG Item 2", "is_default": 1})
self.assertEqual(_get_default_bom_in_item(), bom.name)
bom.is_active = 0
@@ -60,28 +62,33 @@ class TestBOM(ERPNextTestCase):
self.assertEqual(_get_default_bom_in_item(), "")
bom.is_active = 1
- bom.is_default=1
+ bom.is_default = 1
bom.save()
self.assertTrue(_get_default_bom_in_item(), bom.name)
def test_update_bom_cost_in_all_boms(self):
# get current rate for '_Test Item 2'
- rm_rate = frappe.db.sql("""select rate from `tabBOM Item`
+ rm_rate = frappe.db.sql(
+ """select rate from `tabBOM Item`
where parent='BOM-_Test Item Home Desktop Manufactured-001'
- and item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""")
+ and item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'"""
+ )
rm_rate = rm_rate[0][0] if rm_rate else 0
# Reset item valuation rate
- reset_item_valuation_rate(item_code='_Test Item 2', qty=200, rate=rm_rate + 10)
+ reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_rate + 10)
# update cost of all BOMs based on latest valuation rate
update_cost()
# check if new valuation rate updated in all BOMs
- for d in frappe.db.sql("""select rate from `tabBOM Item`
- where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""", as_dict=1):
- self.assertEqual(d.rate, rm_rate + 10)
+ for d in frappe.db.sql(
+ """select rate from `tabBOM Item`
+ where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""",
+ as_dict=1,
+ ):
+ self.assertEqual(d.rate, rm_rate + 10)
def test_bom_cost(self):
bom = frappe.copy_doc(test_records[2])
@@ -96,7 +103,9 @@ class TestBOM(ERPNextTestCase):
for row in bom.items:
raw_material_cost += row.amount
- base_raw_material_cost = raw_material_cost * flt(bom.conversion_rate, bom.precision("conversion_rate"))
+ base_raw_material_cost = raw_material_cost * flt(
+ bom.conversion_rate, bom.precision("conversion_rate")
+ )
base_op_cost = op_cost * flt(bom.conversion_rate, bom.precision("conversion_rate"))
# test amounts in selected currency, almostEqual checks for 7 digits by default
@@ -124,14 +133,15 @@ class TestBOM(ERPNextTestCase):
for op_row in bom.operations:
self.assertAlmostEqual(op_row.cost_per_unit, op_row.operating_cost / 2)
- self.assertAlmostEqual(bom.operating_cost, op_cost/2)
+ self.assertAlmostEqual(bom.operating_cost, op_cost / 2)
bom.delete()
def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self):
frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1)
for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)):
- frappe.db.sql("delete from `tabItem Price` where price_list='_Test Price List' and item_code=%s",
- item_code)
+ frappe.db.sql(
+ "delete from `tabItem Price` where price_list='_Test Price List' and item_code=%s", item_code
+ )
item_price = frappe.new_doc("Item Price")
item_price.price_list = "_Test Price List"
item_price.item_code = item_code
@@ -146,7 +156,7 @@ class TestBOM(ERPNextTestCase):
bom.items[0].conversion_factor = 5
bom.insert()
- bom.update_cost(update_hour_rate = False)
+ bom.update_cost(update_hour_rate=False)
# test amounts in selected currency
self.assertEqual(bom.items[0].rate, 300)
@@ -171,11 +181,12 @@ class TestBOM(ERPNextTestCase):
bom.insert()
reset_item_valuation_rate(
- item_code='_Test Item',
- warehouse_list=frappe.get_all("Warehouse",
- {"is_group":0, "company": bom.company}, pluck="name"),
+ item_code="_Test Item",
+ warehouse_list=frappe.get_all(
+ "Warehouse", {"is_group": 0, "company": bom.company}, pluck="name"
+ ),
qty=200,
- rate=200
+ rate=200,
)
bom.update_cost()
@@ -184,77 +195,72 @@ class TestBOM(ERPNextTestCase):
def test_subcontractor_sourced_item(self):
item_code = "_Test Subcontracted FG Item 1"
- set_backflush_based_on('Material Transferred for Subcontract')
+ set_backflush_based_on("Material Transferred for Subcontract")
- if not frappe.db.exists('Item', item_code):
- make_item(item_code, {
- 'is_stock_item': 1,
- 'is_sub_contracted_item': 1,
- 'stock_uom': 'Nos'
- })
+ if not frappe.db.exists("Item", item_code):
+ make_item(item_code, {"is_stock_item": 1, "is_sub_contracted_item": 1, "stock_uom": "Nos"})
- if not frappe.db.exists('Item', "Test Extra Item 1"):
- make_item("Test Extra Item 1", {
- 'is_stock_item': 1,
- 'stock_uom': 'Nos'
- })
+ if not frappe.db.exists("Item", "Test Extra Item 1"):
+ make_item("Test Extra Item 1", {"is_stock_item": 1, "stock_uom": "Nos"})
- if not frappe.db.exists('Item', "Test Extra Item 2"):
- make_item("Test Extra Item 2", {
- 'is_stock_item': 1,
- 'stock_uom': 'Nos'
- })
+ if not frappe.db.exists("Item", "Test Extra Item 2"):
+ make_item("Test Extra Item 2", {"is_stock_item": 1, "stock_uom": "Nos"})
- if not frappe.db.exists('Item', "Test Extra Item 3"):
- make_item("Test Extra Item 3", {
- 'is_stock_item': 1,
- 'stock_uom': 'Nos'
- })
- bom = frappe.get_doc({
- 'doctype': 'BOM',
- 'is_default': 1,
- 'item': item_code,
- 'currency': 'USD',
- 'quantity': 1,
- 'company': '_Test Company'
- })
+ if not frappe.db.exists("Item", "Test Extra Item 3"):
+ make_item("Test Extra Item 3", {"is_stock_item": 1, "stock_uom": "Nos"})
+ bom = frappe.get_doc(
+ {
+ "doctype": "BOM",
+ "is_default": 1,
+ "item": item_code,
+ "currency": "USD",
+ "quantity": 1,
+ "company": "_Test Company",
+ }
+ )
for item in ["Test Extra Item 1", "Test Extra Item 2"]:
- item_doc = frappe.get_doc('Item', item)
+ item_doc = frappe.get_doc("Item", item)
- bom.append('items', {
- 'item_code': item,
- 'qty': 1,
- 'uom': item_doc.stock_uom,
- 'stock_uom': item_doc.stock_uom,
- 'rate': item_doc.valuation_rate
- })
+ bom.append(
+ "items",
+ {
+ "item_code": item,
+ "qty": 1,
+ "uom": item_doc.stock_uom,
+ "stock_uom": item_doc.stock_uom,
+ "rate": item_doc.valuation_rate,
+ },
+ )
- bom.append('items', {
- 'item_code': "Test Extra Item 3",
- 'qty': 1,
- 'uom': item_doc.stock_uom,
- 'stock_uom': item_doc.stock_uom,
- 'rate': 0,
- 'sourced_by_supplier': 1
- })
+ bom.append(
+ "items",
+ {
+ "item_code": "Test Extra Item 3",
+ "qty": 1,
+ "uom": item_doc.stock_uom,
+ "stock_uom": item_doc.stock_uom,
+ "rate": 0,
+ "sourced_by_supplier": 1,
+ },
+ )
bom.insert(ignore_permissions=True)
bom.update_cost()
bom.submit()
# test that sourced_by_supplier rate is zero even after updating cost
self.assertEqual(bom.items[2].rate, 0)
# test in Purchase Order sourced_by_supplier is not added to Supplied Item
- po = create_purchase_order(item_code=item_code, qty=1,
- is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
+ po = create_purchase_order(
+ item_code=item_code, qty=1, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
+ )
bom_items = sorted([d.item_code for d in bom.items if d.sourced_by_supplier != 1])
supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
self.assertEqual(bom_items, supplied_items)
-
def test_bom_recursion_1st_level(self):
"""BOM should not allow BOM item again in child"""
item_code = "_Test BOM Recursion"
- make_item(item_code, {'is_stock_item': 1})
+ make_item(item_code, {"is_stock_item": 1})
bom = frappe.new_doc("BOM")
bom.item = item_code
@@ -268,8 +274,8 @@ class TestBOM(ERPNextTestCase):
def test_bom_recursion_transitive(self):
item1 = "_Test BOM Recursion"
item2 = "_Test BOM Recursion 2"
- make_item(item1, {'is_stock_item': 1})
- make_item(item2, {'is_stock_item': 1})
+ make_item(item1, {"is_stock_item": 1})
+ make_item(item2, {"is_stock_item": 1})
bom1 = frappe.new_doc("BOM")
bom1.item = item1
@@ -327,7 +333,10 @@ class TestBOM(ERPNextTestCase):
def test_bom_tree_representation(self):
bom_tree = {
"Assembly": {
- "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
+ "SubAssembly1": {
+ "ChildPart1": {},
+ "ChildPart2": {},
+ },
"SubAssembly2": {"ChildPart3": {}},
"SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}},
"ChildPart5": {},
@@ -338,7 +347,7 @@ class TestBOM(ERPNextTestCase):
parent_bom = create_nested_bom(bom_tree, prefix="")
created_tree = parent_bom.get_tree_representation()
- reqd_order = level_order_traversal(bom_tree)[1:] # skip first item
+ reqd_order = level_order_traversal(bom_tree)[1:] # skip first item
created_order = created_tree.level_order_traversal()
self.assertEqual(len(reqd_order), len(created_order))
@@ -347,17 +356,28 @@ class TestBOM(ERPNextTestCase):
self.assertEqual(reqd_item, created_item.item_code)
def test_bom_item_query(self):
- query = partial(item_query, doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters={"is_stock_item": 1})
+ query = partial(
+ item_query,
+ doctype="Item",
+ txt="",
+ searchfield="name",
+ start=0,
+ page_len=20,
+ filters={"is_stock_item": 1},
+ )
test_items = query(txt="_Test")
filtered = query(txt="_Test Item 2")
- self.assertNotEqual(len(test_items), len(filtered), msg="Item filtering showing excessive results")
+ self.assertNotEqual(
+ len(test_items), len(filtered), msg="Item filtering showing excessive results"
+ )
self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results")
-
def test_valid_transfer_defaults(self):
- bom_with_op = frappe.db.get_value("BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1})
+ bom_with_op = frappe.db.get_value(
+ "BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1}
+ )
bom = frappe.copy_doc(frappe.get_doc("BOM", bom_with_op), ignore_no_copy=False)
# test defaults
@@ -385,10 +405,107 @@ class TestBOM(ERPNextTestCase):
self.assertEqual(bom.transfer_material_against, "Work Order")
bom.delete()
+ def test_bom_name_length(self):
+ """test >140 char names"""
+ bom_tree = {"x" * 140: {" ".join(["abc"] * 35): {}}}
+ create_nested_bom(bom_tree, prefix="")
+
+ def test_version_index(self):
+
+ bom = frappe.new_doc("BOM")
+
+ version_index_test_cases = [
+ (1, []),
+ (1, ["BOM#XYZ"]),
+ (2, ["BOM/ITEM/001"]),
+ (2, ["BOM-ITEM-001"]),
+ (3, ["BOM-ITEM-001", "BOM-ITEM-002"]),
+ (4, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-003"]),
+ ]
+
+ for expected_index, existing_boms in version_index_test_cases:
+ with self.subTest():
+ self.assertEqual(
+ expected_index,
+ bom.get_next_version_index(existing_boms),
+ msg=f"Incorrect index for {existing_boms}",
+ )
+
+ def test_bom_versioning(self):
+ bom_tree = {frappe.generate_hash(length=10): {frappe.generate_hash(length=10): {}}}
+ bom = create_nested_bom(bom_tree, prefix="")
+ self.assertEqual(int(bom.name.split("-")[-1]), 1)
+ original_bom_name = bom.name
+
+ bom.cancel()
+ bom.reload()
+ self.assertEqual(bom.name, original_bom_name)
+
+ # create a new amendment
+ amendment = frappe.copy_doc(bom)
+ amendment.docstatus = 0
+ amendment.amended_from = bom.name
+
+ amendment.save()
+ amendment.submit()
+ amendment.reload()
+
+ self.assertNotEqual(amendment.name, bom.name)
+ # `origname-001-1` version
+ self.assertEqual(int(amendment.name.split("-")[-1]), 1)
+ self.assertEqual(int(amendment.name.split("-")[-2]), 1)
+
+ # create a new version
+ version = frappe.copy_doc(amendment)
+ version.docstatus = 0
+ version.amended_from = None
+ version.save()
+ self.assertNotEqual(amendment.name, version.name)
+ self.assertEqual(int(version.name.split("-")[-1]), 2)
+
+ def test_clear_inpection_quality(self):
+
+ bom = frappe.copy_doc(test_records[2], ignore_no_copy=True)
+ bom.docstatus = 0
+ bom.is_default = 0
+ bom.quality_inspection_template = "_Test Quality Inspection Template"
+ bom.inspection_required = 1
+ bom.save()
+ bom.reload()
+
+ self.assertEqual(bom.quality_inspection_template, "_Test Quality Inspection Template")
+
+ bom.inspection_required = 0
+ bom.save()
+ bom.reload()
+
+ self.assertEqual(bom.quality_inspection_template, None)
+
+ def test_bom_pricing_based_on_lpp(self):
+ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+
+ parent = frappe.generate_hash(length=10)
+ child = frappe.generate_hash(length=10)
+ bom_tree = {parent: {child: {}}}
+ bom = create_nested_bom(bom_tree, prefix="")
+
+ # add last purchase price
+ make_purchase_receipt(item_code=child, rate=42)
+
+ bom = frappe.copy_doc(bom)
+ bom.docstatus = 0
+ bom.amended_from = None
+ bom.rm_cost_as_per = "Last Purchase Rate"
+ bom.conversion_rate = 1
+ bom.save()
+ bom.submit()
+ self.assertEqual(bom.items[0].rate, 42)
+
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
+
def level_order_traversal(node):
traversal = []
q = deque()
@@ -403,9 +520,9 @@ def level_order_traversal(node):
return traversal
+
def create_nested_bom(tree, prefix="_Test bom "):
- """ Helper function to create a simple nested bom from tree describing item names. (along with required items)
- """
+ """Helper function to create a simple nested bom from tree describing item names. (along with required items)"""
def create_items(bom_tree):
for item_code, subtree in bom_tree.items():
@@ -413,6 +530,7 @@ def create_nested_bom(tree, prefix="_Test bom "):
if not frappe.db.exists("Item", bom_item_code):
frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert()
create_items(subtree)
+
create_items(tree)
def dfs(tree, node):
@@ -434,6 +552,7 @@ def create_nested_bom(tree, prefix="_Test bom "):
bom = frappe.get_doc(doctype="BOM", item=bom_item_code)
for child_item in child_items.keys():
bom.append("items", {"item_code": prefix + child_item})
+ bom.company = "_Test Company"
bom.currency = "INR"
bom.insert()
bom.submit()
@@ -446,10 +565,13 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non
warehouse_list = [warehouse_list]
if not warehouse_list:
- warehouse_list = frappe.db.sql_list("""
+ warehouse_list = frappe.db.sql_list(
+ """
select warehouse from `tabBin`
where item_code=%s and actual_qty > 0
- """, item_code)
+ """,
+ item_code,
+ )
if not warehouse_list:
warehouse_list.append("_Test Warehouse - _TC")
@@ -457,44 +579,51 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non
for warehouse in warehouse_list:
create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=qty, rate=rate)
+
def create_bom_with_process_loss_item(
- fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1):
+ fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1
+):
bom_doc = frappe.new_doc("BOM")
bom_doc.item = fg_item.item_code
bom_doc.quantity = fg_qty
- bom_doc.append("items", {
- "item_code": bom_item.item_code,
- "qty": 1,
- "uom": bom_item.stock_uom,
- "stock_uom": bom_item.stock_uom,
- "rate": 100.0
- })
- bom_doc.append("scrap_items", {
- "item_code": fg_item.item_code,
- "qty": scrap_qty,
- "stock_qty": scrap_qty,
- "uom": fg_item.stock_uom,
- "stock_uom": fg_item.stock_uom,
- "rate": scrap_rate,
- "is_process_loss": is_process_loss
- })
+ bom_doc.append(
+ "items",
+ {
+ "item_code": bom_item.item_code,
+ "qty": 1,
+ "uom": bom_item.stock_uom,
+ "stock_uom": bom_item.stock_uom,
+ "rate": 100.0,
+ },
+ )
+ bom_doc.append(
+ "scrap_items",
+ {
+ "item_code": fg_item.item_code,
+ "qty": scrap_qty,
+ "stock_qty": scrap_qty,
+ "uom": fg_item.stock_uom,
+ "stock_uom": fg_item.stock_uom,
+ "rate": scrap_rate,
+ "is_process_loss": is_process_loss,
+ },
+ )
bom_doc.currency = "INR"
return bom_doc
+
def create_process_loss_bom_items():
item_list = [
("_Test Item - Non Whole UOM", "Kg"),
("_Test Item - Whole UOM", "Unit"),
- ("_Test PL BOM Item", "Unit")
+ ("_Test PL BOM Item", "Unit"),
]
return [create_process_loss_bom_item(it) for it in item_list]
+
def create_process_loss_bom_item(item_tuple):
item_code, stock_uom = item_tuple
if frappe.db.exists("Item", item_code) is None:
- return make_item(
- item_code,
- {'stock_uom':stock_uom, 'valuation_rate':100}
- )
+ return make_item(item_code, {"stock_uom": stock_uom, "valuation_rate": 100})
else:
return frappe.get_doc("Item", item_code)
diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
index ec617f3aaa9..210c0ea6a72 100644
--- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
+++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
@@ -65,7 +65,8 @@
"label": "Hour Rate",
"oldfieldname": "hour_rate",
"oldfieldtype": "Currency",
- "options": "currency"
+ "options": "currency",
+ "precision": "2"
},
{
"description": "In minutes",
@@ -99,7 +100,6 @@
"read_only": 1
},
{
- "default": "5",
"depends_on": "eval:parent.doctype == 'BOM'",
"fieldname": "base_operating_cost",
"fieldtype": "Currency",
@@ -177,7 +177,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-09-13 16:45:01.092868",
+ "modified": "2022-04-08 01:18:33.547481",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",
diff --git a/erpnext/manufacturing/doctype/bom_update_log/__init__.py b/erpnext/manufacturing/doctype/bom_update_log/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js
new file mode 100644
index 00000000000..6da808e26d1
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('BOM Update Log', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
new file mode 100644
index 00000000000..98c1acb71ce
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
@@ -0,0 +1,109 @@
+{
+ "actions": [],
+ "autoname": "BOM-UPDT-LOG-.#####",
+ "creation": "2022-03-16 14:23:35.210155",
+ "description": "BOM Update Tool Log with job status maintained",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "current_bom",
+ "new_bom",
+ "column_break_3",
+ "update_type",
+ "status",
+ "error_log",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "current_bom",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Current BOM",
+ "options": "BOM"
+ },
+ {
+ "fieldname": "new_bom",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "New BOM",
+ "options": "BOM"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "update_type",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Update Type",
+ "options": "Replace BOM\nUpdate Cost"
+ },
+ {
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "label": "Status",
+ "options": "Queued\nIn Progress\nCompleted\nFailed"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "BOM Update Log",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "error_log",
+ "fieldtype": "Link",
+ "label": "Error Log",
+ "options": "Error Log"
+ }
+ ],
+ "in_create": 1,
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2022-03-31 12:51:44.885102",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "BOM Update Log",
+ "naming_rule": "Expression (old style)",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
new file mode 100644
index 00000000000..c3df96c99b1
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
@@ -0,0 +1,165 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+from typing import Dict, List, Optional
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import cstr, flt
+from typing_extensions import Literal
+
+from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
+
+
+class BOMMissingError(frappe.ValidationError):
+ pass
+
+
+class BOMUpdateLog(Document):
+ def validate(self):
+ if self.update_type == "Replace BOM":
+ self.validate_boms_are_specified()
+ self.validate_same_bom()
+ self.validate_bom_items()
+
+ self.status = "Queued"
+
+ def validate_boms_are_specified(self):
+ if self.update_type == "Replace BOM" and not (self.current_bom and self.new_bom):
+ frappe.throw(
+ msg=_("Please mention the Current and New BOM for replacement."),
+ title=_("Mandatory"),
+ exc=BOMMissingError,
+ )
+
+ def validate_same_bom(self):
+ if cstr(self.current_bom) == cstr(self.new_bom):
+ frappe.throw(_("Current BOM and New BOM can not be same"))
+
+ def validate_bom_items(self):
+ current_bom_item = frappe.db.get_value("BOM", self.current_bom, "item")
+ new_bom_item = frappe.db.get_value("BOM", self.new_bom, "item")
+
+ if current_bom_item != new_bom_item:
+ frappe.throw(_("The selected BOMs are not for the same item"))
+
+ def on_submit(self):
+ if frappe.flags.in_test:
+ return
+
+ if self.update_type == "Replace BOM":
+ boms = {"current_bom": self.current_bom, "new_bom": self.new_bom}
+ frappe.enqueue(
+ method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job",
+ doc=self,
+ boms=boms,
+ timeout=40000,
+ )
+ else:
+ frappe.enqueue(
+ method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job",
+ doc=self,
+ update_type="Update Cost",
+ timeout=40000,
+ )
+
+
+def replace_bom(boms: Dict) -> None:
+ """Replace current BOM with new BOM in parent BOMs."""
+ current_bom = boms.get("current_bom")
+ new_bom = boms.get("new_bom")
+
+ unit_cost = get_new_bom_unit_cost(new_bom)
+ update_new_bom_in_bom_items(unit_cost, current_bom, new_bom)
+
+ frappe.cache().delete_key("bom_children")
+ parent_boms = get_parent_boms(new_bom)
+
+ for bom in parent_boms:
+ bom_obj = frappe.get_doc("BOM", bom)
+ # this is only used for versioning and we do not want
+ # to make separate db calls by using load_doc_before_save
+ # which proves to be expensive while doing bulk replace
+ bom_obj._doc_before_save = bom_obj
+ bom_obj.update_exploded_items()
+ bom_obj.calculate_cost()
+ bom_obj.update_parent_cost()
+ bom_obj.db_update()
+ if bom_obj.meta.get("track_changes") and not bom_obj.flags.ignore_version:
+ bom_obj.save_version()
+
+
+def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None:
+ bom_item = frappe.qb.DocType("BOM Item")
+ (
+ frappe.qb.update(bom_item)
+ .set(bom_item.bom_no, new_bom)
+ .set(bom_item.rate, unit_cost)
+ .set(bom_item.amount, (bom_item.stock_qty * unit_cost))
+ .where(
+ (bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")
+ )
+ ).run()
+
+
+def get_parent_boms(new_bom: str, bom_list: Optional[List] = None) -> List:
+ bom_list = bom_list or []
+ bom_item = frappe.qb.DocType("BOM Item")
+
+ parents = (
+ frappe.qb.from_(bom_item)
+ .select(bom_item.parent)
+ .where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM"))
+ .run(as_dict=True)
+ )
+
+ for d in parents:
+ if new_bom == d.parent:
+ frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent))
+
+ bom_list.append(d.parent)
+ get_parent_boms(d.parent, bom_list)
+
+ return list(set(bom_list))
+
+
+def get_new_bom_unit_cost(new_bom: str) -> float:
+ bom = frappe.qb.DocType("BOM")
+ new_bom_unitcost = (
+ frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == new_bom).run()
+ )
+
+ return flt(new_bom_unitcost[0][0])
+
+
+def run_bom_job(
+ doc: "BOMUpdateLog",
+ boms: Optional[Dict[str, str]] = None,
+ update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM",
+) -> None:
+ try:
+ doc.db_set("status", "In Progress")
+ if not frappe.flags.in_test:
+ frappe.db.commit()
+
+ frappe.db.auto_commit_on_many_writes = 1
+
+ boms = frappe._dict(boms or {})
+
+ if update_type == "Replace BOM":
+ replace_bom(boms)
+ else:
+ update_cost()
+
+ doc.db_set("status", "Completed")
+
+ except Exception:
+ frappe.db.rollback()
+ error_log = frappe.log_error(message=frappe.get_traceback(), title=_("BOM Update Tool Error"))
+
+ doc.db_set("status", "Failed")
+ doc.db_set("error_log", error_log.name)
+
+ finally:
+ frappe.db.auto_commit_on_many_writes = 0
+ frappe.db.commit() # nosemgrep
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js
new file mode 100644
index 00000000000..e39b5637c78
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js
@@ -0,0 +1,13 @@
+frappe.listview_settings['BOM Update Log'] = {
+ add_fields: ["status"],
+ get_indicator: function(doc) {
+ let status_map = {
+ "Queued": "orange",
+ "In Progress": "blue",
+ "Completed": "green",
+ "Failed": "red"
+ };
+
+ return [__(doc.status), status_map[doc.status], "status,=," + doc.status];
+ }
+};
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py
new file mode 100644
index 00000000000..47efea961b4
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py
@@ -0,0 +1,96 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import (
+ BOMMissingError,
+ run_bom_job,
+)
+from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom
+
+test_records = frappe.get_test_records("BOM")
+
+
+class TestBOMUpdateLog(FrappeTestCase):
+ "Test BOM Update Tool Operations via BOM Update Log."
+
+ def setUp(self):
+ bom_doc = frappe.copy_doc(test_records[0])
+ bom_doc.items[1].item_code = "_Test Item"
+ bom_doc.insert()
+
+ self.boms = frappe._dict(
+ current_bom="BOM-_Test Item Home Desktop Manufactured-001",
+ new_bom=bom_doc.name,
+ )
+
+ self.new_bom_doc = bom_doc
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ if self._testMethodName == "test_bom_update_log_completion":
+ # clear logs and delete BOM created via setUp
+ frappe.db.delete("BOM Update Log")
+ self.new_bom_doc.cancel()
+ self.new_bom_doc.delete()
+
+ # explicitly commit and restore to original state
+ frappe.db.commit() # nosemgrep
+
+ def test_bom_update_log_validate(self):
+ "Test if BOM presence is validated."
+
+ with self.assertRaises(BOMMissingError):
+ enqueue_replace_bom(boms={})
+
+ with self.assertRaises(frappe.ValidationError):
+ enqueue_replace_bom(boms=frappe._dict(current_bom=self.boms.new_bom, new_bom=self.boms.new_bom))
+
+ with self.assertRaises(frappe.ValidationError):
+ enqueue_replace_bom(boms=frappe._dict(current_bom=self.boms.new_bom, new_bom="Dummy BOM"))
+
+ def test_bom_update_log_queueing(self):
+ "Test if BOM Update Log is created and queued."
+
+ log = enqueue_replace_bom(
+ boms=self.boms,
+ )
+
+ self.assertEqual(log.docstatus, 1)
+ self.assertEqual(log.status, "Queued")
+
+ def test_bom_update_log_completion(self):
+ "Test if BOM Update Log handles job completion correctly."
+
+ log = enqueue_replace_bom(
+ boms=self.boms,
+ )
+
+ # Explicitly commits log, new bom (setUp) and replacement impact.
+ # Is run via background jobs IRL
+ run_bom_job(
+ doc=log,
+ boms=self.boms,
+ update_type="Replace BOM",
+ )
+ log.reload()
+
+ self.assertEqual(log.status, "Completed")
+
+ # teardown (undo replace impact) due to commit
+ boms = frappe._dict(
+ current_bom=self.boms.new_bom,
+ new_bom=self.boms.current_bom,
+ )
+ log2 = enqueue_replace_bom(
+ boms=self.boms,
+ )
+ run_bom_job( # Explicitly commits
+ doc=log2,
+ boms=boms,
+ update_type="Replace BOM",
+ )
+ self.assertEqual(log2.status, "Completed")
diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js
index bf5fe2e18de..7ba6517a4fb 100644
--- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js
+++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js
@@ -20,30 +20,67 @@ frappe.ui.form.on('BOM Update Tool', {
refresh: function(frm) {
frm.disable_save();
+ frm.events.disable_button(frm, "replace");
+
+ frm.add_custom_button(__("View BOM Update Log"), () => {
+ frappe.set_route("List", "BOM Update Log");
+ });
},
- replace: function(frm) {
+ disable_button: (frm, field, disable=true) => {
+ frm.get_field(field).input.disabled = disable;
+ },
+
+ current_bom: (frm) => {
+ if (frm.doc.current_bom && frm.doc.new_bom) {
+ frm.events.disable_button(frm, "replace", false);
+ }
+ },
+
+ new_bom: (frm) => {
+ if (frm.doc.current_bom && frm.doc.new_bom) {
+ frm.events.disable_button(frm, "replace", false);
+ }
+ },
+
+ replace: (frm) => {
if (frm.doc.current_bom && frm.doc.new_bom) {
frappe.call({
method: "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.enqueue_replace_bom",
freeze: true,
args: {
- args: {
+ boms: {
"current_bom": frm.doc.current_bom,
"new_bom": frm.doc.new_bom
}
+ },
+ callback: result => {
+ if (result && result.message && !result.exc) {
+ frm.events.confirm_job_start(frm, result.message);
+ }
}
});
}
},
- update_latest_price_in_all_boms: function() {
+ update_latest_price_in_all_boms: (frm) => {
frappe.call({
method: "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.enqueue_update_cost",
freeze: true,
- callback: function() {
- frappe.msgprint(__("Latest price updated in all BOMs"));
+ callback: result => {
+ if (result && result.message && !result.exc) {
+ frm.events.confirm_job_start(frm, result.message);
+ }
}
});
+ },
+
+ confirm_job_start: (frm, log_data) => {
+ let log_link = frappe.utils.get_form_link("BOM Update Log", log_data.name, true);
+ frappe.msgprint({
+ "message": __("BOM Updation is queued and may take a few minutes. Check {0} for progress.", [log_link]),
+ "title": __("BOM Update Initiated"),
+ "indicator": "blue"
+ });
}
});
diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
index f9c3b062179..4061c5af7c2 100644
--- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
+++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
@@ -1,114 +1,71 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-
import json
+from typing import TYPE_CHECKING, Dict, Optional, Union
+
+from typing_extensions import Literal
+
+if TYPE_CHECKING:
+ from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog
-import click
import frappe
-from frappe import _
from frappe.model.document import Document
-from frappe.utils import cstr, flt
-from six import string_types
from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order
class BOMUpdateTool(Document):
- def replace_bom(self):
- self.validate_bom()
+ pass
- unit_cost = get_new_bom_unit_cost(self.new_bom)
- self.update_new_bom(unit_cost)
-
- frappe.cache().delete_key('bom_children')
- bom_list = self.get_parent_boms(self.new_bom)
-
- with click.progressbar(bom_list) as bom_list:
- pass
- for bom in bom_list:
- try:
- bom_obj = frappe.get_cached_doc('BOM', bom)
- # this is only used for versioning and we do not want
- # to make separate db calls by using load_doc_before_save
- # which proves to be expensive while doing bulk replace
- bom_obj._doc_before_save = bom_obj
- bom_obj.update_new_bom(self.current_bom, self.new_bom, unit_cost)
- bom_obj.update_exploded_items()
- bom_obj.calculate_cost()
- bom_obj.update_parent_cost()
- bom_obj.db_update()
- if bom_obj.meta.get('track_changes') and not bom_obj.flags.ignore_version:
- bom_obj.save_version()
- except Exception:
- frappe.log_error(frappe.get_traceback())
-
- def validate_bom(self):
- if cstr(self.current_bom) == cstr(self.new_bom):
- frappe.throw(_("Current BOM and New BOM can not be same"))
-
- if frappe.db.get_value("BOM", self.current_bom, "item") \
- != frappe.db.get_value("BOM", self.new_bom, "item"):
- frappe.throw(_("The selected BOMs are not for the same item"))
-
- def update_new_bom(self, unit_cost):
- frappe.db.sql("""update `tabBOM Item` set bom_no=%s,
- rate=%s, amount=stock_qty*%s where bom_no = %s and docstatus < 2 and parenttype='BOM'""",
- (self.new_bom, unit_cost, unit_cost, self.current_bom))
-
- def get_parent_boms(self, bom, bom_list=None):
- if bom_list is None:
- bom_list = []
- data = frappe.db.sql("""SELECT DISTINCT parent FROM `tabBOM Item`
- WHERE bom_no = %s AND docstatus < 2 AND parenttype='BOM'""", bom)
-
- for d in data:
- if self.new_bom == d[0]:
- frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(bom, self.new_bom))
-
- bom_list.append(d[0])
- self.get_parent_boms(d[0], bom_list)
-
- return list(set(bom_list))
-
-def get_new_bom_unit_cost(bom):
- new_bom_unitcost = frappe.db.sql("""SELECT `total_cost`/`quantity`
- FROM `tabBOM` WHERE name = %s""", bom)
-
- return flt(new_bom_unitcost[0][0]) if new_bom_unitcost else 0
@frappe.whitelist()
-def enqueue_replace_bom(args):
- if isinstance(args, string_types):
- args = json.loads(args)
+def enqueue_replace_bom(
+ boms: Optional[Union[Dict, str]] = None, args: Optional[Union[Dict, str]] = None
+) -> "BOMUpdateLog":
+ """Returns a BOM Update Log (that queues a job) for BOM Replacement."""
+ boms = boms or args
+ if isinstance(boms, str):
+ boms = json.loads(boms)
+
+ update_log = create_bom_update_log(boms=boms)
+ return update_log
- frappe.enqueue("erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.replace_bom", args=args, timeout=40000)
- frappe.msgprint(_("Queued for replacing the BOM. It may take a few minutes."))
@frappe.whitelist()
-def enqueue_update_cost():
- frappe.enqueue("erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost", timeout=40000)
- frappe.msgprint(_("Queued for updating latest price in all Bill of Materials. It may take a few minutes."))
+def enqueue_update_cost() -> "BOMUpdateLog":
+ """Returns a BOM Update Log (that queues a job) for BOM Cost Updation."""
+ update_log = create_bom_update_log(update_type="Update Cost")
+ return update_log
-def update_latest_price_in_all_boms():
+
+def auto_update_latest_price_in_all_boms() -> None:
+ """Called via hooks.py."""
if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"):
update_cost()
-def replace_bom(args):
- frappe.db.auto_commit_on_many_writes = 1
- args = frappe._dict(args)
- doc = frappe.get_doc("BOM Update Tool")
- doc.current_bom = args.current_bom
- doc.new_bom = args.new_bom
- doc.replace_bom()
-
- frappe.db.auto_commit_on_many_writes = 0
-
-def update_cost():
- frappe.db.auto_commit_on_many_writes = 1
+def update_cost() -> None:
+ """Updates Cost for all BOMs from bottom to top."""
bom_list = get_boms_in_bottom_up_order()
for bom in bom_list:
frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True)
- frappe.db.auto_commit_on_many_writes = 0
+
+def create_bom_update_log(
+ boms: Optional[Dict[str, str]] = None,
+ update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM",
+) -> "BOMUpdateLog":
+ """Creates a BOM Update Log that handles the background job."""
+
+ boms = boms or {}
+ current_bom = boms.get("current_bom")
+ new_bom = boms.get("new_bom")
+ return frappe.get_doc(
+ {
+ "doctype": "BOM Update Log",
+ "current_bom": current_bom,
+ "new_bom": new_bom,
+ "update_type": update_type,
+ }
+ ).submit()
diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
index 12576cbf322..fae72a0f6f7 100644
--- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
+++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
@@ -2,15 +2,19 @@
# License: GNU General Public License v3. See license.txt
import frappe
+from frappe.tests.utils import FrappeTestCase
+from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import replace_bom
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.stock.doctype.item.test_item import create_item
-from erpnext.tests.utils import ERPNextTestCase
-test_records = frappe.get_test_records('BOM')
+test_records = frappe.get_test_records("BOM")
+
+
+class TestBOMUpdateTool(FrappeTestCase):
+ "Test major functions run via BOM Update Tool."
-class TestBOMUpdateTool(ERPNextTestCase):
def test_replace_bom(self):
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
@@ -18,18 +22,16 @@ class TestBOMUpdateTool(ERPNextTestCase):
bom_doc.items[1].item_code = "_Test Item"
bom_doc.insert()
- update_tool = frappe.get_doc("BOM Update Tool")
- update_tool.current_bom = current_bom
- update_tool.new_bom = bom_doc.name
- update_tool.replace_bom()
+ boms = frappe._dict(current_bom=current_bom, new_bom=bom_doc.name)
+ replace_bom(boms)
self.assertFalse(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", current_bom))
self.assertTrue(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", bom_doc.name))
# reverse, as it affects other testcases
- update_tool.current_bom = bom_doc.name
- update_tool.new_bom = current_bom
- update_tool.replace_bom()
+ boms.current_bom = bom_doc.name
+ boms.new_bom = current_bom
+ replace_bom(boms)
def test_bom_cost(self):
for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]:
@@ -37,10 +39,13 @@ class TestBOMUpdateTool(ERPNextTestCase):
if item_doc.valuation_rate != 100.00:
frappe.db.set_value("Item", item_doc.name, "valuation_rate", 100)
- bom_no = frappe.db.get_value('BOM', {'item': 'BOM Cost Test Item 1'}, "name")
+ bom_no = frappe.db.get_value("BOM", {"item": "BOM Cost Test Item 1"}, "name")
if not bom_no:
- doc = make_bom(item = 'BOM Cost Test Item 1',
- raw_materials =['BOM Cost Test Item 2', 'BOM Cost Test Item 3'], currency="INR")
+ doc = make_bom(
+ item="BOM Cost Test Item 1",
+ raw_materials=["BOM Cost Test Item 2", "BOM Cost Test Item 3"],
+ currency="INR",
+ )
else:
doc = frappe.get_doc("BOM", bom_no)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index 768cea33dae..20bbbeaa8ea 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -28,12 +28,12 @@ frappe.ui.form.on('Job Card', {
frappe.flags.resume_job = 0;
let has_items = frm.doc.items && frm.doc.items.length;
- if (frm.doc.__onload.work_order_stopped) {
+ if (!frm.is_new() && frm.doc.__onload.work_order_stopped) {
frm.disable_save();
return;
}
- if (!frm.doc.__islocal && has_items && frm.doc.docstatus < 2) {
+ if (!frm.is_new() && has_items && frm.doc.docstatus < 2) {
let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
@@ -73,7 +73,18 @@ frappe.ui.form.on('Job Card', {
if (frm.doc.docstatus == 0 && !frm.is_new() &&
(frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
- frm.trigger("prepare_timer_buttons");
+
+ // if Job Card is link to Work Order, the job card must not be able to start if Work Order not "Started"
+ // and if stock mvt for WIP is required
+ if (frm.doc.work_order) {
+ frappe.db.get_value('Work Order', frm.doc.work_order, ['skip_transfer', 'status'], (result) => {
+ if (result.skip_transfer === 1 || result.status == 'In Process') {
+ frm.trigger("prepare_timer_buttons");
+ }
+ });
+ } else {
+ frm.trigger("prepare_timer_buttons");
+ }
}
frm.trigger("setup_quality_inspection");
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json
index 137561aa2a9..dd5bb89bee3 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.json
+++ b/erpnext/manufacturing/doctype/job_card/job_card.json
@@ -420,7 +420,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-11-12 10:15:03.572401",
+ "modified": "2021-11-12 10:15:06.572401",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 3c406156ebd..c8b37a9c80d 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -26,15 +26,27 @@ from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings
)
-class OverlapError(frappe.ValidationError): pass
+class OverlapError(frappe.ValidationError):
+ pass
+
+
+class OperationMismatchError(frappe.ValidationError):
+ pass
+
+
+class OperationSequenceError(frappe.ValidationError):
+ pass
+
+
+class JobCardCancelError(frappe.ValidationError):
+ pass
-class OperationMismatchError(frappe.ValidationError): pass
-class OperationSequenceError(frappe.ValidationError): pass
-class JobCardCancelError(frappe.ValidationError): pass
class JobCard(Document):
def onload(self):
- excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
+ excess_transfer = frappe.db.get_single_value(
+ "Manufacturing Settings", "job_card_excess_transfer"
+ )
self.set_onload("job_card_excess_transfer", excess_transfer)
self.set_onload("work_order_stopped", self.is_work_order_stopped())
@@ -48,27 +60,35 @@ class JobCard(Document):
self.validate_work_order()
def set_sub_operations(self):
- if self.operation:
+ if not self.sub_operations and self.operation:
self.sub_operations = []
- for row in frappe.get_all('Sub Operation',
- filters = {'parent': self.operation}, fields=['operation', 'idx'], order_by='idx'):
- row.status = 'Pending'
+ for row in frappe.get_all(
+ "Sub Operation",
+ filters={"parent": self.operation},
+ fields=["operation", "idx"],
+ order_by="idx",
+ ):
+ row.status = "Pending"
row.sub_operation = row.operation
- self.append('sub_operations', row)
+ self.append("sub_operations", row)
def validate_time_logs(self):
self.total_time_in_mins = 0.0
self.total_completed_qty = 0.0
- if self.get('time_logs'):
- for d in self.get('time_logs'):
+ if self.get("time_logs"):
+ for d in self.get("time_logs"):
if d.to_time and get_datetime(d.from_time) > get_datetime(d.to_time):
frappe.throw(_("Row {0}: From time must be less than to time").format(d.idx))
data = self.get_overlap_for(d)
if data:
- frappe.throw(_("Row {0}: From Time and To Time of {1} is overlapping with {2}")
- .format(d.idx, self.name, data.name), OverlapError)
+ frappe.throw(
+ _("Row {0}: From Time and To Time of {1} is overlapping with {2}").format(
+ d.idx, self.name, data.name
+ ),
+ OverlapError,
+ )
if d.from_time and d.to_time:
d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60
@@ -86,8 +106,9 @@ class JobCard(Document):
production_capacity = 1
if self.workstation:
- production_capacity = frappe.get_cached_value("Workstation",
- self.workstation, 'production_capacity') or 1
+ production_capacity = (
+ frappe.get_cached_value("Workstation", self.workstation, "production_capacity") or 1
+ )
validate_overlap_for = " and jc.workstation = %(workstation)s "
if args.get("employee"):
@@ -95,11 +116,12 @@ class JobCard(Document):
production_capacity = 1
validate_overlap_for = " and jctl.employee = %(employee)s "
- extra_cond = ''
+ extra_cond = ""
if check_next_available_slot:
extra_cond = " or (%(from_time)s <= jctl.from_time and %(to_time)s <= jctl.to_time)"
- existing = frappe.db.sql("""select jc.name as name, jctl.to_time from
+ existing = frappe.db.sql(
+ """select jc.name as name, jctl.to_time from
`tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and
(
(%(from_time)s > jctl.from_time and %(from_time)s < jctl.to_time) or
@@ -107,15 +129,19 @@ class JobCard(Document):
(%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time) {0}
)
and jctl.name != %(name)s and jc.name != %(parent)s and jc.docstatus < 2 {1}
- order by jctl.to_time desc limit 1""".format(extra_cond, validate_overlap_for),
+ order by jctl.to_time desc limit 1""".format(
+ extra_cond, validate_overlap_for
+ ),
{
"from_time": args.from_time,
"to_time": args.to_time,
"name": args.name or "No Name",
"parent": args.parent or "No Name",
"employee": args.get("employee"),
- "workstation": self.workstation
- }, as_dict=True)
+ "workstation": self.workstation,
+ },
+ as_dict=True,
+ )
if existing and production_capacity > len(existing):
return
@@ -125,10 +151,7 @@ class JobCard(Document):
def schedule_time_logs(self, row):
row.remaining_time_in_mins = row.time_in_mins
while row.remaining_time_in_mins > 0:
- args = frappe._dict({
- "from_time": row.planned_start_time,
- "to_time": row.planned_end_time
- })
+ args = frappe._dict({"from_time": row.planned_start_time, "to_time": row.planned_end_time})
self.validate_overlap_for_workstation(args, row)
self.check_workstation_time(row)
@@ -141,13 +164,16 @@ class JobCard(Document):
def check_workstation_time(self, row):
workstation_doc = frappe.get_cached_doc("Workstation", self.workstation)
- if (not workstation_doc.working_hours or
- cint(frappe.db.get_single_value("Manufacturing Settings", "allow_overtime"))):
+ if not workstation_doc.working_hours or cint(
+ frappe.db.get_single_value("Manufacturing Settings", "allow_overtime")
+ ):
if get_datetime(row.planned_end_time) < get_datetime(row.planned_start_time):
row.planned_end_time = add_to_date(row.planned_start_time, minutes=row.time_in_mins)
row.remaining_time_in_mins = 0.0
else:
- row.remaining_time_in_mins -= time_diff_in_minutes(row.planned_end_time, row.planned_start_time)
+ row.remaining_time_in_mins -= time_diff_in_minutes(
+ row.planned_end_time, row.planned_start_time
+ )
self.update_time_logs(row)
return
@@ -167,14 +193,15 @@ class JobCard(Document):
workstation_start_time = datetime.datetime.combine(start_date, get_time(time_slot.start_time))
workstation_end_time = datetime.datetime.combine(start_date, get_time(time_slot.end_time))
- if (get_datetime(row.planned_start_time) >= workstation_start_time and
- get_datetime(row.planned_start_time) <= workstation_end_time):
+ if (
+ get_datetime(row.planned_start_time) >= workstation_start_time
+ and get_datetime(row.planned_start_time) <= workstation_end_time
+ ):
time_in_mins = time_diff_in_minutes(workstation_end_time, row.planned_start_time)
# If remaining time fit in workstation time logs else split hours as per workstation time
if time_in_mins > row.remaining_time_in_mins:
- row.planned_end_time = add_to_date(row.planned_start_time,
- minutes=row.remaining_time_in_mins)
+ row.planned_end_time = add_to_date(row.planned_start_time, minutes=row.remaining_time_in_mins)
row.remaining_time_in_mins = 0
else:
row.planned_end_time = add_to_date(row.planned_start_time, minutes=time_in_mins)
@@ -182,14 +209,16 @@ class JobCard(Document):
self.update_time_logs(row)
- if total_idx != (i+1) and row.remaining_time_in_mins > 0:
- row.planned_start_time = datetime.datetime.combine(start_date,
- get_time(workstation_doc.working_hours[i+1].start_time))
+ if total_idx != (i + 1) and row.remaining_time_in_mins > 0:
+ row.planned_start_time = datetime.datetime.combine(
+ start_date, get_time(workstation_doc.working_hours[i + 1].start_time)
+ )
if row.remaining_time_in_mins > 0:
start_date = add_days(start_date, 1)
- row.planned_start_time = datetime.datetime.combine(start_date,
- get_time(workstation_doc.working_hours[0].start_time))
+ row.planned_start_time = datetime.datetime.combine(
+ start_date, get_time(workstation_doc.working_hours[0].start_time)
+ )
def add_time_log(self, args):
last_row = []
@@ -204,21 +233,25 @@ class JobCard(Document):
if last_row and args.get("complete_time"):
for row in self.time_logs:
if not row.to_time:
- row.update({
- "to_time": get_datetime(args.get("complete_time")),
- "operation": args.get("sub_operation"),
- "completed_qty": args.get("completed_qty") or 0.0
- })
+ row.update(
+ {
+ "to_time": get_datetime(args.get("complete_time")),
+ "operation": args.get("sub_operation"),
+ "completed_qty": args.get("completed_qty") or 0.0,
+ }
+ )
elif args.get("start_time"):
- new_args = frappe._dict({
- "from_time": get_datetime(args.get("start_time")),
- "operation": args.get("sub_operation"),
- "completed_qty": 0.0
- })
+ new_args = frappe._dict(
+ {
+ "from_time": get_datetime(args.get("start_time")),
+ "operation": args.get("sub_operation"),
+ "completed_qty": 0.0,
+ }
+ )
if employees:
for name in employees:
- new_args.employee = name.get('employee')
+ new_args.employee = name.get("employee")
self.add_start_time_log(new_args)
else:
self.add_start_time_log(new_args)
@@ -236,10 +269,7 @@ class JobCard(Document):
def set_employees(self, employees):
for name in employees:
- self.append('employee', {
- 'employee': name.get('employee'),
- 'completed_qty': 0.0
- })
+ self.append("employee", {"employee": name.get("employee"), "completed_qty": 0.0})
def reset_timer_value(self, args):
self.started_time = None
@@ -263,13 +293,17 @@ class JobCard(Document):
operation_wise_completed_time = {}
for time_log in self.time_logs:
if time_log.operation not in operation_wise_completed_time:
- operation_wise_completed_time.setdefault(time_log.operation,
- frappe._dict({"status": "Pending", "completed_qty":0.0, "completed_time": 0.0, "employee": []}))
+ operation_wise_completed_time.setdefault(
+ time_log.operation,
+ frappe._dict(
+ {"status": "Pending", "completed_qty": 0.0, "completed_time": 0.0, "employee": []}
+ ),
+ )
op_row = operation_wise_completed_time[time_log.operation]
op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete"
- if self.status == 'On Hold':
- op_row.status = 'Pause'
+ if self.status == "On Hold":
+ op_row.status = "Pause"
op_row.employee.append(time_log.employee)
if time_log.time_in_mins:
@@ -279,7 +313,7 @@ class JobCard(Document):
for row in self.sub_operations:
operation_deatils = operation_wise_completed_time.get(row.sub_operation)
if operation_deatils:
- if row.status != 'Complete':
+ if row.status != "Complete":
row.status = operation_deatils.status
row.completed_time = operation_deatils.completed_time
@@ -289,43 +323,52 @@ class JobCard(Document):
if operation_deatils.completed_qty:
row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee))
else:
- row.status = 'Pending'
+ row.status = "Pending"
row.completed_time = 0.0
row.completed_qty = 0.0
def update_time_logs(self, row):
- self.append("time_logs", {
- "from_time": row.planned_start_time,
- "to_time": row.planned_end_time,
- "completed_qty": 0,
- "time_in_mins": time_diff_in_minutes(row.planned_end_time, row.planned_start_time),
- })
+ self.append(
+ "time_logs",
+ {
+ "from_time": row.planned_start_time,
+ "to_time": row.planned_end_time,
+ "completed_qty": 0,
+ "time_in_mins": time_diff_in_minutes(row.planned_end_time, row.planned_start_time),
+ },
+ )
@frappe.whitelist()
def get_required_items(self):
- if not self.get('work_order'):
+ if not self.get("work_order"):
return
- doc = frappe.get_doc('Work Order', self.get('work_order'))
- if doc.transfer_material_against == 'Work Order' or doc.skip_transfer:
+ doc = frappe.get_doc("Work Order", self.get("work_order"))
+ if doc.transfer_material_against == "Work Order" or doc.skip_transfer:
return
for d in doc.required_items:
if not d.operation:
- frappe.throw(_("Row {0} : Operation is required against the raw material item {1}")
- .format(d.idx, d.item_code))
+ frappe.throw(
+ _("Row {0} : Operation is required against the raw material item {1}").format(
+ d.idx, d.item_code
+ )
+ )
- if self.get('operation') == d.operation:
- self.append('items', {
- "item_code": d.item_code,
- "source_warehouse": d.source_warehouse,
- "uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'),
- "item_name": d.item_name,
- "description": d.description,
- "required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty,
- "rate": d.rate,
- "amount": d.amount
- })
+ if self.get("operation") == d.operation:
+ self.append(
+ "items",
+ {
+ "item_code": d.item_code,
+ "source_warehouse": d.source_warehouse,
+ "uom": frappe.db.get_value("Item", d.item_code, "stock_uom"),
+ "item_name": d.item_name,
+ "description": d.description,
+ "required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty,
+ "rate": d.rate,
+ "amount": d.amount,
+ },
+ )
def on_submit(self):
self.validate_transfer_qty()
@@ -339,31 +382,52 @@ class JobCard(Document):
def validate_transfer_qty(self):
if self.items and self.transferred_qty < self.for_quantity:
- frappe.throw(_('Materials needs to be transferred to the work in progress warehouse for the job card {0}')
- .format(self.name))
+ frappe.throw(
+ _(
+ "Materials needs to be transferred to the work in progress warehouse for the job card {0}"
+ ).format(self.name)
+ )
def validate_job_card(self):
- if self.work_order and frappe.get_cached_value('Work Order', self.work_order, 'status') == 'Stopped':
- frappe.throw(_("Transaction not allowed against stopped Work Order {0}")
- .format(get_link_to_form('Work Order', self.work_order)))
+ if (
+ self.work_order
+ and frappe.get_cached_value("Work Order", self.work_order, "status") == "Stopped"
+ ):
+ frappe.throw(
+ _("Transaction not allowed against stopped Work Order {0}").format(
+ get_link_to_form("Work Order", self.work_order)
+ )
+ )
if not self.time_logs:
- frappe.throw(_("Time logs are required for {0} {1}")
- .format(bold("Job Card"), get_link_to_form("Job Card", self.name)))
+ frappe.throw(
+ _("Time logs are required for {0} {1}").format(
+ bold("Job Card"), get_link_to_form("Job Card", self.name)
+ )
+ )
if self.for_quantity and self.total_completed_qty != self.for_quantity:
total_completed_qty = bold(_("Total Completed Qty"))
qty_to_manufacture = bold(_("Qty to Manufacture"))
- frappe.throw(_("The {0} ({1}) must be equal to {2} ({3})")
- .format(total_completed_qty, bold(self.total_completed_qty), qty_to_manufacture,bold(self.for_quantity)))
+ frappe.throw(
+ _("The {0} ({1}) must be equal to {2} ({3})").format(
+ total_completed_qty,
+ bold(self.total_completed_qty),
+ qty_to_manufacture,
+ bold(self.for_quantity),
+ )
+ )
def update_work_order(self):
if not self.work_order:
return
- if self.is_corrective_job_card and not cint(frappe.db.get_single_value('Manufacturing Settings',
- 'add_corrective_operation_cost_in_finished_good_valuation')):
+ if self.is_corrective_job_card and not cint(
+ frappe.db.get_single_value(
+ "Manufacturing Settings", "add_corrective_operation_cost_in_finished_good_valuation"
+ )
+ ):
return
for_quantity, time_in_mins = 0, 0
@@ -375,7 +439,7 @@ class JobCard(Document):
for_quantity = flt(data[0].completed_qty)
time_in_mins = flt(data[0].time_in_mins)
- wo = frappe.get_doc('Work Order', self.work_order)
+ wo = frappe.get_doc("Work Order", self.work_order)
if self.is_corrective_job_card:
self.update_corrective_in_work_order(wo)
@@ -386,8 +450,11 @@ class JobCard(Document):
def update_corrective_in_work_order(self, wo):
wo.corrective_operation_cost = 0.0
- for row in frappe.get_all('Job Card', fields = ['total_time_in_mins', 'hour_rate'],
- filters = {'is_corrective_job_card': 1, 'docstatus': 1, 'work_order': self.work_order}):
+ for row in frappe.get_all(
+ "Job Card",
+ fields=["total_time_in_mins", "hour_rate"],
+ filters={"is_corrective_job_card": 1, "docstatus": 1, "work_order": self.work_order},
+ ):
wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate)
wo.calculate_operating_cost()
@@ -395,27 +462,37 @@ class JobCard(Document):
wo.save()
def validate_produced_quantity(self, for_quantity, wo):
- if self.docstatus < 2: return
+ if self.docstatus < 2:
+ return
if wo.produced_qty > for_quantity:
- first_part_msg = (_("The {0} {1} is used to calculate the valuation cost for the finished good {2}.")
- .format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item)))
+ first_part_msg = _(
+ "The {0} {1} is used to calculate the valuation cost for the finished good {2}."
+ ).format(
+ frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item)
+ )
- second_part_msg = (_("Kindly cancel the Manufacturing Entries first against the work order {0}.")
- .format(frappe.bold(get_link_to_form("Work Order", self.work_order))))
+ second_part_msg = _(
+ "Kindly cancel the Manufacturing Entries first against the work order {0}."
+ ).format(frappe.bold(get_link_to_form("Work Order", self.work_order)))
- frappe.throw(_("{0} {1}").format(first_part_msg, second_part_msg),
- JobCardCancelError, title = _("Error"))
+ frappe.throw(
+ _("{0} {1}").format(first_part_msg, second_part_msg), JobCardCancelError, title=_("Error")
+ )
def update_work_order_data(self, for_quantity, time_in_mins, wo):
- time_data = frappe.db.sql("""
+ time_data = frappe.db.sql(
+ """
SELECT
min(from_time) as start_time, max(to_time) as end_time
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
WHERE
jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s
and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0
- """, (self.work_order, self.operation_id), as_dict=1)
+ """,
+ (self.work_order, self.operation_id),
+ as_dict=1,
+ )
for data in wo.operations:
if data.get("name") == self.operation_id:
@@ -434,92 +511,118 @@ class JobCard(Document):
wo.save()
def get_current_operation_data(self):
- return frappe.get_all('Job Card',
- fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
- filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id,
- "is_corrective_job_card": 0})
+ return frappe.get_all(
+ "Job Card",
+ fields=["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
+ filters={
+ "docstatus": 1,
+ "work_order": self.work_order,
+ "operation_id": self.operation_id,
+ "is_corrective_job_card": 0,
+ },
+ )
def set_transferred_qty_in_job_card(self, ste_doc):
for row in ste_doc.items:
- if not row.job_card_item: continue
+ if not row.job_card_item:
+ continue
- qty = frappe.db.sql(""" SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se
+ qty = frappe.db.sql(
+ """ SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se
WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and
se.purpose = 'Material Transfer for Manufacture'
- """, (row.job_card_item))[0][0]
+ """,
+ (row.job_card_item),
+ )[0][0]
- frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty))
+ frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(qty))
def set_transferred_qty(self, update_status=False):
"Set total FG Qty for which RM was transferred."
if not self.items:
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
- doc = frappe.get_doc('Work Order', self.get('work_order'))
- if doc.transfer_material_against == 'Work Order' or doc.skip_transfer:
+ doc = frappe.get_doc("Work Order", self.get("work_order"))
+ if doc.transfer_material_against == "Work Order" or doc.skip_transfer:
return
if self.items:
# sum of 'For Quantity' of Stock Entries against JC
- self.transferred_qty = frappe.db.get_value('Stock Entry', {
- 'job_card': self.name,
- 'work_order': self.work_order,
- 'docstatus': 1,
- 'purpose': 'Material Transfer for Manufacture'
- }, 'sum(fg_completed_qty)') or 0
+ self.transferred_qty = (
+ frappe.db.get_value(
+ "Stock Entry",
+ {
+ "job_card": self.name,
+ "work_order": self.work_order,
+ "docstatus": 1,
+ "purpose": "Material Transfer for Manufacture",
+ },
+ "sum(fg_completed_qty)",
+ )
+ or 0
+ )
self.db_set("transferred_qty", self.transferred_qty)
qty = 0
if self.work_order:
- doc = frappe.get_doc('Work Order', self.work_order)
- if doc.transfer_material_against == 'Job Card' and not doc.skip_transfer:
+ doc = frappe.get_doc("Work Order", self.work_order)
+ if doc.transfer_material_against == "Job Card" and not doc.skip_transfer:
completed = True
for d in doc.operations:
- if d.status != 'Completed':
+ if d.status != "Completed":
completed = False
break
if completed:
- job_cards = frappe.get_all('Job Card', filters = {'work_order': self.work_order,
- 'docstatus': ('!=', 2)}, fields = 'sum(transferred_qty) as qty', group_by='operation_id')
+ job_cards = frappe.get_all(
+ "Job Card",
+ filters={"work_order": self.work_order, "docstatus": ("!=", 2)},
+ fields="sum(transferred_qty) as qty",
+ group_by="operation_id",
+ )
if job_cards:
qty = min(d.qty for d in job_cards)
- doc.db_set('material_transferred_for_manufacturing', qty)
+ doc.db_set("material_transferred_for_manufacturing", qty)
self.set_status(update_status)
def set_status(self, update_status=False):
- if self.status == "On Hold": return
+ if self.status == "On Hold":
+ return
- self.status = {
- 0: "Open",
- 1: "Submitted",
- 2: "Cancelled"
- }[self.docstatus or 0]
+ self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
+
+ if self.for_quantity <= self.transferred_qty:
+ self.status = "Material Transferred"
if self.time_logs:
- self.status = 'Work In Progress'
+ self.status = "Work In Progress"
- if (self.docstatus == 1 and
- (self.for_quantity <= self.total_completed_qty or not self.items)):
- self.status = 'Completed'
-
- if self.status != 'Completed':
- if self.for_quantity <= self.transferred_qty:
- self.status = 'Material Transferred'
+ if self.docstatus == 1 and (self.for_quantity <= self.total_completed_qty or not self.items):
+ self.status = "Completed"
if update_status:
- self.db_set('status', self.status)
+ self.db_set("status", self.status)
def validate_operation_id(self):
- if (self.get("operation_id") and self.get("operation_row_number") and self.operation and self.work_order and
- frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name") != self.operation_id):
+ if (
+ self.get("operation_id")
+ and self.get("operation_row_number")
+ and self.operation
+ and self.work_order
+ and frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name")
+ != self.operation_id
+ ):
work_order = bold(get_link_to_form("Work Order", self.work_order))
- frappe.throw(_("Operation {0} does not belong to the work order {1}")
- .format(bold(self.operation), work_order), OperationMismatchError)
+ frappe.throw(
+ _("Operation {0} does not belong to the work order {1}").format(
+ bold(self.operation), work_order
+ ),
+ OperationMismatchError,
+ )
def validate_sequence_id(self):
if self.is_corrective_job_card:
@@ -535,18 +638,25 @@ class JobCard(Document):
current_operation_qty += flt(self.total_completed_qty)
- data = frappe.get_all("Work Order Operation",
- fields = ["operation", "status", "completed_qty"],
- filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ('<', self.sequence_id)},
- order_by = "sequence_id, idx")
+ data = frappe.get_all(
+ "Work Order Operation",
+ fields=["operation", "status", "completed_qty"],
+ filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ("<", self.sequence_id)},
+ order_by="sequence_id, idx",
+ )
- message = "Job Card {0}: As per the sequence of the operations in the work order {1}".format(bold(self.name),
- bold(get_link_to_form("Work Order", self.work_order)))
+ message = "Job Card {0}: As per the sequence of the operations in the work order {1}".format(
+ bold(self.name), bold(get_link_to_form("Work Order", self.work_order))
+ )
for row in data:
if row.status != "Completed" and row.completed_qty < current_operation_qty:
- frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
- .format(message, bold(row.operation), bold(self.operation)), OperationSequenceError)
+ frappe.throw(
+ _("{0}, complete the operation {1} before the operation {2}.").format(
+ message, bold(row.operation), bold(self.operation)
+ ),
+ OperationSequenceError,
+ )
def validate_work_order(self):
if self.is_work_order_stopped():
@@ -554,13 +664,14 @@ class JobCard(Document):
def is_work_order_stopped(self):
if self.work_order:
- status = frappe.get_value('Work Order', self.work_order)
+ status = frappe.get_value("Work Order", self.work_order)
if status == "Closed":
return True
return False
+
@frappe.whitelist()
def make_time_log(args):
if isinstance(args, str):
@@ -571,16 +682,17 @@ def make_time_log(args):
doc.validate_sequence_id()
doc.add_time_log(args)
+
@frappe.whitelist()
def get_operation_details(work_order, operation):
if work_order and operation:
- return frappe.get_all("Work Order Operation", fields = ["name", "idx"],
- filters = {
- "parent": work_order,
- "operation": operation
- }
+ return frappe.get_all(
+ "Work Order Operation",
+ fields=["name", "idx"],
+ filters={"parent": work_order, "operation": operation},
)
+
@frappe.whitelist()
def get_operations(doctype, txt, searchfield, start, page_len, filters):
if not filters.get("work_order"):
@@ -590,12 +702,16 @@ def get_operations(doctype, txt, searchfield, start, page_len, filters):
if txt:
args["operation"] = ("like", "%{0}%".format(txt))
- return frappe.get_all("Work Order Operation",
- filters = args,
- fields = ["distinct operation as operation"],
- limit_start = start,
- limit_page_length = page_len,
- order_by="idx asc", as_list=1)
+ return frappe.get_all(
+ "Work Order Operation",
+ filters=args,
+ fields=["distinct operation as operation"],
+ limit_start=start,
+ limit_page_length=page_len,
+ order_by="idx asc",
+ as_list=1,
+ )
+
@frappe.whitelist()
def make_material_request(source_name, target_doc=None):
@@ -605,26 +721,29 @@ def make_material_request(source_name, target_doc=None):
def set_missing_values(source, target):
target.material_request_type = "Material Transfer"
- doclist = get_mapped_doc("Job Card", source_name, {
- "Job Card": {
- "doctype": "Material Request",
- "field_map": {
- "name": "job_card",
+ doclist = get_mapped_doc(
+ "Job Card",
+ source_name,
+ {
+ "Job Card": {
+ "doctype": "Material Request",
+ "field_map": {
+ "name": "job_card",
+ },
+ },
+ "Job Card Item": {
+ "doctype": "Material Request Item",
+ "field_map": {"required_qty": "qty", "uom": "stock_uom", "name": "job_card_item"},
+ "postprocess": update_item,
},
},
- "Job Card Item": {
- "doctype": "Material Request Item",
- "field_map": {
- "required_qty": "qty",
- "uom": "stock_uom",
- "name": "job_card_item"
- },
- "postprocess": update_item,
- }
- }, target_doc, set_missing_values)
+ target_doc,
+ set_missing_values,
+ )
return doclist
+
@frappe.whitelist()
def make_stock_entry(source_name, target_doc=None):
def update_item(source, target, source_parent):
@@ -642,7 +761,7 @@ def make_stock_entry(source_name, target_doc=None):
target.from_bom = 1
# avoid negative 'For Quantity'
- pending_fg_qty = flt(source.get('for_quantity', 0)) - flt(source.get('transferred_qty', 0))
+ pending_fg_qty = flt(source.get("for_quantity", 0)) - flt(source.get("transferred_qty", 0))
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
target.set_transfer_qty()
@@ -650,36 +769,45 @@ def make_stock_entry(source_name, target_doc=None):
target.set_missing_values()
target.set_stock_entry_type()
- wo_allows_alternate_item = frappe.db.get_value("Work Order", target.work_order, "allow_alternative_item")
+ wo_allows_alternate_item = frappe.db.get_value(
+ "Work Order", target.work_order, "allow_alternative_item"
+ )
for item in target.items:
- item.allow_alternative_item = int(wo_allows_alternate_item and
- frappe.get_cached_value("Item", item.item_code, "allow_alternative_item"))
+ item.allow_alternative_item = int(
+ wo_allows_alternate_item
+ and frappe.get_cached_value("Item", item.item_code, "allow_alternative_item")
+ )
- doclist = get_mapped_doc("Job Card", source_name, {
- "Job Card": {
- "doctype": "Stock Entry",
- "field_map": {
- "name": "job_card",
- "for_quantity": "fg_completed_qty"
+ doclist = get_mapped_doc(
+ "Job Card",
+ source_name,
+ {
+ "Job Card": {
+ "doctype": "Stock Entry",
+ "field_map": {"name": "job_card", "for_quantity": "fg_completed_qty"},
+ },
+ "Job Card Item": {
+ "doctype": "Stock Entry Detail",
+ "field_map": {
+ "source_warehouse": "s_warehouse",
+ "required_qty": "qty",
+ "name": "job_card_item",
+ },
+ "postprocess": update_item,
+ "condition": lambda doc: doc.required_qty > 0,
},
},
- "Job Card Item": {
- "doctype": "Stock Entry Detail",
- "field_map": {
- "source_warehouse": "s_warehouse",
- "required_qty": "qty",
- "name": "job_card_item"
- },
- "postprocess": update_item,
- "condition": lambda doc: doc.required_qty > 0
- }
- }, target_doc, set_missing_values)
+ target_doc,
+ set_missing_values,
+ )
return doclist
+
def time_diff_in_minutes(string_ed_date, string_st_date):
return time_diff(string_ed_date, string_st_date).total_seconds() / 60
+
@frappe.whitelist()
def get_job_details(start, end, filters=None):
events = []
@@ -687,41 +815,49 @@ def get_job_details(start, end, filters=None):
event_color = {
"Completed": "#cdf5a6",
"Material Transferred": "#ffdd9e",
- "Work In Progress": "#D3D3D3"
+ "Work In Progress": "#D3D3D3",
}
from frappe.desk.reportview import get_filters_cond
+
conditions = get_filters_cond("Job Card", filters, [])
- job_cards = frappe.db.sql(""" SELECT `tabJob Card`.name, `tabJob Card`.work_order,
+ job_cards = frappe.db.sql(
+ """ SELECT `tabJob Card`.name, `tabJob Card`.work_order,
`tabJob Card`.status, ifnull(`tabJob Card`.remarks, ''),
min(`tabJob Card Time Log`.from_time) as from_time,
max(`tabJob Card Time Log`.to_time) as to_time
FROM `tabJob Card` , `tabJob Card Time Log`
WHERE
`tabJob Card`.name = `tabJob Card Time Log`.parent {0}
- group by `tabJob Card`.name""".format(conditions), as_dict=1)
+ group by `tabJob Card`.name""".format(
+ conditions
+ ),
+ as_dict=1,
+ )
for d in job_cards:
- subject_data = []
- for field in ["name", "work_order", "remarks"]:
- if not d.get(field): continue
+ subject_data = []
+ for field in ["name", "work_order", "remarks"]:
+ if not d.get(field):
+ continue
- subject_data.append(d.get(field))
+ subject_data.append(d.get(field))
- color = event_color.get(d.status)
- job_card_data = {
- 'from_time': d.from_time,
- 'to_time': d.to_time,
- 'name': d.name,
- 'subject': '\n'.join(subject_data),
- 'color': color if color else "#89bcde"
- }
+ color = event_color.get(d.status)
+ job_card_data = {
+ "from_time": d.from_time,
+ "to_time": d.to_time,
+ "name": d.name,
+ "subject": "\n".join(subject_data),
+ "color": color if color else "#89bcde",
+ }
- events.append(job_card_data)
+ events.append(job_card_data)
return events
+
@frappe.whitelist()
def make_corrective_job_card(source_name, operation=None, for_operation=None, target_doc=None):
def set_missing_values(source, target):
@@ -729,20 +865,26 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta
target.operation = operation
target.for_operation = for_operation
- target.set('time_logs', [])
- target.set('employee', [])
- target.set('items', [])
+ target.set("time_logs", [])
+ target.set("employee", [])
+ target.set("items", [])
target.set_sub_operations()
target.get_required_items()
target.validate_time_logs()
- doclist = get_mapped_doc("Job Card", source_name, {
- "Job Card": {
- "doctype": "Job Card",
- "field_map": {
- "name": "for_job_card",
- },
- }
- }, target_doc, set_missing_values)
+ doclist = get_mapped_doc(
+ "Job Card",
+ source_name,
+ {
+ "Job Card": {
+ "doctype": "Job Card",
+ "field_map": {
+ "name": "for_job_card",
+ },
+ }
+ },
+ target_doc,
+ set_missing_values,
+ )
return doclist
diff --git a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py
index 24362f8246d..14c1f36d0dc 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py
@@ -1,21 +1,12 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'job_card',
- 'non_standard_fieldnames': {
- 'Quality Inspection': 'reference_name'
- },
- 'transactions': [
- {
- 'label': _('Transactions'),
- 'items': ['Material Request', 'Stock Entry']
- },
- {
- 'label': _('Reference'),
- 'items': ['Quality Inspection']
- }
- ]
+ "fieldname": "job_card",
+ "non_standard_fieldnames": {"Quality Inspection": "reference_name"},
+ "transactions": [
+ {"label": _("Transactions"), "items": ["Material Request", "Stock Entry"]},
+ {"label": _("Reference"), "items": ["Quality Inspection"]},
+ ],
}
diff --git a/erpnext/manufacturing/doctype/job_card/job_card_list.js b/erpnext/manufacturing/doctype/job_card/job_card_list.js
index 8017209e7de..7f60bdc6d92 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card_list.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card_list.js
@@ -1,4 +1,5 @@
frappe.listview_settings['Job Card'] = {
+ has_indicator_for_draft: true,
get_indicator: function(doc) {
if (doc.status === "Work In Progress") {
return [__("Work In Progress"), "orange", "status,=,Work In Progress"];
diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py
index bb5004ba86f..4647ddf05f7 100644
--- a/erpnext/manufacturing/doctype/job_card/test_job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py
@@ -2,6 +2,7 @@
# See license.txt
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import random_string
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError
@@ -11,22 +12,19 @@ from erpnext.manufacturing.doctype.job_card.job_card import (
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-from erpnext.tests.utils import ERPNextTestCase
-class TestJobCard(ERPNextTestCase):
+class TestJobCard(FrappeTestCase):
def setUp(self):
make_bom_for_jc_tests()
transfer_material_against, source_warehouse = None, None
- tests_that_skip_setup = (
- "test_job_card_material_transfer_correctness",
- )
+ tests_that_skip_setup = ("test_job_card_material_transfer_correctness",)
tests_that_transfer_against_jc = (
"test_job_card_multiple_materials_transfer",
"test_job_card_excess_material_transfer",
- "test_job_card_partial_material_transfer"
+ "test_job_card_partial_material_transfer",
)
if self._testMethodName in tests_that_skip_setup:
@@ -40,7 +38,7 @@ class TestJobCard(ERPNextTestCase):
item="_Test FG Item 2",
qty=2,
transfer_material_against=transfer_material_against,
- source_warehouse=source_warehouse
+ source_warehouse=source_warehouse,
)
def tearDown(self):
@@ -48,8 +46,9 @@ class TestJobCard(ERPNextTestCase):
def test_job_card(self):
- job_cards = frappe.get_all('Job Card',
- filters = {'work_order': self.work_order.name}, fields = ["operation_id", "name"])
+ job_cards = frappe.get_all(
+ "Job Card", filters={"work_order": self.work_order.name}, fields=["operation_id", "name"]
+ )
if job_cards:
job_card = job_cards[0]
@@ -63,30 +62,38 @@ class TestJobCard(ERPNextTestCase):
frappe.delete_doc("Job Card", d.name)
def test_job_card_with_different_work_station(self):
- job_cards = frappe.get_all('Job Card',
- filters = {'work_order': self.work_order.name},
- fields = ["operation_id", "workstation", "name", "for_quantity"])
+ job_cards = frappe.get_all(
+ "Job Card",
+ filters={"work_order": self.work_order.name},
+ fields=["operation_id", "workstation", "name", "for_quantity"],
+ )
job_card = job_cards[0]
if job_card:
- workstation = frappe.db.get_value("Workstation",
- {"name": ("not in", [job_card.workstation])}, "name")
+ workstation = frappe.db.get_value(
+ "Workstation", {"name": ("not in", [job_card.workstation])}, "name"
+ )
if not workstation or job_card.workstation == workstation:
workstation = make_workstation(workstation_name=random_string(5)).name
doc = frappe.get_doc("Job Card", job_card.name)
doc.workstation = workstation
- doc.append("time_logs", {
- "from_time": "2009-01-01 12:06:25",
- "to_time": "2009-01-01 12:37:25",
- "time_in_mins": "31.00002",
- "completed_qty": job_card.for_quantity
- })
+ doc.append(
+ "time_logs",
+ {
+ "from_time": "2009-01-01 12:06:25",
+ "to_time": "2009-01-01 12:37:25",
+ "time_in_mins": "31.00002",
+ "completed_qty": job_card.for_quantity,
+ },
+ )
doc.submit()
- completed_qty = frappe.db.get_value("Work Order Operation", job_card.operation_id, "completed_qty")
+ completed_qty = frappe.db.get_value(
+ "Work Order Operation", job_card.operation_id, "completed_qty"
+ )
self.assertEqual(completed_qty, job_card.for_quantity)
doc.cancel()
@@ -97,51 +104,49 @@ class TestJobCard(ERPNextTestCase):
def test_job_card_overlap(self):
wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
- jc1_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
- jc2_name = frappe.db.get_value("Job Card", {'work_order': wo2.name})
+ jc1_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
+ jc2_name = frappe.db.get_value("Job Card", {"work_order": wo2.name})
jc1 = frappe.get_doc("Job Card", jc1_name)
jc2 = frappe.get_doc("Job Card", jc2_name)
- employee = "_T-Employee-00001" # from test records
+ employee = "_T-Employee-00001" # from test records
- jc1.append("time_logs", {
- "from_time": "2021-01-01 00:00:00",
- "to_time": "2021-01-01 08:00:00",
- "completed_qty": 1,
- "employee": employee,
- })
+ jc1.append(
+ "time_logs",
+ {
+ "from_time": "2021-01-01 00:00:00",
+ "to_time": "2021-01-01 08:00:00",
+ "completed_qty": 1,
+ "employee": employee,
+ },
+ )
jc1.save()
# add a new entry in same time slice
- jc2.append("time_logs", {
- "from_time": "2021-01-01 00:01:00",
- "to_time": "2021-01-01 06:00:00",
- "completed_qty": 1,
- "employee": employee,
- })
+ jc2.append(
+ "time_logs",
+ {
+ "from_time": "2021-01-01 00:01:00",
+ "to_time": "2021-01-01 06:00:00",
+ "completed_qty": 1,
+ "employee": employee,
+ },
+ )
self.assertRaises(OverlapError, jc2.save)
def test_job_card_multiple_materials_transfer(self):
"Test transferring RMs separately against Job Card with multiple RMs."
+ make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100)
make_stock_entry(
- item_code="_Test Item",
- target="Stores - _TC",
- qty=10,
- basic_rate=100
- )
- make_stock_entry(
- item_code="_Test Item Home Desktop Manufactured",
- target="Stores - _TC",
- qty=6,
- basic_rate=100
+ item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=6, basic_rate=100
)
- job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
+ job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
job_card = frappe.get_doc("Job Card", job_card_name)
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
- del transfer_entry_1.items[1] # transfer only 1 of 2 RMs
+ del transfer_entry_1.items[1] # transfer only 1 of 2 RMs
transfer_entry_1.insert()
transfer_entry_1.submit()
@@ -162,13 +167,14 @@ class TestJobCard(ERPNextTestCase):
def test_job_card_excess_material_transfer(self):
"Test transferring more than required RM against Job Card."
- make_stock_entry(item_code="_Test Item", target="Stores - _TC",
- qty=25, basic_rate=100)
- make_stock_entry(item_code="_Test Item Home Desktop Manufactured",
- target="Stores - _TC", qty=15, basic_rate=100)
+ make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100)
+ make_stock_entry(
+ item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100
+ )
- job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
+ job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
job_card = frappe.get_doc("Job Card", job_card_name)
+ self.assertEqual(job_card.status, "Open")
# fully transfer both RMs
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
@@ -192,11 +198,10 @@ class TestJobCard(ERPNextTestCase):
transfer_entry_3 = make_stock_entry_from_jc(job_card_name)
self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
- job_card.append("time_logs", {
- "from_time": "2021-01-01 00:01:00",
- "to_time": "2021-01-01 06:00:00",
- "completed_qty": 2
- })
+ job_card.append(
+ "time_logs",
+ {"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 2},
+ )
job_card.save()
job_card.submit()
@@ -206,12 +211,12 @@ class TestJobCard(ERPNextTestCase):
def test_job_card_partial_material_transfer(self):
"Test partial material transfer against Job Card"
- make_stock_entry(item_code="_Test Item", target="Stores - _TC",
- qty=25, basic_rate=100)
- make_stock_entry(item_code="_Test Item Home Desktop Manufactured",
- target="Stores - _TC", qty=15, basic_rate=100)
+ make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100)
+ make_stock_entry(
+ item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100
+ )
- job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
+ job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
job_card = frappe.get_doc("Job Card", job_card_name)
# partially transfer
@@ -241,15 +246,14 @@ class TestJobCard(ERPNextTestCase):
def test_job_card_material_transfer_correctness(self):
"""
- 1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card
- 2. Test impact of changing 'For Qty' in such a Stock Entry
+ 1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card
+ 2. Test impact of changing 'For Qty' in such a Stock Entry
"""
create_bom_with_multiple_operations()
work_order = make_wo_with_transfer_against_jc()
job_card_name = frappe.db.get_value(
- "Job Card",
- {"work_order": work_order.name,"operation": "Test Operation A"}
+ "Job Card", {"work_order": work_order.name, "operation": "Test Operation A"}
)
job_card = frappe.get_doc("Job Card", job_card_name)
@@ -275,6 +279,7 @@ class TestJobCard(ERPNextTestCase):
# rollback via tearDown method
+
def create_bom_with_multiple_operations():
"Create a BOM with multiple operations and Material Transfer against Job Card"
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
@@ -286,19 +291,22 @@ def create_bom_with_multiple_operations():
"operation": "Test Operation A",
"workstation": "_Test Workstation A",
"hour_rate_rent": 300,
- "time_in_mins": 60
+ "time_in_mins": 60,
}
make_workstation(row)
make_operation(row)
- bom_doc.append("operations", {
- "operation": "Test Operation A",
- "description": "Test Operation A",
- "workstation": "_Test Workstation A",
- "hour_rate": 300,
- "time_in_mins": 60,
- "operating_cost": 100
- })
+ bom_doc.append(
+ "operations",
+ {
+ "operation": "Test Operation A",
+ "description": "Test Operation A",
+ "workstation": "_Test Workstation A",
+ "hour_rate": 300,
+ "time_in_mins": 60,
+ "operating_cost": 100,
+ },
+ )
bom_doc.transfer_material_against = "Job Card"
bom_doc.save()
@@ -306,6 +314,7 @@ def create_bom_with_multiple_operations():
return bom_doc
+
def make_wo_with_transfer_against_jc():
"Create a WO with multiple operations and Material Transfer against Job Card"
@@ -314,7 +323,7 @@ def make_wo_with_transfer_against_jc():
qty=4,
transfer_material_against="Job Card",
source_warehouse="Stores - _TC",
- do_not_submit=True
+ do_not_submit=True,
)
work_order.required_items[0].operation = "Test Operation A"
work_order.required_items[1].operation = "_Test Operation 1"
@@ -322,8 +331,9 @@ def make_wo_with_transfer_against_jc():
return work_order
+
def make_bom_for_jc_tests():
- test_records = frappe.get_test_records('BOM')
+ test_records = frappe.get_test_records("BOM")
bom = frappe.copy_doc(test_records[2])
bom.set_rate_of_sub_assembly_item_based_on_bom = 0
bom.rm_cost_as_per = "Valuation Rate"
diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py
index c919e8bef1d..730a8575247 100644
--- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py
+++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py
@@ -11,14 +11,19 @@ from frappe.utils import cint
class ManufacturingSettings(Document):
pass
+
def get_mins_between_operations():
- return relativedelta(minutes=cint(frappe.db.get_single_value("Manufacturing Settings",
- "mins_between_operations")) or 10)
+ return relativedelta(
+ minutes=cint(frappe.db.get_single_value("Manufacturing Settings", "mins_between_operations"))
+ or 10
+ )
+
@frappe.whitelist()
def is_material_consumption_enabled():
- if not hasattr(frappe.local, 'material_consumption'):
- frappe.local.material_consumption = cint(frappe.db.get_single_value('Manufacturing Settings',
- 'material_consumption'))
+ if not hasattr(frappe.local, "material_consumption"):
+ frappe.local.material_consumption = cint(
+ frappe.db.get_single_value("Manufacturing Settings", "material_consumption")
+ )
return frappe.local.material_consumption
diff --git a/erpnext/manufacturing/doctype/operation/operation.py b/erpnext/manufacturing/doctype/operation/operation.py
index 41726f30cf5..9c8f9ac8d05 100644
--- a/erpnext/manufacturing/doctype/operation/operation.py
+++ b/erpnext/manufacturing/doctype/operation/operation.py
@@ -19,12 +19,14 @@ class Operation(Document):
operation_list = []
for row in self.sub_operations:
if row.operation in operation_list:
- frappe.throw(_("The operation {0} can not add multiple times")
- .format(frappe.bold(row.operation)))
+ frappe.throw(
+ _("The operation {0} can not add multiple times").format(frappe.bold(row.operation))
+ )
if self.name == row.operation:
- frappe.throw(_("The operation {0} can not be the sub operation")
- .format(frappe.bold(row.operation)))
+ frappe.throw(
+ _("The operation {0} can not be the sub operation").format(frappe.bold(row.operation))
+ )
operation_list.append(row.operation)
diff --git a/erpnext/manufacturing/doctype/operation/operation_dashboard.py b/erpnext/manufacturing/doctype/operation/operation_dashboard.py
index 4a548a64709..8dc901a2968 100644
--- a/erpnext/manufacturing/doctype/operation/operation_dashboard.py
+++ b/erpnext/manufacturing/doctype/operation/operation_dashboard.py
@@ -1,14 +1,8 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'operation',
- 'transactions': [
- {
- 'label': _('Manufacture'),
- 'items': ['BOM', 'Work Order', 'Job Card']
- }
- ]
+ "fieldname": "operation",
+ "transactions": [{"label": _("Manufacture"), "items": ["BOM", "Work Order", "Job Card"]}],
}
diff --git a/erpnext/manufacturing/doctype/operation/test_operation.py b/erpnext/manufacturing/doctype/operation/test_operation.py
index e511084e7d0..ce9f8e06495 100644
--- a/erpnext/manufacturing/doctype/operation/test_operation.py
+++ b/erpnext/manufacturing/doctype/operation/test_operation.py
@@ -5,11 +5,13 @@ import unittest
import frappe
-test_records = frappe.get_test_records('Operation')
+test_records = frappe.get_test_records("Operation")
+
class TestOperation(unittest.TestCase):
pass
+
def make_operation(*args, **kwargs):
args = args if args else kwargs
if isinstance(args, tuple):
@@ -18,11 +20,9 @@ def make_operation(*args, **kwargs):
args = frappe._dict(args)
if not frappe.db.exists("Operation", args.operation):
- doc = frappe.get_doc({
- "doctype": "Operation",
- "name": args.operation,
- "workstation": args.workstation
- })
+ doc = frappe.get_doc(
+ {"doctype": "Operation", "name": args.operation, "workstation": args.workstation}
+ )
doc.insert()
return doc
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index f3ded994814..5653e1be75d 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -2,6 +2,13 @@
// For license information, please see license.txt
frappe.ui.form.on('Production Plan', {
+
+ before_save: function(frm) {
+ // preserve temporary names on production plan item to re-link sub-assembly items
+ frm.doc.po_items.forEach(item => {
+ item.temporary_name = item.name;
+ });
+ },
setup: function(frm) {
frm.custom_make_buttons = {
'Work Order': 'Work Order / Subcontract PO',
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 56cf2b4f08a..23b32379413 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -189,7 +189,7 @@
"label": "Select Items to Manufacture"
},
{
- "depends_on": "get_items_from",
+ "depends_on": "eval:doc.get_items_from && doc.docstatus == 0",
"fieldname": "get_items",
"fieldtype": "Button",
"label": "Get Finished Goods for Manufacture"
@@ -197,6 +197,7 @@
{
"fieldname": "po_items",
"fieldtype": "Table",
+ "label": "Assembly Items",
"no_copy": 1,
"options": "Production Plan Item",
"reqd": 1
@@ -357,6 +358,7 @@
"options": "Production Plan Sub Assembly Item"
},
{
+ "depends_on": "eval:doc.po_items && doc.po_items.length && doc.docstatus == 0",
"fieldname": "get_sub_assembly_items",
"fieldtype": "Button",
"label": "Get Sub Assembly Items"
@@ -376,7 +378,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-09-06 18:35:59.642232",
+ "modified": "2022-03-25 09:15:25.017664",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",
@@ -397,5 +399,6 @@
}
],
"sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 60771592da5..ff739e9d72b 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -32,10 +32,11 @@ class ProductionPlan(Document):
self.set_pending_qty_in_row_without_reference()
self.calculate_total_planned_qty()
self.set_status()
+ self._rename_temporary_references()
def set_pending_qty_in_row_without_reference(self):
"Set Pending Qty in independent rows (not from SO or MR)."
- if self.docstatus > 0: # set only to initialise value before submit
+ if self.docstatus > 0: # set only to initialise value before submit
return
for item in self.po_items:
@@ -48,7 +49,7 @@ class ProductionPlan(Document):
self.total_planned_qty += flt(d.planned_qty)
def validate_data(self):
- for d in self.get('po_items'):
+ for d in self.get("po_items"):
if not d.bom_no:
frappe.throw(_("Please select BOM for Item in Row {0}").format(d.idx))
else:
@@ -57,9 +58,21 @@ class ProductionPlan(Document):
if not flt(d.planned_qty):
frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx))
+ def _rename_temporary_references(self):
+ """po_items and sub_assembly_items items are both constructed client side without saving.
+
+ Attempt to fix linkages by using temporary names to map final row names.
+ """
+ new_name_map = {d.temporary_name: d.name for d in self.po_items if d.temporary_name}
+ actual_names = {d.name for d in self.po_items}
+
+ for sub_assy in self.sub_assembly_items:
+ if sub_assy.production_plan_item not in actual_names:
+ sub_assy.production_plan_item = new_name_map.get(sub_assy.production_plan_item)
+
@frappe.whitelist()
def get_open_sales_orders(self):
- """ Pull sales orders which are pending to deliver based on criteria selected"""
+ """Pull sales orders which are pending to deliver based on criteria selected"""
open_so = get_sales_orders(self)
if open_so:
@@ -68,20 +81,23 @@ class ProductionPlan(Document):
frappe.msgprint(_("Sales orders are not available for production"))
def add_so_in_table(self, open_so):
- """ Add sales orders in the table"""
- self.set('sales_orders', [])
+ """Add sales orders in the table"""
+ self.set("sales_orders", [])
for data in open_so:
- self.append('sales_orders', {
- 'sales_order': data.name,
- 'sales_order_date': data.transaction_date,
- 'customer': data.customer,
- 'grand_total': data.base_grand_total
- })
+ self.append(
+ "sales_orders",
+ {
+ "sales_order": data.name,
+ "sales_order_date": data.transaction_date,
+ "customer": data.customer,
+ "grand_total": data.base_grand_total,
+ },
+ )
@frappe.whitelist()
def get_pending_material_requests(self):
- """ Pull Material Requests that are pending based on criteria selected"""
+ """Pull Material Requests that are pending based on criteria selected"""
mr_filter = item_filter = ""
if self.from_date:
mr_filter += " and mr.transaction_date >= %(from_date)s"
@@ -93,7 +109,8 @@ class ProductionPlan(Document):
if self.item_code:
item_filter += " and mr_item.item_code = %(item)s"
- pending_mr = frappe.db.sql("""
+ pending_mr = frappe.db.sql(
+ """
select distinct mr.name, mr.transaction_date
from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
where mr_item.parent = mr.name
@@ -102,29 +119,34 @@ class ProductionPlan(Document):
and mr_item.qty > ifnull(mr_item.ordered_qty,0) {0} {1}
and (exists (select name from `tabBOM` bom where bom.item=mr_item.item_code
and bom.is_active = 1))
- """.format(mr_filter, item_filter), {
+ """.format(
+ mr_filter, item_filter
+ ),
+ {
"from_date": self.from_date,
"to_date": self.to_date,
"warehouse": self.warehouse,
"item": self.item_code,
- "company": self.company
- }, as_dict=1)
+ "company": self.company,
+ },
+ as_dict=1,
+ )
self.add_mr_in_table(pending_mr)
def add_mr_in_table(self, pending_mr):
- """ Add Material Requests in the table"""
- self.set('material_requests', [])
+ """Add Material Requests in the table"""
+ self.set("material_requests", [])
for data in pending_mr:
- self.append('material_requests', {
- 'material_request': data.name,
- 'material_request_date': data.transaction_date
- })
+ self.append(
+ "material_requests",
+ {"material_request": data.name, "material_request_date": data.transaction_date},
+ )
@frappe.whitelist()
def get_items(self):
- self.set('po_items', [])
+ self.set("po_items", [])
if self.get_items_from == "Sales Order":
self.get_so_items()
@@ -139,10 +161,12 @@ class ProductionPlan(Document):
def get_bom_item(self):
"""Check if Item or if its Template has a BOM."""
bom_item = None
- has_bom = frappe.db.exists({'doctype': 'BOM', 'item': self.item_code, 'docstatus': 1})
+ has_bom = frappe.db.exists({"doctype": "BOM", "item": self.item_code, "docstatus": 1})
if not has_bom:
- template_item = frappe.db.get_value('Item', self.item_code, ['variant_of'])
- bom_item = "bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item
+ template_item = frappe.db.get_value("Item", self.item_code, ["variant_of"])
+ bom_item = (
+ "bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item
+ )
return bom_item
def get_so_items(self):
@@ -154,11 +178,12 @@ class ProductionPlan(Document):
item_condition = ""
bom_item = "bom.item = so_item.item_code"
- if self.item_code and frappe.db.exists('Item', self.item_code):
+ if self.item_code and frappe.db.exists("Item", self.item_code):
bom_item = self.get_bom_item() or bom_item
- item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code))
+ item_condition = " and so_item.item_code = {0}".format(frappe.db.escape(self.item_code))
- items = frappe.db.sql("""
+ items = frappe.db.sql(
+ """
select
distinct parent, item_code, warehouse,
(qty - work_order_qty) * conversion_factor as pending_qty,
@@ -168,16 +193,17 @@ class ProductionPlan(Document):
where
parent in (%s) and docstatus = 1 and qty > work_order_qty
and exists (select name from `tabBOM` bom where %s
- and bom.is_active = 1) %s""" %
- (", ".join(["%s"] * len(so_list)),
- bom_item,
- item_condition),
- tuple(so_list), as_dict=1)
+ and bom.is_active = 1) %s"""
+ % (", ".join(["%s"] * len(so_list)), bom_item, item_condition),
+ tuple(so_list),
+ as_dict=1,
+ )
if self.item_code:
- item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code))
+ item_condition = " and so_item.item_code = {0}".format(frappe.db.escape(self.item_code))
- packed_items = frappe.db.sql("""select distinct pi.parent, pi.item_code, pi.warehouse as warehouse,
+ packed_items = frappe.db.sql(
+ """select distinct pi.parent, pi.item_code, pi.warehouse as warehouse,
(((so_item.qty - so_item.work_order_qty) * pi.qty) / so_item.qty)
as pending_qty, pi.parent_item, pi.description, so_item.name
from `tabSales Order Item` so_item, `tabPacked Item` pi
@@ -185,16 +211,23 @@ class ProductionPlan(Document):
and pi.parent_item = so_item.item_code
and so_item.parent in (%s) and so_item.qty > so_item.work_order_qty
and exists (select name from `tabBOM` bom where bom.item=pi.item_code
- and bom.is_active = 1) %s""" % \
- (", ".join(["%s"] * len(so_list)), item_condition), tuple(so_list), as_dict=1)
+ and bom.is_active = 1) %s"""
+ % (", ".join(["%s"] * len(so_list)), item_condition),
+ tuple(so_list),
+ as_dict=1,
+ )
self.add_items(items + packed_items)
self.calculate_total_planned_qty()
def get_mr_items(self):
# Check for empty table or empty rows
- if not self.get("material_requests") or not self.get_so_mr_list("material_request", "material_requests"):
- frappe.throw(_("Please fill the Material Requests table"), title=_("Material Requests Required"))
+ if not self.get("material_requests") or not self.get_so_mr_list(
+ "material_request", "material_requests"
+ ):
+ frappe.throw(
+ _("Please fill the Material Requests table"), title=_("Material Requests Required")
+ )
mr_list = self.get_so_mr_list("material_request", "material_requests")
@@ -202,13 +235,17 @@ class ProductionPlan(Document):
if self.item_code:
item_condition = " and mr_item.item_code ={0}".format(frappe.db.escape(self.item_code))
- items = frappe.db.sql("""select distinct parent, name, item_code, warehouse, description,
+ items = frappe.db.sql(
+ """select distinct parent, name, item_code, warehouse, description,
(qty - ordered_qty) * conversion_factor as pending_qty
from `tabMaterial Request Item` mr_item
where parent in (%s) and docstatus = 1 and qty > ordered_qty
and exists (select name from `tabBOM` bom where bom.item=mr_item.item_code
- and bom.is_active = 1) %s""" % \
- (", ".join(["%s"] * len(mr_list)), item_condition), tuple(mr_list), as_dict=1)
+ and bom.is_active = 1) %s"""
+ % (", ".join(["%s"] * len(mr_list)), item_condition),
+ tuple(mr_list),
+ as_dict=1,
+ )
self.add_items(items)
self.calculate_total_planned_qty()
@@ -219,37 +256,36 @@ class ProductionPlan(Document):
item_details = get_item_details(data.item_code)
if self.combine_items:
if item_details.bom_no in refs:
- refs[item_details.bom_no]['so_details'].append({
- 'sales_order': data.parent,
- 'sales_order_item': data.name,
- 'qty': data.pending_qty
- })
- refs[item_details.bom_no]['qty'] += data.pending_qty
+ refs[item_details.bom_no]["so_details"].append(
+ {"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty}
+ )
+ refs[item_details.bom_no]["qty"] += data.pending_qty
continue
else:
refs[item_details.bom_no] = {
- 'qty': data.pending_qty,
- 'po_item_ref': data.name,
- 'so_details': []
+ "qty": data.pending_qty,
+ "po_item_ref": data.name,
+ "so_details": [],
}
- refs[item_details.bom_no]['so_details'].append({
- 'sales_order': data.parent,
- 'sales_order_item': data.name,
- 'qty': data.pending_qty
- })
+ refs[item_details.bom_no]["so_details"].append(
+ {"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty}
+ )
- pi = self.append('po_items', {
- 'warehouse': data.warehouse,
- 'item_code': data.item_code,
- 'description': data.description or item_details.description,
- 'stock_uom': item_details and item_details.stock_uom or '',
- 'bom_no': item_details and item_details.bom_no or '',
- 'planned_qty': data.pending_qty,
- 'pending_qty': data.pending_qty,
- 'planned_start_date': now_datetime(),
- 'product_bundle_item': data.parent_item
- })
+ pi = self.append(
+ "po_items",
+ {
+ "warehouse": data.warehouse,
+ "item_code": data.item_code,
+ "description": data.description or item_details.description,
+ "stock_uom": item_details and item_details.stock_uom or "",
+ "bom_no": item_details and item_details.bom_no or "",
+ "planned_qty": data.pending_qty,
+ "pending_qty": data.pending_qty,
+ "planned_start_date": now_datetime(),
+ "product_bundle_item": data.parent_item,
+ },
+ )
pi._set_defaults()
if self.get_items_from == "Sales Order":
@@ -264,20 +300,23 @@ class ProductionPlan(Document):
if refs:
for po_item in self.po_items:
- po_item.planned_qty = refs[po_item.bom_no]['qty']
- po_item.pending_qty = refs[po_item.bom_no]['qty']
- po_item.sales_order = ''
+ po_item.planned_qty = refs[po_item.bom_no]["qty"]
+ po_item.pending_qty = refs[po_item.bom_no]["qty"]
+ po_item.sales_order = ""
self.add_pp_ref(refs)
def add_pp_ref(self, refs):
for bom_no in refs:
- for so_detail in refs[bom_no]['so_details']:
- self.append('prod_plan_references', {
- 'item_reference': refs[bom_no]['po_item_ref'],
- 'sales_order': so_detail['sales_order'],
- 'sales_order_item': so_detail['sales_order_item'],
- 'qty': so_detail['qty']
- })
+ for so_detail in refs[bom_no]["so_details"]:
+ self.append(
+ "prod_plan_references",
+ {
+ "item_reference": refs[bom_no]["po_item_ref"],
+ "sales_order": so_detail["sales_order"],
+ "sales_order_item": so_detail["sales_order_item"],
+ "qty": so_detail["qty"],
+ },
+ )
def calculate_total_produced_qty(self):
self.total_produced_qty = 0
@@ -295,27 +334,24 @@ class ProductionPlan(Document):
self.calculate_total_produced_qty()
self.set_status()
- self.db_set('status', self.status)
+ self.db_set("status", self.status)
def on_cancel(self):
- self.db_set('status', 'Cancelled')
+ self.db_set("status", "Cancelled")
self.delete_draft_work_order()
def delete_draft_work_order(self):
- for d in frappe.get_all('Work Order', fields = ["name"],
- filters = {'docstatus': 0, 'production_plan': ("=", self.name)}):
- frappe.delete_doc('Work Order', d.name)
+ for d in frappe.get_all(
+ "Work Order", fields=["name"], filters={"docstatus": 0, "production_plan": ("=", self.name)}
+ ):
+ frappe.delete_doc("Work Order", d.name)
@frappe.whitelist()
def set_status(self, close=None):
- self.status = {
- 0: 'Draft',
- 1: 'Submitted',
- 2: 'Cancelled'
- }.get(self.docstatus)
+ self.status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}.get(self.docstatus)
if close:
- self.db_set('status', 'Closed')
+ self.db_set("status", "Closed")
return
if self.total_produced_qty > 0:
@@ -323,12 +359,12 @@ class ProductionPlan(Document):
if self.all_items_completed():
self.status = "Completed"
- if self.status != 'Completed':
+ if self.status != "Completed":
self.update_ordered_status()
self.update_requested_status()
if close is not None:
- self.db_set('status', self.status)
+ self.db_set("status", self.status)
def update_ordered_status(self):
update_status = False
@@ -336,8 +372,8 @@ class ProductionPlan(Document):
if d.planned_qty == d.ordered_qty:
update_status = True
- if update_status and self.status != 'Completed':
- self.status = 'In Process'
+ if update_status and self.status != "Completed":
+ self.status = "In Process"
def update_requested_status(self):
if not self.mr_items:
@@ -349,44 +385,44 @@ class ProductionPlan(Document):
update_status = False
if update_status:
- self.status = 'Material Requested'
+ self.status = "Material Requested"
def get_production_items(self):
item_dict = {}
for d in self.po_items:
item_details = {
- "production_item" : d.item_code,
- "use_multi_level_bom" : d.include_exploded_items,
- "sales_order" : d.sales_order,
- "sales_order_item" : d.sales_order_item,
- "material_request" : d.material_request,
- "material_request_item" : d.material_request_item,
- "bom_no" : d.bom_no,
- "description" : d.description,
- "stock_uom" : d.stock_uom,
- "company" : self.company,
- "fg_warehouse" : d.warehouse,
- "production_plan" : self.name,
- "production_plan_item" : d.name,
- "product_bundle_item" : d.product_bundle_item,
- "planned_start_date" : d.planned_start_date,
- "project" : self.project
+ "production_item": d.item_code,
+ "use_multi_level_bom": d.include_exploded_items,
+ "sales_order": d.sales_order,
+ "sales_order_item": d.sales_order_item,
+ "material_request": d.material_request,
+ "material_request_item": d.material_request_item,
+ "bom_no": d.bom_no,
+ "description": d.description,
+ "stock_uom": d.stock_uom,
+ "company": self.company,
+ "fg_warehouse": d.warehouse,
+ "production_plan": self.name,
+ "production_plan_item": d.name,
+ "product_bundle_item": d.product_bundle_item,
+ "planned_start_date": d.planned_start_date,
+ "project": self.project,
}
- if not item_details['project'] and d.sales_order:
- item_details['project'] = frappe.get_cached_value("Sales Order", d.sales_order, "project")
+ if not item_details["project"] and d.sales_order:
+ item_details["project"] = frappe.get_cached_value("Sales Order", d.sales_order, "project")
if self.get_items_from == "Material Request":
- item_details.update({
- "qty": d.planned_qty
- })
+ item_details.update({"qty": d.planned_qty})
item_dict[(d.item_code, d.material_request_item, d.warehouse)] = item_details
else:
- item_details.update({
- "qty": flt(item_dict.get((d.item_code, d.sales_order, d.warehouse),{})
- .get("qty")) + (flt(d.planned_qty) - flt(d.ordered_qty))
- })
+ item_details.update(
+ {
+ "qty": flt(item_dict.get((d.item_code, d.sales_order, d.warehouse), {}).get("qty"))
+ + (flt(d.planned_qty) - flt(d.ordered_qty))
+ }
+ )
item_dict[(d.item_code, d.sales_order, d.warehouse)] = item_details
return item_dict
@@ -402,15 +438,15 @@ class ProductionPlan(Document):
self.make_work_order_for_finished_goods(wo_list, default_warehouses)
self.make_work_order_for_subassembly_items(wo_list, subcontracted_po, default_warehouses)
self.make_subcontracted_purchase_order(subcontracted_po, po_list)
- self.show_list_created_message('Work Order', wo_list)
- self.show_list_created_message('Purchase Order', po_list)
+ self.show_list_created_message("Work Order", wo_list)
+ self.show_list_created_message("Purchase Order", po_list)
def make_work_order_for_finished_goods(self, wo_list, default_warehouses):
items_data = self.get_production_items()
for key, item in items_data.items():
if self.sub_assembly_items:
- item['use_multi_level_bom'] = 0
+ item["use_multi_level_bom"] = 0
set_default_warehouses(item, default_warehouses)
work_order = self.create_work_order(item)
@@ -419,13 +455,14 @@ class ProductionPlan(Document):
def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po, default_warehouses):
for row in self.sub_assembly_items:
- if row.type_of_manufacturing == 'Subcontract':
+ if row.type_of_manufacturing == "Subcontract":
subcontracted_po.setdefault(row.supplier, []).append(row)
continue
work_order_data = {
- 'wip_warehouse': default_warehouses.get('wip_warehouse'),
- 'fg_warehouse': default_warehouses.get('fg_warehouse')
+ "wip_warehouse": default_warehouses.get("wip_warehouse"),
+ "fg_warehouse": default_warehouses.get("fg_warehouse"),
+ "company": self.get("company"),
}
self.prepare_data_for_sub_assembly_items(row, work_order_data)
@@ -434,41 +471,60 @@ class ProductionPlan(Document):
wo_list.append(work_order)
def prepare_data_for_sub_assembly_items(self, row, wo_data):
- for field in ["production_item", "item_name", "qty", "fg_warehouse",
- "description", "bom_no", "stock_uom", "bom_level",
- "production_plan_item", "schedule_date"]:
+ for field in [
+ "production_item",
+ "item_name",
+ "qty",
+ "fg_warehouse",
+ "description",
+ "bom_no",
+ "stock_uom",
+ "bom_level",
+ "production_plan_item",
+ "schedule_date",
+ ]:
if row.get(field):
wo_data[field] = row.get(field)
- wo_data.update({
- "use_multi_level_bom": 0,
- "production_plan": self.name,
- "production_plan_sub_assembly_item": row.name
- })
+ wo_data.update(
+ {
+ "use_multi_level_bom": 0,
+ "production_plan": self.name,
+ "production_plan_sub_assembly_item": row.name,
+ }
+ )
def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders):
if not subcontracted_po:
return
for supplier, po_list in subcontracted_po.items():
- po = frappe.new_doc('Purchase Order')
+ po = frappe.new_doc("Purchase Order")
+ po.company = self.company
po.supplier = supplier
po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
- po.is_subcontracted = 'Yes'
+ po.is_subcontracted = "Yes"
for row in po_list:
po_data = {
- 'item_code': row.production_item,
- 'warehouse': row.fg_warehouse,
- 'production_plan_sub_assembly_item': row.name,
- 'bom': row.bom_no,
- 'production_plan': self.name
+ "item_code": row.production_item,
+ "warehouse": row.fg_warehouse,
+ "production_plan_sub_assembly_item": row.name,
+ "bom": row.bom_no,
+ "production_plan": self.name,
}
- for field in ['schedule_date', 'qty', 'uom', 'stock_uom', 'item_name',
- 'description', 'production_plan_item']:
+ for field in [
+ "schedule_date",
+ "qty",
+ "uom",
+ "stock_uom",
+ "item_name",
+ "description",
+ "production_plan_item",
+ ]:
po_data[field] = row.get(field)
- po.append('items', po_data)
+ po.append("items", po_data)
po.set_missing_values()
po.flags.ignore_mandatory = True
@@ -490,7 +546,7 @@ class ProductionPlan(Document):
wo = frappe.new_doc("Work Order")
wo.update(item)
- wo.planned_start_date = item.get('planned_start_date') or item.get('schedule_date')
+ wo.planned_start_date = item.get("planned_start_date") or item.get("schedule_date")
if item.get("warehouse"):
wo.fg_warehouse = item.get("warehouse")
@@ -508,54 +564,60 @@ class ProductionPlan(Document):
@frappe.whitelist()
def make_material_request(self):
- '''Create Material Requests grouped by Sales Order and Material Request Type'''
+ """Create Material Requests grouped by Sales Order and Material Request Type"""
material_request_list = []
material_request_map = {}
for item in self.mr_items:
- item_doc = frappe.get_cached_doc('Item', item.item_code)
+ item_doc = frappe.get_cached_doc("Item", item.item_code)
material_request_type = item.material_request_type or item_doc.default_material_request_type
# key for Sales Order:Material Request Type:Customer
- key = '{}:{}:{}'.format(item.sales_order, material_request_type, item_doc.customer or '')
+ key = "{}:{}:{}".format(item.sales_order, material_request_type, item_doc.customer or "")
schedule_date = add_days(nowdate(), cint(item_doc.lead_time_days))
if not key in material_request_map:
# make a new MR for the combination
material_request_map[key] = frappe.new_doc("Material Request")
material_request = material_request_map[key]
- material_request.update({
- "transaction_date": nowdate(),
- "status": "Draft",
- "company": self.company,
- 'material_request_type': material_request_type,
- 'customer': item_doc.customer or ''
- })
+ material_request.update(
+ {
+ "transaction_date": nowdate(),
+ "status": "Draft",
+ "company": self.company,
+ "material_request_type": material_request_type,
+ "customer": item_doc.customer or "",
+ }
+ )
material_request_list.append(material_request)
else:
material_request = material_request_map[key]
# add item
- material_request.append("items", {
- "item_code": item.item_code,
- "from_warehouse": item.from_warehouse,
- "qty": item.quantity,
- "schedule_date": schedule_date,
- "warehouse": item.warehouse,
- "sales_order": item.sales_order,
- 'production_plan': self.name,
- 'material_request_plan_item': item.name,
- "project": frappe.db.get_value("Sales Order", item.sales_order, "project") \
- if item.sales_order else None
- })
+ material_request.append(
+ "items",
+ {
+ "item_code": item.item_code,
+ "from_warehouse": item.from_warehouse,
+ "qty": item.quantity,
+ "schedule_date": schedule_date,
+ "warehouse": item.warehouse,
+ "sales_order": item.sales_order,
+ "production_plan": self.name,
+ "material_request_plan_item": item.name,
+ "project": frappe.db.get_value("Sales Order", item.sales_order, "project")
+ if item.sales_order
+ else None,
+ },
+ )
for material_request in material_request_list:
# submit
material_request.flags.ignore_permissions = 1
material_request.run_method("set_missing_values")
- if self.get('submit_material_request'):
+ if self.get("submit_material_request"):
material_request.submit()
else:
material_request.save()
@@ -563,10 +625,12 @@ class ProductionPlan(Document):
frappe.flags.mute_messages = False
if material_request_list:
- material_request_list = ["""{1}""".format(m.name, m.name) \
- for m in material_request_list]
+ material_request_list = [
+ """{1}""".format(m.name, m.name)
+ for m in material_request_list
+ ]
msgprint(_("{0} created").format(comma_and(material_request_list)))
- else :
+ else:
msgprint(_("No material request created"))
@frappe.whitelist()
@@ -577,7 +641,7 @@ class ProductionPlan(Document):
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
- self.sub_assembly_items.sort(key= lambda d: d.bom_level, reverse=True)
+ self.sub_assembly_items.sort(key=lambda d: d.bom_level, reverse=True)
for idx, row in enumerate(self.sub_assembly_items, start=1):
row.idx = idx
@@ -587,14 +651,16 @@ class ProductionPlan(Document):
data.production_plan_item = row.name
data.fg_warehouse = row.warehouse
data.schedule_date = row.planned_start_date
- data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item
- else "In House")
+ data.type_of_manufacturing = manufacturing_type or (
+ "Subcontract" if data.is_sub_contracted_item else "In House"
+ )
self.append("sub_assembly_items", data)
def all_items_completed(self):
- all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001
- for d in self.po_items)
+ all_items_produced = all(
+ flt(d.planned_qty) - flt(d.produced_qty) < 0.000001 for d in self.po_items
+ )
if not all_items_produced:
return False
@@ -611,40 +677,81 @@ class ProductionPlan(Document):
all_work_orders_completed = all(s == "Completed" for s in wo_status)
return all_work_orders_completed
+
@frappe.whitelist()
def download_raw_materials(doc, warehouses=None):
if isinstance(doc, str):
doc = frappe._dict(json.loads(doc))
- item_list = [['Item Code', 'Item Name', 'Description',
- 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
- 'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned Qty',
- 'Reserved Qty for Production', 'Safety Stock', 'Required Qty']]
+ item_list = [
+ [
+ "Item Code",
+ "Item Name",
+ "Description",
+ "Stock UOM",
+ "Warehouse",
+ "Required Qty as per BOM",
+ "Projected Qty",
+ "Available Qty In Hand",
+ "Ordered Qty",
+ "Planned Qty",
+ "Reserved Qty for Production",
+ "Safety Stock",
+ "Required Qty",
+ ]
+ ]
doc.warehouse = None
frappe.flags.show_qty_in_stock_uom = 1
- items = get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True)
+ items = get_items_for_material_requests(
+ doc, warehouses=warehouses, get_parent_warehouse_data=True
+ )
for d in items:
- item_list.append([d.get('item_code'), d.get('item_name'),
- d.get('description'), d.get('stock_uom'), d.get('warehouse'),
- d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'),
- d.get('planned_qty'), d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')])
+ item_list.append(
+ [
+ d.get("item_code"),
+ d.get("item_name"),
+ d.get("description"),
+ d.get("stock_uom"),
+ d.get("warehouse"),
+ d.get("required_bom_qty"),
+ d.get("projected_qty"),
+ d.get("actual_qty"),
+ d.get("ordered_qty"),
+ d.get("planned_qty"),
+ d.get("reserved_qty_for_production"),
+ d.get("safety_stock"),
+ d.get("quantity"),
+ ]
+ )
- if not doc.get('for_warehouse'):
- row = {'item_code': d.get('item_code')}
+ if not doc.get("for_warehouse"):
+ row = {"item_code": d.get("item_code")}
for bin_dict in get_bin_details(row, doc.company, all_warehouse=True):
- if d.get("warehouse") == bin_dict.get('warehouse'):
+ if d.get("warehouse") == bin_dict.get("warehouse"):
continue
- item_list.append(['', '', '', bin_dict.get('warehouse'), '',
- bin_dict.get('projected_qty', 0), bin_dict.get('actual_qty', 0),
- bin_dict.get('ordered_qty', 0), bin_dict.get('reserved_qty_for_production', 0)])
+ item_list.append(
+ [
+ "",
+ "",
+ "",
+ bin_dict.get("warehouse"),
+ "",
+ bin_dict.get("projected_qty", 0),
+ bin_dict.get("actual_qty", 0),
+ bin_dict.get("ordered_qty", 0),
+ bin_dict.get("reserved_qty_for_production", 0),
+ ]
+ )
build_csv_response(item_list, doc.name)
+
def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1):
- for d in frappe.db.sql("""select bei.item_code, item.default_bom as bom,
+ for d in frappe.db.sql(
+ """select bei.item_code, item.default_bom as bom,
ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name,
bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse,
item.default_material_request_type, item.min_order_qty, item_default.default_warehouse,
@@ -660,21 +767,38 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
where
bei.docstatus < 2
and bom.name=%s and item.is_stock_item in (1, {0})
- group by bei.item_code, bei.stock_uom""".format(0 if include_non_stock_items else 1),
- (planned_qty, company, bom_no), as_dict=1):
+ group by bei.item_code, bei.stock_uom""".format(
+ 0 if include_non_stock_items else 1
+ ),
+ (planned_qty, company, bom_no),
+ as_dict=1,
+ ):
if not d.conversion_factor and d.purchase_uom:
d.conversion_factor = get_uom_conversion_factor(d.item_code, d.purchase_uom)
- item_details.setdefault(d.get('item_code'), d)
+ item_details.setdefault(d.get("item_code"), d)
return item_details
-def get_uom_conversion_factor(item_code, uom):
- return frappe.db.get_value('UOM Conversion Detail',
- {'parent': item_code, 'uom': uom}, 'conversion_factor')
-def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_items,
- include_subcontracted_items, parent_qty, planned_qty=1):
- items = frappe.db.sql("""
+def get_uom_conversion_factor(item_code, uom):
+ return frappe.db.get_value(
+ "UOM Conversion Detail", {"parent": item_code, "uom": uom}, "conversion_factor"
+ )
+
+
+def get_subitems(
+ doc,
+ data,
+ item_details,
+ bom_no,
+ company,
+ include_non_stock_items,
+ include_subcontracted_items,
+ parent_qty,
+ planned_qty=1,
+):
+ items = frappe.db.sql(
+ """
SELECT
bom_item.item_code, default_material_request_type, item.item_name,
ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * %(planned_qty)s, 0) as qty,
@@ -694,15 +818,15 @@ def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_ite
bom.name = %(bom)s
and bom_item.docstatus < 2
and item.is_stock_item in (1, {0})
- group by bom_item.item_code""".format(0 if include_non_stock_items else 1),{
- 'bom': bom_no,
- 'parent_qty': parent_qty,
- 'planned_qty': planned_qty,
- 'company': company
- }, as_dict=1)
+ group by bom_item.item_code""".format(
+ 0 if include_non_stock_items else 1
+ ),
+ {"bom": bom_no, "parent_qty": parent_qty, "planned_qty": planned_qty, "company": company},
+ as_dict=1,
+ )
for d in items:
- if not data.get('include_exploded_items') or not d.default_bom:
+ if not data.get("include_exploded_items") or not d.default_bom:
if d.item_code in item_details:
item_details[d.item_code].qty = item_details[d.item_code].qty + d.qty
else:
@@ -711,89 +835,107 @@ def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_ite
item_details[d.item_code] = d
- if data.get('include_exploded_items') and d.default_bom:
- if ((d.default_material_request_type in ["Manufacture", "Purchase"] and
- not d.is_sub_contracted) or (d.is_sub_contracted and include_subcontracted_items)):
+ if data.get("include_exploded_items") and d.default_bom:
+ if (
+ d.default_material_request_type in ["Manufacture", "Purchase"] and not d.is_sub_contracted
+ ) or (d.is_sub_contracted and include_subcontracted_items):
if d.qty > 0:
- get_subitems(doc, data, item_details, d.default_bom, company,
- include_non_stock_items, include_subcontracted_items, d.qty)
+ get_subitems(
+ doc,
+ data,
+ item_details,
+ d.default_bom,
+ company,
+ include_non_stock_items,
+ include_subcontracted_items,
+ d.qty,
+ )
return item_details
-def get_material_request_items(row, sales_order, company,
- ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict):
- total_qty = row['qty']
+
+def get_material_request_items(
+ row, sales_order, company, ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict
+):
+ total_qty = row["qty"]
required_qty = 0
if ignore_existing_ordered_qty or bin_dict.get("projected_qty", 0) < 0:
required_qty = total_qty
elif total_qty > bin_dict.get("projected_qty", 0):
required_qty = total_qty - bin_dict.get("projected_qty", 0)
- if required_qty > 0 and required_qty < row['min_order_qty']:
- required_qty = row['min_order_qty']
+ if required_qty > 0 and required_qty < row["min_order_qty"]:
+ required_qty = row["min_order_qty"]
item_group_defaults = get_item_group_defaults(row.item_code, company)
- if not row['purchase_uom']:
- row['purchase_uom'] = row['stock_uom']
+ if not row["purchase_uom"]:
+ row["purchase_uom"] = row["stock_uom"]
- if row['purchase_uom'] != row['stock_uom']:
- if not (row['conversion_factor'] or frappe.flags.show_qty_in_stock_uom):
- frappe.throw(_("UOM Conversion factor ({0} -> {1}) not found for item: {2}")
- .format(row['purchase_uom'], row['stock_uom'], row.item_code))
+ if row["purchase_uom"] != row["stock_uom"]:
+ if not (row["conversion_factor"] or frappe.flags.show_qty_in_stock_uom):
+ frappe.throw(
+ _("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format(
+ row["purchase_uom"], row["stock_uom"], row.item_code
+ )
+ )
- required_qty = required_qty / row['conversion_factor']
+ required_qty = required_qty / row["conversion_factor"]
- if frappe.db.get_value("UOM", row['purchase_uom'], "must_be_whole_number"):
+ if frappe.db.get_value("UOM", row["purchase_uom"], "must_be_whole_number"):
required_qty = ceil(required_qty)
if include_safety_stock:
- required_qty += flt(row['safety_stock'])
+ required_qty += flt(row["safety_stock"])
if required_qty > 0:
return {
- 'item_code': row.item_code,
- 'item_name': row.item_name,
- 'quantity': required_qty,
- 'required_bom_qty': total_qty,
- 'stock_uom': row.get("stock_uom"),
- 'warehouse': warehouse or row.get('source_warehouse') \
- or row.get('default_warehouse') or item_group_defaults.get("default_warehouse"),
- 'safety_stock': row.safety_stock,
- 'actual_qty': bin_dict.get("actual_qty", 0),
- 'projected_qty': bin_dict.get("projected_qty", 0),
- 'ordered_qty': bin_dict.get("ordered_qty", 0),
- 'reserved_qty_for_production': bin_dict.get("reserved_qty_for_production", 0),
- 'min_order_qty': row['min_order_qty'],
- 'material_request_type': row.get("default_material_request_type"),
- 'sales_order': sales_order,
- 'description': row.get("description"),
- 'uom': row.get("purchase_uom") or row.get("stock_uom")
+ "item_code": row.item_code,
+ "item_name": row.item_name,
+ "quantity": required_qty,
+ "required_bom_qty": total_qty,
+ "stock_uom": row.get("stock_uom"),
+ "warehouse": warehouse
+ or row.get("source_warehouse")
+ or row.get("default_warehouse")
+ or item_group_defaults.get("default_warehouse"),
+ "safety_stock": row.safety_stock,
+ "actual_qty": bin_dict.get("actual_qty", 0),
+ "projected_qty": bin_dict.get("projected_qty", 0),
+ "ordered_qty": bin_dict.get("ordered_qty", 0),
+ "reserved_qty_for_production": bin_dict.get("reserved_qty_for_production", 0),
+ "min_order_qty": row["min_order_qty"],
+ "material_request_type": row.get("default_material_request_type"),
+ "sales_order": sales_order,
+ "description": row.get("description"),
+ "uom": row.get("purchase_uom") or row.get("stock_uom"),
}
+
def get_sales_orders(self):
so_filter = item_filter = ""
bom_item = "bom.item = so_item.item_code"
date_field_mapper = {
- 'from_date': ('>=', 'so.transaction_date'),
- 'to_date': ('<=', 'so.transaction_date'),
- 'from_delivery_date': ('>=', 'so_item.delivery_date'),
- 'to_delivery_date': ('<=', 'so_item.delivery_date')
+ "from_date": (">=", "so.transaction_date"),
+ "to_date": ("<=", "so.transaction_date"),
+ "from_delivery_date": (">=", "so_item.delivery_date"),
+ "to_delivery_date": ("<=", "so_item.delivery_date"),
}
for field, value in date_field_mapper.items():
if self.get(field):
so_filter += f" and {value[1]} {value[0]} %({field})s"
- for field in ['customer', 'project', 'sales_order_status']:
+ for field in ["customer", "project", "sales_order_status"]:
if self.get(field):
- so_field = 'status' if field == 'sales_order_status' else field
+ so_field = "status" if field == "sales_order_status" else field
so_filter += f" and so.{so_field} = %({field})s"
- if self.item_code and frappe.db.exists('Item', self.item_code):
+ if self.item_code and frappe.db.exists("Item", self.item_code):
bom_item = self.get_bom_item() or bom_item
item_filter += " and so_item.item_code = %(item_code)s"
- open_so = frappe.db.sql(f"""
+ open_so = frappe.db.sql(
+ f"""
select distinct so.name, so.transaction_date, so.customer, so.base_grand_total
from `tabSales Order` so, `tabSales Order Item` so_item
where so_item.parent = so.name
@@ -806,10 +948,14 @@ def get_sales_orders(self):
where pi.parent = so.name and pi.parent_item = so_item.item_code
and exists (select name from `tabBOM` bom where bom.item=pi.item_code
and bom.is_active = 1)))
- """, self.as_dict(), as_dict=1)
+ """,
+ self.as_dict(),
+ as_dict=1,
+ )
return open_so
+
@frappe.whitelist()
def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
if isinstance(row, str):
@@ -818,30 +964,42 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
company = frappe.db.escape(company)
conditions, warehouse = "", ""
- conditions = " and warehouse in (select name from `tabWarehouse` where company = {0})".format(company)
+ conditions = " and warehouse in (select name from `tabWarehouse` where company = {0})".format(
+ company
+ )
if not all_warehouse:
- warehouse = for_warehouse or row.get('source_warehouse') or row.get('default_warehouse')
+ warehouse = for_warehouse or row.get("source_warehouse") or row.get("default_warehouse")
if warehouse:
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
conditions = """ and warehouse in (select name from `tabWarehouse`
where lft >= {0} and rgt <= {1} and name=`tabBin`.warehouse and company = {2})
- """.format(lft, rgt, company)
+ """.format(
+ lft, rgt, company
+ )
- return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty,
+ return frappe.db.sql(
+ """ select ifnull(sum(projected_qty),0) as projected_qty,
ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty,
ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse,
ifnull(sum(planned_qty),0) as planned_qty
from `tabBin` where item_code = %(item_code)s {conditions}
group by item_code, warehouse
- """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1)
+ """.format(
+ conditions=conditions
+ ),
+ {"item_code": row["item_code"]},
+ as_dict=1,
+ )
+
@frappe.whitelist()
def get_so_details(sales_order):
- return frappe.db.get_value("Sales Order", sales_order,
- ['transaction_date', 'customer', 'grand_total'], as_dict=1
+ return frappe.db.get_value(
+ "Sales Order", sales_order, ["transaction_date", "customer", "grand_total"], as_dict=1
)
+
def get_warehouse_list(warehouses):
warehouse_list = []
@@ -849,7 +1007,7 @@ def get_warehouse_list(warehouses):
warehouses = json.loads(warehouses)
for row in warehouses:
- child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse"))
+ child_warehouses = frappe.db.get_descendants("Warehouse", row.get("warehouse"))
if child_warehouses:
warehouse_list.extend(child_warehouses)
else:
@@ -857,6 +1015,7 @@ def get_warehouse_list(warehouses):
return warehouse_list
+
@frappe.whitelist()
def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None):
if isinstance(doc, str):
@@ -865,73 +1024,92 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
if warehouses:
warehouses = list(set(get_warehouse_list(warehouses)))
- if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses:
+ if (
+ doc.get("for_warehouse")
+ and not get_parent_warehouse_data
+ and doc.get("for_warehouse") in warehouses
+ ):
warehouses.remove(doc.get("for_warehouse"))
- doc['mr_items'] = []
+ doc["mr_items"] = []
- po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items')
+ po_items = doc.get("po_items") if doc.get("po_items") else doc.get("items")
# Check for empty table or empty rows
- if not po_items or not [row.get('item_code') for row in po_items if row.get('item_code')]:
- frappe.throw(_("Items to Manufacture are required to pull the Raw Materials associated with it."),
- title=_("Items Required"))
+ if not po_items or not [row.get("item_code") for row in po_items if row.get("item_code")]:
+ frappe.throw(
+ _("Items to Manufacture are required to pull the Raw Materials associated with it."),
+ title=_("Items Required"),
+ )
- company = doc.get('company')
- ignore_existing_ordered_qty = doc.get('ignore_existing_ordered_qty')
- include_safety_stock = doc.get('include_safety_stock')
+ company = doc.get("company")
+ ignore_existing_ordered_qty = doc.get("ignore_existing_ordered_qty")
+ include_safety_stock = doc.get("include_safety_stock")
so_item_details = frappe._dict()
for data in po_items:
if not data.get("include_exploded_items") and doc.get("sub_assembly_items"):
data["include_exploded_items"] = 1
- planned_qty = data.get('required_qty') or data.get('planned_qty')
- ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or ignore_existing_ordered_qty
- warehouse = doc.get('for_warehouse')
+ planned_qty = data.get("required_qty") or data.get("planned_qty")
+ ignore_existing_ordered_qty = (
+ data.get("ignore_existing_ordered_qty") or ignore_existing_ordered_qty
+ )
+ warehouse = doc.get("for_warehouse")
item_details = {}
if data.get("bom") or data.get("bom_no"):
- if data.get('required_qty'):
- bom_no = data.get('bom')
+ if data.get("required_qty"):
+ bom_no = data.get("bom")
include_non_stock_items = 1
- include_subcontracted_items = 1 if data.get('include_exploded_items') else 0
+ include_subcontracted_items = 1 if data.get("include_exploded_items") else 0
else:
- bom_no = data.get('bom_no')
- include_subcontracted_items = doc.get('include_subcontracted_items')
- include_non_stock_items = doc.get('include_non_stock_items')
+ bom_no = data.get("bom_no")
+ include_subcontracted_items = doc.get("include_subcontracted_items")
+ include_non_stock_items = doc.get("include_non_stock_items")
if not planned_qty:
- frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get('idx')))
+ frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx")))
if bom_no:
- if data.get('include_exploded_items') and include_subcontracted_items:
+ if data.get("include_exploded_items") and include_subcontracted_items:
# fetch exploded items from BOM
- item_details = get_exploded_items(item_details,
- company, bom_no, include_non_stock_items, planned_qty=planned_qty)
+ item_details = get_exploded_items(
+ item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty
+ )
else:
- item_details = get_subitems(doc, data, item_details, bom_no, company,
- include_non_stock_items, include_subcontracted_items, 1, planned_qty=planned_qty)
- elif data.get('item_code'):
- item_master = frappe.get_doc('Item', data['item_code']).as_dict()
+ item_details = get_subitems(
+ doc,
+ data,
+ item_details,
+ bom_no,
+ company,
+ include_non_stock_items,
+ include_subcontracted_items,
+ 1,
+ planned_qty=planned_qty,
+ )
+ elif data.get("item_code"):
+ item_master = frappe.get_doc("Item", data["item_code"]).as_dict()
purchase_uom = item_master.purchase_uom or item_master.stock_uom
- conversion_factor = (get_uom_conversion_factor(item_master.name, purchase_uom)
- if item_master.purchase_uom else 1.0)
+ conversion_factor = (
+ get_uom_conversion_factor(item_master.name, purchase_uom) if item_master.purchase_uom else 1.0
+ )
item_details[item_master.name] = frappe._dict(
{
- 'item_name' : item_master.item_name,
- 'default_bom' : doc.bom,
- 'purchase_uom' : purchase_uom,
- 'default_warehouse': item_master.default_warehouse,
- 'min_order_qty' : item_master.min_order_qty,
- 'default_material_request_type' : item_master.default_material_request_type,
- 'qty': planned_qty or 1,
- 'is_sub_contracted' : item_master.is_subcontracted_item,
- 'item_code' : item_master.name,
- 'description' : item_master.description,
- 'stock_uom' : item_master.stock_uom,
- 'conversion_factor' : conversion_factor,
- 'safety_stock': item_master.safety_stock
+ "item_name": item_master.item_name,
+ "default_bom": doc.bom,
+ "purchase_uom": purchase_uom,
+ "default_warehouse": item_master.default_warehouse,
+ "min_order_qty": item_master.min_order_qty,
+ "default_material_request_type": item_master.default_material_request_type,
+ "qty": planned_qty or 1,
+ "is_sub_contracted": item_master.is_subcontracted_item,
+ "item_code": item_master.name,
+ "description": item_master.description,
+ "stock_uom": item_master.stock_uom,
+ "conversion_factor": conversion_factor,
+ "safety_stock": item_master.safety_stock,
}
)
@@ -940,7 +1118,9 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
for item_code, details in iteritems(item_details):
so_item_details.setdefault(sales_order, frappe._dict())
if item_code in so_item_details.get(sales_order, {}):
- so_item_details[sales_order][item_code]['qty'] = so_item_details[sales_order][item_code].get("qty", 0) + flt(details.qty)
+ so_item_details[sales_order][item_code]["qty"] = so_item_details[sales_order][item_code].get(
+ "qty", 0
+ ) + flt(details.qty)
else:
so_item_details[sales_order][item_code] = details
@@ -952,8 +1132,15 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
bin_dict = bin_dict[0] if bin_dict else {}
if details.qty > 0:
- items = get_material_request_items(details, sales_order, company,
- ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict)
+ items = get_material_request_items(
+ details,
+ sales_order,
+ company,
+ ignore_existing_ordered_qty,
+ include_safety_stock,
+ warehouse,
+ bin_dict,
+ )
if items:
mr_items.append(items)
@@ -966,51 +1153,62 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
if not mr_items:
to_enable = frappe.bold(_("Ignore Existing Projected Quantity"))
- warehouse = frappe.bold(doc.get('for_warehouse'))
- message = _("As there are sufficient raw materials, Material Request is not required for Warehouse {0}.").format(warehouse) + "
"
+ warehouse = frappe.bold(doc.get("for_warehouse"))
+ message = (
+ _(
+ "As there are sufficient raw materials, Material Request is not required for Warehouse {0}."
+ ).format(warehouse)
+ + "
'
- msg += _('You must {} your {} in order to have document id of {} length 16.').format(
- bold(_('modify')), bold(_('naming series')), bold(_('maximum'))
+ title = _("Document ID Too Long")
+ msg = _("As you have E-Invoicing enabled, to be able to generate IRN for this invoice")
+ msg += ", "
+ msg += _("document id {} exceed 16 letters.").format(bold(_("should not")))
+ msg += "
"
+ msg += _("You must {} your {} in order to have document id of {} length 16.").format(
+ bold(_("modify")), bold(_("naming series")), bold(_("maximum"))
)
- msg += _('Please account for ammended documents too.')
+ msg += _("Please account for ammended documents too.")
frappe.throw(msg, title=title)
+
def read_json(name):
- file_path = os.path.join(os.path.dirname(__file__), '{name}.json'.format(name=name))
- with open(file_path, 'r') as f:
+ file_path = os.path.join(os.path.dirname(__file__), "{name}.json".format(name=name))
+ with open(file_path, "r") as f:
return cstr(f.read())
+
def get_transaction_details(invoice):
- supply_type = ''
- if invoice.gst_category == 'Registered Regular': supply_type = 'B2B'
- elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP'
- elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP'
- elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP'
+ supply_type = ""
+ if (
+ invoice.gst_category == "Registered Regular" or invoice.gst_category == "Registered Composition"
+ ):
+ supply_type = "B2B"
+ elif invoice.gst_category == "SEZ":
+ if invoice.export_type == "Without Payment of Tax":
+ supply_type = "SEZWOP"
+ else:
+ supply_type = "SEZWP"
+ elif invoice.gst_category == "Overseas":
+ if invoice.export_type == "Without Payment of Tax":
+ supply_type = "EXPWOP"
+ else:
+ supply_type = "EXPWP"
+ elif invoice.gst_category == "Deemed Export":
+ supply_type = "DEXP"
if not supply_type:
- rr, sez, overseas, export = bold('Registered Regular'), bold('SEZ'), bold('Overseas'), bold('Deemed Export')
- frappe.throw(_('GST category should be one of {}, {}, {}, {}').format(rr, sez, overseas, export),
- title=_('Invalid Supply Type'))
+ rr, rc, sez, overseas, export = (
+ bold("Registered Regular"),
+ bold("Registered Composition"),
+ bold("SEZ"),
+ bold("Overseas"),
+ bold("Deemed Export"),
+ )
+ frappe.throw(
+ _("GST category should be one of {}, {}, {}, {}, {}").format(rr, rc, sez, overseas, export),
+ title=_("Invalid Supply Type"),
+ )
+
+ return frappe._dict(
+ dict(tax_scheme="GST", supply_type=supply_type, reverse_charge=invoice.reverse_charge)
+ )
- return frappe._dict(dict(
- tax_scheme='GST',
- supply_type=supply_type,
- reverse_charge=invoice.reverse_charge
- ))
def get_doc_details(invoice):
- if getdate(invoice.posting_date) < getdate('2021-01-01'):
- frappe.throw(_('IRN generation is not allowed for invoices dated before 1st Jan 2021'), title=_('Not Allowed'))
+ if getdate(invoice.posting_date) < getdate("2021-01-01"):
+ frappe.throw(
+ _("IRN generation is not allowed for invoices dated before 1st Jan 2021"),
+ title=_("Not Allowed"),
+ )
- invoice_type = 'CRN' if invoice.is_return else 'INV'
+ invoice_type = "CRN" if invoice.is_return else "INV"
invoice_name = invoice.name
- invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy')
+ invoice_date = format_date(invoice.posting_date, "dd/mm/yyyy")
+
+ return frappe._dict(
+ dict(invoice_type=invoice_type, invoice_name=invoice_name, invoice_date=invoice_date)
+ )
- return frappe._dict(dict(
- invoice_type=invoice_type,
- invoice_name=invoice_name,
- invoice_date=invoice_date
- ))
def validate_address_fields(address, skip_gstin_validation):
- if ((not address.gstin and not skip_gstin_validation)
+ if (
+ (not address.gstin and not skip_gstin_validation)
or not address.city
or not address.pincode
or not address.address_title
or not address.address_line1
- or not address.gst_state_number):
+ or not address.gst_state_number
+ ):
frappe.throw(
- msg=_('Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again.').format(address.name),
- title=_('Missing Address Fields')
+ msg=_(
+ "Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again."
+ ).format(address.name),
+ title=_("Missing Address Fields"),
)
if address.address_line2 and len(address.address_line2) < 2:
# to prevent "The field Address 2 must be a string with a minimum length of 3 and a maximum length of 100"
address.address_line2 = ""
+
def get_party_details(address_name, skip_gstin_validation=False):
- addr = frappe.get_doc('Address', address_name)
+ addr = frappe.get_doc("Address", address_name)
validate_address_fields(addr, skip_gstin_validation)
@@ -162,44 +209,53 @@ def get_party_details(address_name, skip_gstin_validation=False):
# according to einvoice standard
addr.pincode = 999999
- party_address_details = frappe._dict(dict(
- legal_name=sanitize_for_json(addr.address_title),
- location=sanitize_for_json(addr.city),
- pincode=addr.pincode, gstin=addr.gstin,
- state_code=addr.gst_state_number,
- address_line1=sanitize_for_json(addr.address_line1),
- address_line2=sanitize_for_json(addr.address_line2)
- ))
+ party_address_details = frappe._dict(
+ dict(
+ legal_name=sanitize_for_json(addr.address_title),
+ location=sanitize_for_json(addr.city),
+ pincode=addr.pincode,
+ gstin=addr.gstin,
+ state_code=addr.gst_state_number,
+ address_line1=sanitize_for_json(addr.address_line1),
+ address_line2=sanitize_for_json(addr.address_line2),
+ )
+ )
return party_address_details
+
def get_overseas_address_details(address_name):
address_title, address_line1, address_line2, city = frappe.db.get_value(
- 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city']
+ "Address", address_name, ["address_title", "address_line1", "address_line2", "city"]
)
if not address_title or not address_line1 or not city:
frappe.throw(
- msg=_('Address lines and city is mandatory for address {}. Please set them and try again.').format(
- get_link_to_form('Address', address_name)
- ),
- title=_('Missing Address Fields')
+ msg=_(
+ "Address lines and city is mandatory for address {}. Please set them and try again."
+ ).format(get_link_to_form("Address", address_name)),
+ title=_("Missing Address Fields"),
)
- return frappe._dict(dict(
- gstin='URP',
- legal_name=sanitize_for_json(address_title),
- location=city,
- address_line1=sanitize_for_json(address_line1),
- address_line2=sanitize_for_json(address_line2),
- pincode=999999, state_code=96, place_of_supply=96
- ))
+ return frappe._dict(
+ dict(
+ gstin="URP",
+ legal_name=sanitize_for_json(address_title),
+ location=city,
+ address_line1=sanitize_for_json(address_line1),
+ address_line2=sanitize_for_json(address_line2),
+ pincode=999999,
+ state_code=96,
+ place_of_supply=96,
+ )
+ )
+
def get_item_list(invoice):
item_list = []
for d in invoice.items:
- einvoice_item_schema = read_json('einv_item_template')
+ einvoice_item_schema = read_json("einv_item_template")
item = frappe._dict({})
item.update(d.as_dict())
@@ -215,31 +271,41 @@ def get_item_list(invoice):
item.taxable_value = abs(item.taxable_value)
item.discount_amount = 0
- item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
- item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
- item.is_service_item = 'Y' if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else 'N'
+ item.is_service_item = "Y" if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else "N"
item.serial_no = ""
item = update_item_taxes(invoice, item)
item.total_value = abs(
- item.taxable_value + item.igst_amount + item.sgst_amount +
- item.cgst_amount + item.cess_amount + item.cess_nadv_amount + item.other_charges
+ item.taxable_value
+ + item.igst_amount
+ + item.sgst_amount
+ + item.cgst_amount
+ + item.cess_amount
+ + item.cess_nadv_amount
+ + item.other_charges
)
einv_item = einvoice_item_schema.format(item=item)
item_list.append(einv_item)
- return ', '.join(item_list)
+ return ", ".join(item_list)
+
def update_item_taxes(invoice, item):
gst_accounts = get_gst_accounts(invoice.company)
gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
for attr in [
- 'tax_rate', 'cess_rate', 'cess_nadv_amount',
- 'cgst_amount', 'sgst_amount', 'igst_amount',
- 'cess_amount', 'cess_nadv_amount', 'other_charges'
- ]:
+ "tax_rate",
+ "cess_rate",
+ "cess_nadv_amount",
+ "cgst_amount",
+ "sgst_amount",
+ "igst_amount",
+ "cess_amount",
+ "cess_nadv_amount",
+ "other_charges",
+ ]:
item[attr] = 0
for t in invoice.taxes:
@@ -254,35 +320,43 @@ def update_item_taxes(invoice, item):
if t.account_head in gst_accounts.cess_account:
item_tax_amount_after_discount = item_tax_detail[1]
- if t.charge_type == 'On Item Quantity':
+ if t.charge_type == "On Item Quantity":
item.cess_nadv_amount += abs(item_tax_amount_after_discount)
else:
item.cess_rate += item_tax_rate
item.cess_amount += abs(item_tax_amount_after_discount)
- for tax_type in ['igst', 'cgst', 'sgst']:
- if t.account_head in gst_accounts[f'{tax_type}_account']:
+ for tax_type in ["igst", "cgst", "sgst", "utgst"]:
+ if t.account_head in gst_accounts[f"{tax_type}_account"]:
item.tax_rate += item_tax_rate
- item[f'{tax_type}_amount'] += abs(item_tax_amount)
+ if tax_type == "utgst":
+ # utgst taxes are reported same as sgst tax
+ item["sgst_amount"] += abs(item_tax_amount)
+ else:
+ item[f"{tax_type}_amount"] += abs(item_tax_amount)
else:
# TODO: other charges per item
pass
return item
+
def get_invoice_value_details(invoice):
invoice_value_details = frappe._dict(dict())
- invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
+ invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get("items")]))
invoice_value_details.invoice_discount_amt = 0
invoice_value_details.round_off = invoice.base_rounding_adjustment
- invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total)
+ invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(
+ invoice.base_grand_total
+ )
invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total)
invoice_value_details = update_invoice_taxes(invoice, invoice_value_details)
return invoice_value_details
+
def update_invoice_taxes(invoice, invoice_value_details):
gst_accounts = get_gst_accounts(invoice.company)
gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
@@ -301,90 +375,121 @@ def update_invoice_taxes(invoice, invoice_value_details):
# using after discount amt since item also uses after discount amt for cess calc
invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount)
- for tax_type in ['igst', 'cgst', 'sgst']:
- if t.account_head in gst_accounts[f'{tax_type}_account']:
+ for tax_type in ["igst", "cgst", "sgst", "utgst"]:
+ if t.account_head in gst_accounts[f"{tax_type}_account"]:
+ if tax_type == "utgst":
+ invoice_value_details["total_sgst_amt"] += abs(tax_amount)
+ else:
+ invoice_value_details[f"total_{tax_type}_amt"] += abs(tax_amount)
- invoice_value_details[f'total_{tax_type}_amt'] += abs(tax_amount)
update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows)
+
else:
invoice_value_details.total_other_charges += abs(tax_amount)
return invoice_value_details
-def update_other_charges(tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows):
+
+def update_other_charges(
+ tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows
+):
prev_row_id = cint(tax_row.row_id) - 1
if tax_row.account_head in gst_accounts_list and prev_row_id not in considered_rows:
- if tax_row.charge_type == 'On Previous Row Amount':
- amount = invoice.get('taxes')[prev_row_id].tax_amount_after_discount_amount
+ if tax_row.charge_type == "On Previous Row Amount":
+ amount = invoice.get("taxes")[prev_row_id].tax_amount_after_discount_amount
invoice_value_details.total_other_charges -= abs(amount)
considered_rows.append(prev_row_id)
- if tax_row.charge_type == 'On Previous Row Total':
- amount = invoice.get('taxes')[prev_row_id].base_total - invoice.base_net_total
+ if tax_row.charge_type == "On Previous Row Total":
+ amount = invoice.get("taxes")[prev_row_id].base_total - invoice.base_net_total
invoice_value_details.total_other_charges -= abs(amount)
considered_rows.append(prev_row_id)
+
def get_payment_details(invoice):
payee_name = invoice.company
- mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments])
+ mode_of_payment = ""
paid_amount = invoice.base_paid_amount
outstanding_amount = invoice.outstanding_amount
- return frappe._dict(dict(
- payee_name=payee_name, mode_of_payment=mode_of_payment,
- paid_amount=paid_amount, outstanding_amount=outstanding_amount
- ))
+ return frappe._dict(
+ dict(
+ payee_name=payee_name,
+ mode_of_payment=mode_of_payment,
+ paid_amount=paid_amount,
+ outstanding_amount=outstanding_amount,
+ )
+ )
+
def get_return_doc_reference(invoice):
- invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date')
- return frappe._dict(dict(
- invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy')
- ))
+ invoice_date = frappe.db.get_value("Sales Invoice", invoice.return_against, "posting_date")
+ return frappe._dict(
+ dict(invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, "dd/mm/yyyy"))
+ )
+
def get_eway_bill_details(invoice):
if invoice.is_return:
- frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'),
- title=_('Invalid Fields'))
+ frappe.throw(
+ _(
+ "E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice."
+ ),
+ title=_("Invalid Fields"),
+ )
+ mode_of_transport = {"": "", "Road": "1", "Air": "2", "Rail": "3", "Ship": "4"}
+ vehicle_type = {"Regular": "R", "Over Dimensional Cargo (ODC)": "O"}
- mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
- vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
+ return frappe._dict(
+ dict(
+ gstin=invoice.gst_transporter_id,
+ name=invoice.transporter_name,
+ mode_of_transport=mode_of_transport[invoice.mode_of_transport or ""] or None,
+ distance=invoice.distance or 0,
+ document_name=invoice.lr_no,
+ document_date=format_date(invoice.lr_date, "dd/mm/yyyy"),
+ vehicle_no=invoice.vehicle_no,
+ vehicle_type=vehicle_type[invoice.gst_vehicle_type],
+ )
+ )
- return frappe._dict(dict(
- gstin=invoice.gst_transporter_id,
- name=invoice.transporter_name,
- mode_of_transport=mode_of_transport[invoice.mode_of_transport],
- distance=invoice.distance or 0,
- document_name=invoice.lr_no,
- document_date=format_date(invoice.lr_date, 'dd/mm/yyyy'),
- vehicle_no=invoice.vehicle_no,
- vehicle_type=vehicle_type[invoice.gst_vehicle_type]
- ))
def validate_mandatory_fields(invoice):
if not invoice.company_address:
frappe.throw(
- _('Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again.'),
- title=_('Missing Fields')
+ _(
+ "Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again."
+ ),
+ title=_("Missing Fields"),
)
if not invoice.customer_address:
frappe.throw(
- _('Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again.'),
- title=_('Missing Fields')
+ _(
+ "Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again."
+ ),
+ title=_("Missing Fields"),
)
- if not frappe.db.get_value('Address', invoice.company_address, 'gstin'):
+ if not frappe.db.get_value("Address", invoice.company_address, "gstin"):
frappe.throw(
- _('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
- title=_('Missing Fields')
+ _(
+ "GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address."
+ ),
+ title=_("Missing Fields"),
)
- if invoice.gst_category != 'Overseas' and not frappe.db.get_value('Address', invoice.customer_address, 'gstin'):
+ if invoice.gst_category != "Overseas" and not frappe.db.get_value(
+ "Address", invoice.customer_address, "gstin"
+ ):
frappe.throw(
- _('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'),
- title=_('Missing Fields')
+ _(
+ "GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address."
+ ),
+ title=_("Missing Fields"),
)
+
def validate_totals(einvoice):
- item_list = einvoice['ItemList']
- value_details = einvoice['ValDtls']
+ item_list = einvoice["ItemList"]
+ value_details = einvoice["ValDtls"]
total_item_ass_value = 0
total_item_cgst_value = 0
@@ -392,39 +497,89 @@ def validate_totals(einvoice):
total_item_igst_value = 0
total_item_value = 0
for item in item_list:
- total_item_ass_value += flt(item['AssAmt'])
- total_item_cgst_value += flt(item['CgstAmt'])
- total_item_sgst_value += flt(item['SgstAmt'])
- total_item_igst_value += flt(item['IgstAmt'])
- total_item_value += flt(item['TotItemVal'])
+ total_item_ass_value += flt(item["AssAmt"])
+ total_item_cgst_value += flt(item["CgstAmt"])
+ total_item_sgst_value += flt(item["SgstAmt"])
+ total_item_igst_value += flt(item["IgstAmt"])
+ total_item_value += flt(item["TotItemVal"])
- if abs(flt(item['AssAmt']) * flt(item['GstRt']) / 100) - (flt(item['CgstAmt']) + flt(item['SgstAmt']) + flt(item['IgstAmt'])) > 1:
- frappe.throw(_('Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table.').format(item.idx))
+ if (
+ abs(flt(item["AssAmt"]) * flt(item["GstRt"]) / 100)
+ - (flt(item["CgstAmt"]) + flt(item["SgstAmt"]) + flt(item["IgstAmt"]))
+ > 1
+ ):
+ frappe.throw(
+ _(
+ "Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table."
+ ).format(item.idx)
+ )
- if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1:
- frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.'))
+ if abs(flt(value_details["AssVal"]) - total_item_ass_value) > 1:
+ frappe.throw(
+ _(
+ "Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction."
+ )
+ )
- if abs(flt(value_details['CgstVal']) + flt(value_details['SgstVal']) - total_item_cgst_value - total_item_sgst_value) > 1:
- frappe.throw(_('CGST + SGST value of the items is not equal to total CGST + SGST value. Please review taxes for any correction.'))
+ if (
+ abs(
+ flt(value_details["CgstVal"])
+ + flt(value_details["SgstVal"])
+ - total_item_cgst_value
+ - total_item_sgst_value
+ )
+ > 1
+ ):
+ frappe.throw(
+ _(
+ "CGST + SGST value of the items is not equal to total CGST + SGST value. Please review taxes for any correction."
+ )
+ )
- if abs(flt(value_details['IgstVal']) - total_item_igst_value) > 1:
- frappe.throw(_('IGST value of all items is not equal to total IGST value. Please review taxes for any correction.'))
+ if abs(flt(value_details["IgstVal"]) - total_item_igst_value) > 1:
+ frappe.throw(
+ _(
+ "IGST value of all items is not equal to total IGST value. Please review taxes for any correction."
+ )
+ )
- if abs(flt(value_details['TotInvVal']) + flt(value_details['Discount']) - flt(value_details['OthChrg']) - total_item_value) > 1:
- frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.'))
+ if (
+ abs(
+ flt(value_details["TotInvVal"])
+ + flt(value_details["Discount"])
+ - flt(value_details["OthChrg"])
+ - total_item_value
+ )
+ > 1
+ ):
+ frappe.throw(
+ _(
+ "Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction."
+ )
+ )
- calculated_invoice_value = \
- flt(value_details['AssVal']) + flt(value_details['CgstVal']) \
- + flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \
- + flt(value_details['OthChrg']) - flt(value_details['Discount'])
+ calculated_invoice_value = (
+ flt(value_details["AssVal"])
+ + flt(value_details["CgstVal"])
+ + flt(value_details["SgstVal"])
+ + flt(value_details["IgstVal"])
+ + flt(value_details["CesVal"])
+ + flt(value_details["OthChrg"])
+ - flt(value_details["Discount"])
+ )
+
+ if abs(flt(value_details["TotInvVal"]) - calculated_invoice_value) > 1:
+ frappe.throw(
+ _(
+ "Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction."
+ )
+ )
- if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1:
- frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.'))
def make_einvoice(invoice):
validate_mandatory_fields(invoice)
- schema = read_json('einv_template')
+ schema = read_json("einv_template")
transaction_details = get_transaction_details(invoice)
item_list = get_item_list(invoice)
@@ -432,13 +587,13 @@ def make_einvoice(invoice):
invoice_value_details = get_invoice_value_details(invoice)
seller_details = get_party_details(invoice.company_address)
- if invoice.gst_category == 'Overseas':
+ if invoice.gst_category == "Overseas":
buyer_details = get_overseas_address_details(invoice.customer_address)
else:
buyer_details = get_party_details(invoice.customer_address)
place_of_supply = get_place_of_supply(invoice, invoice.doctype)
if place_of_supply:
- place_of_supply = place_of_supply.split('-')[0]
+ place_of_supply = place_of_supply.split("-")[0]
else:
place_of_supply = sanitize_for_json(invoice.billing_address_gstin)[:2]
buyer_details.update(dict(place_of_supply=place_of_supply))
@@ -448,7 +603,7 @@ def make_einvoice(invoice):
shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({})
if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
- if invoice.gst_category == 'Overseas':
+ if invoice.gst_category == "Overseas":
shipping_details = get_overseas_address_details(invoice.shipping_address_name)
else:
shipping_details = get_party_details(invoice.shipping_address_name, skip_gstin_validation=True)
@@ -470,11 +625,19 @@ def make_einvoice(invoice):
period_details = export_details = frappe._dict({})
einvoice = schema.format(
- transaction_details=transaction_details, doc_details=doc_details, dispatch_details=dispatch_details,
- seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details,
- item_list=item_list, invoice_value_details=invoice_value_details, payment_details=payment_details,
- period_details=period_details, prev_doc_details=prev_doc_details,
- export_details=export_details, eway_bill_details=eway_bill_details
+ transaction_details=transaction_details,
+ doc_details=doc_details,
+ dispatch_details=dispatch_details,
+ seller_details=seller_details,
+ buyer_details=buyer_details,
+ shipping_details=shipping_details,
+ item_list=item_list,
+ invoice_value_details=invoice_value_details,
+ payment_details=payment_details,
+ period_details=period_details,
+ prev_doc_details=prev_doc_details,
+ export_details=export_details,
+ eway_bill_details=eway_bill_details,
)
try:
@@ -491,15 +654,18 @@ def make_einvoice(invoice):
return einvoice
+
def show_link_to_error_log(invoice, einvoice):
err_log = log_error(einvoice)
- link_to_error_log = get_link_to_form('Error Log', err_log.name, 'Error Log')
+ link_to_error_log = get_link_to_form("Error Log", err_log.name, "Error Log")
frappe.throw(
- _('An error occurred while creating e-invoice for {}. Please check {} for more information.').format(
- invoice.name, link_to_error_log),
- title=_('E Invoice Creation Failed')
+ _(
+ "An error occurred while creating e-invoice for {}. Please check {} for more information."
+ ).format(invoice.name, link_to_error_log),
+ title=_("E Invoice Creation Failed"),
)
+
def log_error(data=None):
if isinstance(data, six.string_types):
data = json.loads(data)
@@ -509,16 +675,47 @@ def log_error(data=None):
err_msg = str(sys.exc_info()[1])
data = json.dumps(data, indent=4)
- message = "\n".join([
- "Error", err_msg, seperator,
- "Data:", data, seperator,
- "Exception:", err_tb
- ])
- return frappe.log_error(title=_('E Invoice Request Failed'), message=message)
+ message = "\n".join(["Error", err_msg, seperator, "Data:", data, seperator, "Exception:", err_tb])
+ return frappe.log_error(title=_("E Invoice Request Failed"), message=message)
+
def santize_einvoice_fields(einvoice):
- int_fields = ["Pin","Distance","CrDay"]
- float_fields = ["Qty","FreeQty","UnitPrice","TotAmt","Discount","PreTaxVal","AssAmt","GstRt","IgstAmt","CgstAmt","SgstAmt","CesRt","CesAmt","CesNonAdvlAmt","StateCesRt","StateCesAmt","StateCesNonAdvlAmt","OthChrg","TotItemVal","AssVal","CgstVal","SgstVal","IgstVal","CesVal","StCesVal","Discount","OthChrg","RndOffAmt","TotInvVal","TotInvValFc","PaidAmt","PaymtDue","ExpDuty",]
+ int_fields = ["Pin", "Distance", "CrDay"]
+ float_fields = [
+ "Qty",
+ "FreeQty",
+ "UnitPrice",
+ "TotAmt",
+ "Discount",
+ "PreTaxVal",
+ "AssAmt",
+ "GstRt",
+ "IgstAmt",
+ "CgstAmt",
+ "SgstAmt",
+ "CesRt",
+ "CesAmt",
+ "CesNonAdvlAmt",
+ "StateCesRt",
+ "StateCesAmt",
+ "StateCesNonAdvlAmt",
+ "OthChrg",
+ "TotItemVal",
+ "AssVal",
+ "CgstVal",
+ "SgstVal",
+ "IgstVal",
+ "CesVal",
+ "StCesVal",
+ "Discount",
+ "OthChrg",
+ "RndOffAmt",
+ "TotInvVal",
+ "TotInvValFc",
+ "PaidAmt",
+ "PaymtDue",
+ "ExpDuty",
+ ]
copy = einvoice.copy()
for key, value in copy.items():
if isinstance(value, list):
@@ -550,22 +747,31 @@ def santize_einvoice_fields(einvoice):
return einvoice
+
def safe_json_load(json_string):
try:
return json.loads(json_string)
except json.JSONDecodeError as e:
# print a snippet of 40 characters around the location where error occured
pos = e.pos
- start, end = max(0, pos-20), min(len(json_string)-1, pos+20)
+ start, end = max(0, pos - 20), min(len(json_string) - 1, pos + 20)
snippet = json_string[start:end]
- frappe.throw(_("Error in input data. Please check for any special characters near following input: {}").format(snippet))
+ frappe.throw(
+ _(
+ "Error in input data. Please check for any special characters near following input: {}"
+ ).format(snippet)
+ )
+
class RequestFailed(Exception):
pass
+
+
class CancellationNotAllowed(Exception):
pass
-class GSPConnector():
+
+class GSPConnector:
def __init__(self, doctype=None, docname=None):
self.doctype = doctype
self.docname = docname
@@ -574,15 +780,19 @@ class GSPConnector():
self.set_credentials()
# authenticate url is same for sandbox & live
- self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token'
- self.base_url = 'https://gsp.adaequare.com' if not self.e_invoice_settings.sandbox_mode else 'https://gsp.adaequare.com/test'
+ self.authenticate_url = "https://gsp.adaequare.com/gsp/authenticate?grant_type=token"
+ self.base_url = (
+ "https://gsp.adaequare.com"
+ if not self.e_invoice_settings.sandbox_mode
+ else "https://gsp.adaequare.com/test"
+ )
- self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel'
- self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
- self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice'
- self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin'
- self.cancel_ewaybill_url = self.base_url + '/enriched/ewb/ewayapi?action=CANEWB'
- self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill'
+ self.cancel_irn_url = self.base_url + "/enriched/ei/api/invoice/cancel"
+ self.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn"
+ self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice"
+ self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
+ self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB"
+ self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
def set_invoice(self):
self.invoice = None
@@ -590,10 +800,14 @@ class GSPConnector():
self.invoice = frappe.get_cached_doc(self.doctype, self.docname)
def set_credentials(self):
- self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings')
+ self.e_invoice_settings = frappe.get_cached_doc("E Invoice Settings")
if not self.e_invoice_settings.enable:
- frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings")))
+ frappe.throw(
+ _("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(
+ get_link_to_form("E Invoice Settings", "E Invoice Settings")
+ )
+ )
if self.invoice:
gstin = self.get_seller_gstin()
@@ -601,14 +815,22 @@ class GSPConnector():
if credentials_for_gstin:
self.credentials = credentials_for_gstin[0]
else:
- frappe.throw(_('Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings'))
+ frappe.throw(
+ _(
+ "Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings"
+ )
+ )
else:
- self.credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
+ self.credentials = (
+ self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
+ )
def get_seller_gstin(self):
- gstin = frappe.db.get_value('Address', self.invoice.company_address, 'gstin')
+ gstin = frappe.db.get_value("Address", self.invoice.company_address, "gstin")
if not gstin:
- frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.'))
+ frappe.throw(
+ _("Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.")
+ )
return gstin
def get_auth_token(self):
@@ -618,45 +840,57 @@ class GSPConnector():
return self.e_invoice_settings.auth_token
def make_request(self, request_type, url, headers=None, data=None):
- if request_type == 'post':
- res = make_post_request(url, headers=headers, data=data)
- else:
- res = make_get_request(url, headers=headers, data=data)
+ try:
+ if request_type == "post":
+ res = make_post_request(url, headers=headers, data=data)
+ else:
+ res = make_get_request(url, headers=headers, data=data)
+
+ except requests.exceptions.HTTPError as e:
+ if e.response.status_code in [401, 403] and not hasattr(self, "token_auto_refreshed"):
+ self.auto_refresh_token()
+ headers = self.get_headers()
+ return self.make_request(request_type, url, headers, data)
self.log_request(url, headers, data, res)
return res
+ def auto_refresh_token(self):
+ self.fetch_auth_token()
+ self.token_auto_refreshed = True
+
def log_request(self, url, headers, data, res):
- headers.update({ 'password': self.credentials.password })
- request_log = frappe.get_doc({
- "doctype": "E Invoice Request Log",
- "user": frappe.session.user,
- "reference_invoice": self.invoice.name if self.invoice else None,
- "url": url,
- "headers": json.dumps(headers, indent=4) if headers else None,
- "data": json.dumps(data, indent=4) if isinstance(data, dict) else data,
- "response": json.dumps(res, indent=4) if res else None
- })
+ headers.update({"password": self.credentials.password})
+ request_log = frappe.get_doc(
+ {
+ "doctype": "E Invoice Request Log",
+ "user": frappe.session.user,
+ "reference_invoice": self.invoice.name if self.invoice else None,
+ "url": url,
+ "headers": json.dumps(headers, indent=4) if headers else None,
+ "data": json.dumps(data, indent=4) if isinstance(data, dict) else data,
+ "response": json.dumps(res, indent=4) if res else None,
+ }
+ )
request_log.save(ignore_permissions=True)
frappe.db.commit()
def get_client_credentials(self):
if self.e_invoice_settings.client_id and self.e_invoice_settings.client_secret:
- return self.e_invoice_settings.client_id, self.e_invoice_settings.get_password('client_secret')
+ return self.e_invoice_settings.client_id, self.e_invoice_settings.get_password("client_secret")
return frappe.conf.einvoice_client_id, frappe.conf.einvoice_client_secret
def fetch_auth_token(self):
client_id, client_secret = self.get_client_credentials()
- headers = {
- 'gspappid': client_id,
- 'gspappsecret': client_secret
- }
+ headers = {"gspappid": client_id, "gspappsecret": client_secret}
res = {}
try:
- res = self.make_request('post', self.authenticate_url, headers)
- self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token'))
- self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in'))
+ res = self.make_request("post", self.authenticate_url, headers)
+ self.e_invoice_settings.auth_token = "{} {}".format(
+ res.get("token_type"), res.get("access_token")
+ )
+ self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get("expires_in"))
self.e_invoice_settings.save(ignore_permissions=True)
self.e_invoice_settings.reload()
@@ -666,22 +900,22 @@ class GSPConnector():
def get_headers(self):
return {
- 'content-type': 'application/json',
- 'user_name': self.credentials.username,
- 'password': self.credentials.get_password(),
- 'gstin': self.credentials.gstin,
- 'authorization': self.get_auth_token(),
- 'requestid': str(base64.b64encode(os.urandom(18))),
+ "content-type": "application/json",
+ "user_name": self.credentials.username,
+ "password": self.credentials.get_password(),
+ "gstin": self.credentials.gstin,
+ "authorization": self.get_auth_token(),
+ "requestid": str(base64.b64encode(os.urandom(18))),
}
def fetch_gstin_details(self, gstin):
headers = self.get_headers()
try:
- params = '?gstin={gstin}'.format(gstin=gstin)
- res = self.make_request('get', self.gstin_details_url + params, headers)
- if res.get('success'):
- return res.get('result')
+ params = "?gstin={gstin}".format(gstin=gstin)
+ res = self.make_request("get", self.gstin_details_url + params, headers)
+ if res.get("success"):
+ return res.get("result")
else:
log_error(res)
raise RequestFailed
@@ -692,10 +926,11 @@ class GSPConnector():
except Exception:
log_error()
self.raise_error(True)
+
@staticmethod
def get_gstin_details(gstin):
- '''fetch and cache GSTIN details'''
- if not hasattr(frappe.local, 'gstin_cache'):
+ """fetch and cache GSTIN details"""
+ if not hasattr(frappe.local, "gstin_cache"):
frappe.local.gstin_cache = {}
key = gstin
@@ -703,7 +938,7 @@ class GSPConnector():
details = gsp_connector.fetch_gstin_details(gstin)
frappe.local.gstin_cache[key] = details
- frappe.cache().hset('gstin_cache', key, details)
+ frappe.cache().hset("gstin_cache", key, details)
return details
def generate_irn(self):
@@ -712,27 +947,29 @@ class GSPConnector():
headers = self.get_headers()
einvoice = make_einvoice(self.invoice)
data = json.dumps(einvoice, indent=4)
- res = self.make_request('post', self.generate_irn_url, headers, data)
+ res = self.make_request("post", self.generate_irn_url, headers, data)
- if res.get('success'):
- self.set_einvoice_data(res.get('result'))
+ if res.get("success"):
+ self.set_einvoice_data(res.get("result"))
- elif '2150' in res.get('message'):
+ elif "2150" in res.get("message"):
# IRN already generated but not updated in invoice
# Extract the IRN from the response description and fetch irn details
- irn = res.get('result')[0].get('Desc').get('Irn')
+ irn = res.get("result")[0].get("Desc").get("Irn")
irn_details = self.get_irn_details(irn)
if irn_details:
self.set_einvoice_data(irn_details)
else:
- raise RequestFailed('IRN has already been generated for the invoice but cannot fetch details for the it. \
- Contact ERPNext support to resolve the issue.')
+ raise RequestFailed(
+ "IRN has already been generated for the invoice but cannot fetch details for the it. \
+ Contact ERPNext support to resolve the issue."
+ )
else:
raise RequestFailed
except RequestFailed:
- errors = self.sanitize_error_message(res.get('message'))
+ errors = self.sanitize_error_message(res.get("message"))
self.set_failed_status(errors=errors)
self.raise_error(errors=errors)
@@ -744,7 +981,7 @@ class GSPConnector():
@staticmethod
def bulk_generate_irn(invoices):
gsp_connector = GSPConnector()
- gsp_connector.doctype = 'Sales Invoice'
+ gsp_connector.doctype = "Sales Invoice"
failed = []
@@ -756,10 +993,7 @@ class GSPConnector():
gsp_connector.generate_irn()
except Exception as e:
- failed.append({
- 'docname': invoice,
- 'message': str(e)
- })
+ failed.append({"docname": invoice, "message": str(e)})
return failed
@@ -767,15 +1001,15 @@ class GSPConnector():
headers = self.get_headers()
try:
- params = '?irn={irn}'.format(irn=irn)
- res = self.make_request('get', self.irn_details_url + params, headers)
- if res.get('success'):
- return res.get('result')
+ params = "?irn={irn}".format(irn=irn)
+ res = self.make_request("get", self.irn_details_url + params, headers)
+ if res.get("success"):
+ return res.get("result")
else:
raise RequestFailed
except RequestFailed:
- errors = self.sanitize_error_message(res.get('message'))
+ errors = self.sanitize_error_message(res.get("message"))
self.raise_error(errors=errors)
except Exception:
@@ -787,26 +1021,30 @@ class GSPConnector():
try:
# validate cancellation
if time_diff_in_hours(now_datetime(), self.invoice.ack_date) > 24:
- frappe.throw(_('E-Invoice cannot be cancelled after 24 hours of IRN generation.'), title=_('Not Allowed'), exc=CancellationNotAllowed)
+ frappe.throw(
+ _("E-Invoice cannot be cancelled after 24 hours of IRN generation."),
+ title=_("Not Allowed"),
+ exc=CancellationNotAllowed,
+ )
if not irn:
- frappe.throw(_('IRN not found. You must generate IRN before cancelling.'), title=_('Not Allowed'), exc=CancellationNotAllowed)
+ frappe.throw(
+ _("IRN not found. You must generate IRN before cancelling."),
+ title=_("Not Allowed"),
+ exc=CancellationNotAllowed,
+ )
headers = self.get_headers()
- data = json.dumps({
- 'Irn': irn,
- 'Cnlrsn': reason,
- 'Cnlrem': remark
- }, indent=4)
+ data = json.dumps({"Irn": irn, "Cnlrsn": reason, "Cnlrem": remark}, indent=4)
- res = self.make_request('post', self.cancel_irn_url, headers, data)
- if res.get('success') or '9999' in res.get('message'):
+ res = self.make_request("post", self.cancel_irn_url, headers, data)
+ if res.get("success") or "9999" in res.get("message"):
self.invoice.irn_cancelled = 1
- self.invoice.irn_cancel_date = res.get('result')['CancelDate'] if res.get('result') else ""
- self.invoice.einvoice_status = 'Cancelled'
+ self.invoice.irn_cancel_date = res.get("result")["CancelDate"] if res.get("result") else ""
+ self.invoice.einvoice_status = "Cancelled"
self.invoice.flags.updater_reference = {
- 'doctype': self.invoice.doctype,
- 'docname': self.invoice.name,
- 'label': _('IRN Cancelled - {}').format(remark)
+ "doctype": self.invoice.doctype,
+ "docname": self.invoice.name,
+ "label": _("IRN Cancelled - {}").format(remark),
}
self.update_invoice()
@@ -814,7 +1052,7 @@ class GSPConnector():
raise RequestFailed
except RequestFailed:
- errors = self.sanitize_error_message(res.get('message'))
+ errors = self.sanitize_error_message(res.get("message"))
self.set_failed_status(errors=errors)
self.raise_error(errors=errors)
@@ -830,7 +1068,7 @@ class GSPConnector():
@staticmethod
def bulk_cancel_irn(invoices, reason, remark):
gsp_connector = GSPConnector()
- gsp_connector.doctype = 'Sales Invoice'
+ gsp_connector.doctype = "Sales Invoice"
failed = []
@@ -843,10 +1081,7 @@ class GSPConnector():
gsp_connector.cancel_irn(irn, reason, remark)
except Exception as e:
- failed.append({
- 'docname': invoice,
- 'message': str(e)
- })
+ failed.append({"docname": invoice, "message": str(e)})
return failed
@@ -855,29 +1090,32 @@ class GSPConnector():
headers = self.get_headers()
eway_bill_details = get_eway_bill_details(args)
- data = json.dumps({
- 'Irn': args.irn,
- 'Distance': cint(eway_bill_details.distance),
- 'TransMode': eway_bill_details.mode_of_transport,
- 'TransId': eway_bill_details.gstin,
- 'TransName': eway_bill_details.transporter,
- 'TrnDocDt': eway_bill_details.document_date,
- 'TrnDocNo': eway_bill_details.document_name,
- 'VehNo': eway_bill_details.vehicle_no,
- 'VehType': eway_bill_details.vehicle_type
- }, indent=4)
+ data = json.dumps(
+ {
+ "Irn": args.irn,
+ "Distance": cint(eway_bill_details.distance),
+ "TransMode": eway_bill_details.mode_of_transport,
+ "TransId": eway_bill_details.gstin,
+ "TransName": eway_bill_details.name,
+ "TrnDocDt": eway_bill_details.document_date,
+ "TrnDocNo": eway_bill_details.document_name,
+ "VehNo": eway_bill_details.vehicle_no,
+ "VehType": eway_bill_details.vehicle_type,
+ },
+ indent=4,
+ )
try:
- res = self.make_request('post', self.generate_ewaybill_url, headers, data)
- if res.get('success'):
- self.invoice.ewaybill = res.get('result').get('EwbNo')
- self.invoice.eway_bill_validity = res.get('result').get('EwbValidTill')
+ res = self.make_request("post", self.generate_ewaybill_url, headers, data)
+ if res.get("success"):
+ self.invoice.ewaybill = res.get("result").get("EwbNo")
+ self.invoice.eway_bill_validity = res.get("result").get("EwbValidTill")
self.invoice.eway_bill_cancelled = 0
self.invoice.update(args)
self.invoice.flags.updater_reference = {
- 'doctype': self.invoice.doctype,
- 'docname': self.invoice.name,
- 'label': _('E-Way Bill Generated')
+ "doctype": self.invoice.doctype,
+ "docname": self.invoice.name,
+ "label": _("E-Way Bill Generated"),
}
self.update_invoice()
@@ -885,7 +1123,7 @@ class GSPConnector():
raise RequestFailed
except RequestFailed:
- errors = self.sanitize_error_message(res.get('message'))
+ errors = self.sanitize_error_message(res.get("message"))
self.raise_error(errors=errors)
except Exception:
@@ -894,22 +1132,18 @@ class GSPConnector():
def cancel_eway_bill(self, eway_bill, reason, remark):
headers = self.get_headers()
- data = json.dumps({
- 'ewbNo': eway_bill,
- 'cancelRsnCode': reason,
- 'cancelRmrk': remark
- }, indent=4)
+ data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
headers["username"] = headers["user_name"]
del headers["user_name"]
try:
- res = self.make_request('post', self.cancel_ewaybill_url, headers, data)
- if res.get('success'):
- self.invoice.ewaybill = ''
+ res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
+ if res.get("success"):
+ self.invoice.ewaybill = ""
self.invoice.eway_bill_cancelled = 1
self.invoice.flags.updater_reference = {
- 'doctype': self.invoice.doctype,
- 'docname': self.invoice.name,
- 'label': _('E-Way Bill Cancelled - {}').format(remark)
+ "doctype": self.invoice.doctype,
+ "docname": self.invoice.name,
+ "label": _("E-Way Bill Cancelled - {}").format(remark),
}
self.update_invoice()
@@ -917,7 +1151,7 @@ class GSPConnector():
raise RequestFailed
except RequestFailed:
- errors = self.sanitize_error_message(res.get('message'))
+ errors = self.sanitize_error_message(res.get("message"))
self.raise_error(errors=errors)
except Exception:
@@ -925,24 +1159,24 @@ class GSPConnector():
self.raise_error(True)
def sanitize_error_message(self, message):
- '''
- On validation errors, response message looks something like this:
- message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable,
- 3095 : Supplier GSTIN is inactive'
- we search for string between ':' to extract the error messages
- errors = [
- ': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ',
- ': Test'
- ]
- then we trim down the message by looping over errors
- '''
+ """
+ On validation errors, response message looks something like this:
+ message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable,
+ 3095 : Supplier GSTIN is inactive'
+ we search for string between ':' to extract the error messages
+ errors = [
+ ': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ',
+ ': Test'
+ ]
+ then we trim down the message by looping over errors
+ """
if not message:
return []
- errors = re.findall(': [^:]+', message)
+ errors = re.findall(": [^:]+", message)
for idx, e in enumerate(errors):
# remove colons
- errors[idx] = errors[idx].replace(':', '').strip()
+ errors[idx] = errors[idx].replace(":", "").strip()
# if not last
if idx != len(errors) - 1:
# remove last 7 chars eg: ', 3095 '
@@ -951,39 +1185,41 @@ class GSPConnector():
return errors
def raise_error(self, raise_exception=False, errors=None):
- title = _('E Invoice Request Failed')
+ title = _("E Invoice Request Failed")
if errors:
frappe.throw(errors, title=title, as_list=1)
else:
link_to_error_list = 'Error Log'
frappe.msgprint(
- _('An error occurred while making e-invoicing request. Please check {} for more information.').format(link_to_error_list),
+ _(
+ "An error occurred while making e-invoicing request. Please check {} for more information."
+ ).format(link_to_error_list),
title=title,
raise_exception=raise_exception,
- indicator='red'
+ indicator="red",
)
def set_einvoice_data(self, res):
- enc_signed_invoice = res.get('SignedInvoice')
- dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)['data']
+ enc_signed_invoice = res.get("SignedInvoice")
+ dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)["data"]
- self.invoice.irn = res.get('Irn')
- self.invoice.ewaybill = res.get('EwbNo')
- self.invoice.eway_bill_validity = res.get('EwbValidTill')
- self.invoice.ack_no = res.get('AckNo')
- self.invoice.ack_date = res.get('AckDt')
+ self.invoice.irn = res.get("Irn")
+ self.invoice.ewaybill = res.get("EwbNo")
+ self.invoice.eway_bill_validity = res.get("EwbValidTill")
+ self.invoice.ack_no = res.get("AckNo")
+ self.invoice.ack_date = res.get("AckDt")
self.invoice.signed_einvoice = dec_signed_invoice
- self.invoice.ack_no = res.get('AckNo')
- self.invoice.ack_date = res.get('AckDt')
- self.invoice.signed_qr_code = res.get('SignedQRCode')
- self.invoice.einvoice_status = 'Generated'
+ self.invoice.ack_no = res.get("AckNo")
+ self.invoice.ack_date = res.get("AckDt")
+ self.invoice.signed_qr_code = res.get("SignedQRCode")
+ self.invoice.einvoice_status = "Generated"
self.attach_qrcode_image()
self.invoice.flags.updater_reference = {
- 'doctype': self.invoice.doctype,
- 'docname': self.invoice.name,
- 'label': _('IRN Generated')
+ "doctype": self.invoice.doctype,
+ "docname": self.invoice.name,
+ "label": _("IRN Generated"),
}
self.update_invoice()
@@ -991,19 +1227,22 @@ class GSPConnector():
qrcode = self.invoice.signed_qr_code
doctype = self.invoice.doctype
docname = self.invoice.name
- filename = 'QRCode_{}.png'.format(docname).replace(os.path.sep, "__")
+ filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__")
qr_image = io.BytesIO()
- url = qrcreate(qrcode, error='L')
+ url = qrcreate(qrcode, error="L")
url.png(qr_image, scale=2, quiet_zone=1)
- _file = frappe.get_doc({
- "doctype": "File",
- "file_name": filename,
- "attached_to_doctype": doctype,
- "attached_to_name": docname,
- "attached_to_field": "qrcode_image",
- "is_private": 0,
- "content": qr_image.getvalue()})
+ _file = frappe.get_doc(
+ {
+ "doctype": "File",
+ "file_name": filename,
+ "attached_to_doctype": doctype,
+ "attached_to_name": docname,
+ "attached_to_field": "qrcode_image",
+ "is_private": 0,
+ "content": qr_image.getvalue(),
+ }
+ )
_file.save()
frappe.db.commit()
self.invoice.qrcode_image = _file.file_url
@@ -1015,50 +1254,57 @@ class GSPConnector():
def set_failed_status(self, errors=None):
frappe.db.rollback()
- self.invoice.einvoice_status = 'Failed'
+ self.invoice.einvoice_status = "Failed"
self.invoice.failure_description = self.get_failure_message(errors) if errors else ""
self.update_invoice()
frappe.db.commit()
def get_failure_message(self, errors):
if isinstance(errors, list):
- errors = ', '.join(errors)
+ errors = ", ".join(errors)
return errors
+
def sanitize_for_json(string):
"""Escape JSON specific characters from a string."""
# json.dumps adds double-quotes to the string. Indexing to remove them.
return json.dumps(string)[1:-1]
+
@frappe.whitelist()
def get_einvoice(doctype, docname):
invoice = frappe.get_doc(doctype, docname)
return make_einvoice(invoice)
+
@frappe.whitelist()
def generate_irn(doctype, docname):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.generate_irn()
+
@frappe.whitelist()
def cancel_irn(doctype, docname, irn, reason, remark):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.cancel_irn(irn, reason, remark)
+
@frappe.whitelist()
def generate_eway_bill(doctype, docname, **kwargs):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.generate_eway_bill(**kwargs)
+
@frappe.whitelist()
def cancel_eway_bill(doctype, docname):
# TODO: uncomment when eway_bill api from Adequare is enabled
# gsp_connector = GSPConnector(doctype, docname)
# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
- frappe.db.set_value(doctype, docname, 'ewaybill', '')
- frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1)
+ frappe.db.set_value(doctype, docname, "ewaybill", "")
+ frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1)
+
@frappe.whitelist()
def generate_einvoices(docnames):
@@ -1073,40 +1319,40 @@ def generate_einvoices(docnames):
success = len(docnames) - len(failures)
frappe.msgprint(
- _('{} e-invoices generated successfully').format(success),
- title=_('Bulk E-Invoice Generation Complete')
+ _("{} e-invoices generated successfully").format(success),
+ title=_("Bulk E-Invoice Generation Complete"),
)
else:
enqueue_bulk_action(schedule_bulk_generate_irn, docnames=docnames)
+
def schedule_bulk_generate_irn(docnames):
failures = GSPConnector.bulk_generate_irn(docnames)
frappe.local.message_log = []
- frappe.publish_realtime("bulk_einvoice_generation_complete", {
- "user": frappe.session.user,
- "failures": failures,
- "invoices": docnames
- })
+ frappe.publish_realtime(
+ "bulk_einvoice_generation_complete",
+ {"user": frappe.session.user, "failures": failures, "invoices": docnames},
+ )
+
def show_bulk_action_failure_message(failures):
for doc in failures:
- docname = '{0}'.format(doc.get('docname'))
- message = doc.get('message').replace("'", '"')
- if message[0] == '[':
+ docname = '{0}'.format(doc.get("docname"))
+ message = doc.get("message").replace("'", '"')
+ if message[0] == "[":
errors = json.loads(message)
- error_list = ''.join(['
{}
'.format(err) for err in errors])
- message = '''{} has following errors:
-
\n\n This is to confirm that the {{ doc.company }} received an amount of {{doc.get_formatted(\"amount\")}}\n from {{ doc.donor_name }}\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n\n via the Mode of Payment {{doc.mode_of_payment}}\n\n {% if doc.razorpay_payment_id -%}\n bearing RazorPay Payment ID {{ doc.razorpay_payment_id }}\n {%- endif %}\n\n on {{ doc.get_formatted(\"date_of_donation\") }}\n
\n \n
\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n
\n\n
\n
\n\n
\n
{{doc.company_address_display }}
\n\n",
+ "html": "{% if letter_head and not no_letterhead -%}\n
\n\n This is to confirm that the {{ doc.company }} received an amount of {{doc.get_formatted(\"amount\")}}\n from {{ doc.donor_name }}\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n\n via the Mode of Payment {{doc.mode_of_payment}}\n\n {% if doc.payment_id -%}\n bearing Payment ID {{ doc.payment_id }}\n {%- endif %}\n\n on {{ doc.get_formatted(\"date_of_donation\") }}\n
\n \n
\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n
"
+ msg += " "
+ msg += (
+ ", ".join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "
"
+ )
- msg += _("Note: To merge the items, create a separate Stock Reconciliation for the old item {0}").format(
- frappe.bold(old_name))
+ msg += _(
+ "Note: To merge the items, create a separate Stock Reconciliation for the old item {0}"
+ ).format(frappe.bold(old_name))
frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError)
@@ -466,8 +541,8 @@ class Item(Document):
def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
"Block merge if both old and new items have product bundles."
- old_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name})
- new_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": new_name})
+ old_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": old_name})
+ new_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": new_name})
if old_bundle and new_bundle:
bundle_link = get_link_to_form("Product Bundle", old_bundle)
@@ -480,15 +555,14 @@ class Item(Document):
def validate_duplicate_website_item_before_merge(self, old_name, new_name):
"""
- Block merge if both old and new items have website items against them.
- This is to avoid duplicate website items after merging.
+ Block merge if both old and new items have website items against them.
+ This is to avoid duplicate website items after merging.
"""
web_items = frappe.get_all(
"Website Item",
- filters={
- "item_code": ["in", [old_name, new_name]]
- },
- fields=["item_code", "name"])
+ filters={"item_code": ["in", [old_name, new_name]]},
+ fields=["item_code", "name"],
+ )
if len(web_items) <= 1:
return
@@ -506,42 +580,61 @@ class Item(Document):
def recalculate_bin_qty(self, new_name):
from erpnext.stock.stock_balance import repost_stock
- existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
+
+ existing_allow_negative_stock = frappe.db.get_value(
+ "Stock Settings", None, "allow_negative_stock"
+ )
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
- repost_stock_for_warehouses = frappe.db.sql_list("""select distinct warehouse
- from tabBin where item_code=%s""", new_name)
+ repost_stock_for_warehouses = frappe.get_all(
+ "Stock Ledger Entry",
+ "warehouse",
+ filters={"item_code": new_name},
+ pluck="warehouse",
+ distinct=True,
+ )
# Delete all existing bins to avoid duplicate bins for the same item and warehouse
- frappe.db.sql("delete from `tabBin` where item_code=%s", new_name)
+ frappe.db.delete("Bin", {"item_code": new_name})
for warehouse in repost_stock_for_warehouses:
repost_stock(new_name, warehouse)
- frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
+ frappe.db.set_value(
+ "Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock
+ )
def update_bom_item_desc(self):
if self.is_new():
return
- if self.db_get('description') != self.description:
- frappe.db.sql("""
+ if self.db_get("description") != self.description:
+ frappe.db.sql(
+ """
update `tabBOM`
set description = %s
where item = %s and docstatus < 2
- """, (self.description, self.name))
+ """,
+ (self.description, self.name),
+ )
- frappe.db.sql("""
+ frappe.db.sql(
+ """
update `tabBOM Item`
set description = %s
where item_code = %s and docstatus < 2
- """, (self.description, self.name))
+ """,
+ (self.description, self.name),
+ )
- frappe.db.sql("""
+ frappe.db.sql(
+ """
update `tabBOM Explosion Item`
set description = %s
where item_code = %s and docstatus < 2
- """, (self.description, self.name))
+ """,
+ (self.description, self.name),
+ )
def validate_item_defaults(self):
companies = {row.company for row in self.item_defaults}
@@ -551,41 +644,61 @@ class Item(Document):
validate_item_default_company_links(self.item_defaults)
-
def update_defaults_from_item_group(self):
"""Get defaults from Item Group"""
if self.item_defaults or not self.item_group:
return
- item_defaults = frappe.db.get_values("Item Default", {"parent": self.item_group},
- ['company', 'default_warehouse','default_price_list','buying_cost_center','default_supplier',
- 'expense_account','selling_cost_center','income_account'], as_dict = 1)
+ item_defaults = frappe.db.get_values(
+ "Item Default",
+ {"parent": self.item_group},
+ [
+ "company",
+ "default_warehouse",
+ "default_price_list",
+ "buying_cost_center",
+ "default_supplier",
+ "expense_account",
+ "selling_cost_center",
+ "income_account",
+ ],
+ as_dict=1,
+ )
if item_defaults:
for item in item_defaults:
- self.append('item_defaults', {
- 'company': item.company,
- 'default_warehouse': item.default_warehouse,
- 'default_price_list': item.default_price_list,
- 'buying_cost_center': item.buying_cost_center,
- 'default_supplier': item.default_supplier,
- 'expense_account': item.expense_account,
- 'selling_cost_center': item.selling_cost_center,
- 'income_account': item.income_account
- })
+ self.append(
+ "item_defaults",
+ {
+ "company": item.company,
+ "default_warehouse": item.default_warehouse,
+ "default_price_list": item.default_price_list,
+ "buying_cost_center": item.buying_cost_center,
+ "default_supplier": item.default_supplier,
+ "expense_account": item.expense_account,
+ "selling_cost_center": item.selling_cost_center,
+ "income_account": item.income_account,
+ },
+ )
else:
defaults = frappe.defaults.get_defaults() or {}
# To check default warehouse is belong to the default company
- if defaults.get("default_warehouse") and defaults.company and frappe.db.exists("Warehouse",
- {'name': defaults.default_warehouse, 'company': defaults.company}):
- self.append("item_defaults", {
- "company": defaults.get("company"),
- "default_warehouse": defaults.default_warehouse
- })
+ if (
+ defaults.get("default_warehouse")
+ and defaults.company
+ and frappe.db.exists(
+ "Warehouse", {"name": defaults.default_warehouse, "company": defaults.company}
+ )
+ ):
+ self.append(
+ "item_defaults",
+ {"company": defaults.get("company"), "default_warehouse": defaults.default_warehouse},
+ )
def update_variants(self):
- if self.flags.dont_update_variants or \
- frappe.db.get_single_value('Item Variant Settings', 'do_not_update_variants'):
+ if self.flags.dont_update_variants or frappe.db.get_single_value(
+ "Item Variant Settings", "do_not_update_variants"
+ ):
return
if self.has_variants:
variants = frappe.db.get_all("Item", fields=["item_code"], filters={"variant_of": self.name})
@@ -594,8 +707,13 @@ class Item(Document):
update_variants(variants, self, publish_progress=False)
frappe.msgprint(_("Item Variants updated"))
else:
- frappe.enqueue("erpnext.stock.doctype.item.item.update_variants",
- variants=variants, template=self, now=frappe.flags.in_test, timeout=600)
+ frappe.enqueue(
+ "erpnext.stock.doctype.item.item.update_variants",
+ variants=variants,
+ template=self,
+ now=frappe.flags.in_test,
+ timeout=600,
+ )
def validate_has_variants(self):
if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"):
@@ -627,11 +745,8 @@ class Item(Document):
# fetch all attributes of these items
item_attributes = frappe.get_all(
"Item Variant Attribute",
- filters={
- "parent": ["in", items],
- "attribute": ["in", deleted_attribute]
- },
- fields=["attribute", "parent"]
+ filters={"parent": ["in", items], "attribute": ["in", deleted_attribute]},
+ fields=["attribute", "parent"],
)
not_included = defaultdict(list)
@@ -650,14 +765,18 @@ class Item(Document):
return """
{0}
{1}
-
""".format(title, body)
+ """.format(
+ title, body
+ )
- rows = ''
+ rows = ""
for docname, attr_list in not_included.items():
link = "{0}".format(frappe.bold(_(docname)))
rows += table_row(link, body(attr_list))
- error_description = _('The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template.')
+ error_description = _(
+ "The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template."
+ )
message = """
{0}
@@ -668,25 +787,37 @@ class Item(Document):
{3}
- """.format(error_description, _('Variant Items'), _('Attributes'), rows)
+ """.format(
+ error_description, _("Variant Items"), _("Attributes"), rows
+ )
frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True, wide=True)
-
def validate_stock_exists_for_template_item(self):
if self.stock_ledger_created() and self._doc_before_save:
- if (cint(self._doc_before_save.has_variants) != cint(self.has_variants)
- or self._doc_before_save.variant_of != self.variant_of):
- frappe.throw(_("Cannot change Variant properties after stock transaction. You will have to make a new Item to do this.").format(self.name),
- StockExistsForTemplate)
+ if (
+ cint(self._doc_before_save.has_variants) != cint(self.has_variants)
+ or self._doc_before_save.variant_of != self.variant_of
+ ):
+ frappe.throw(
+ _(
+ "Cannot change Variant properties after stock transaction. You will have to make a new Item to do this."
+ ).format(self.name),
+ StockExistsForTemplate,
+ )
if self.has_variants or self.variant_of:
- if not self.is_child_table_same('attributes'):
+ if not self.is_child_table_same("attributes"):
frappe.throw(
- _('Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item'))
+ _(
+ "Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item"
+ )
+ )
def validate_variant_based_on_change(self):
- if not self.is_new() and (self.variant_of or (self.has_variants and frappe.get_all("Item", {"variant_of": self.name}))):
+ if not self.is_new() and (
+ self.variant_of or (self.has_variants and frappe.get_all("Item", {"variant_of": self.name}))
+ ):
if self.variant_based_on != frappe.db.get_value("Item", self.name, "variant_based_on"):
frappe.throw(_("Variant Based On cannot be changed"))
@@ -699,8 +830,11 @@ class Item(Document):
if self.variant_of:
template_uom = frappe.db.get_value("Item", self.variant_of, "stock_uom")
if template_uom != self.stock_uom:
- frappe.throw(_("Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'")
- .format(self.stock_uom, template_uom))
+ frappe.throw(
+ _("Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'").format(
+ self.stock_uom, template_uom
+ )
+ )
def validate_uom_conversion_factor(self):
if self.uoms:
@@ -714,21 +848,22 @@ class Item(Document):
return
if not self.variant_based_on:
- self.variant_based_on = 'Item Attribute'
+ self.variant_based_on = "Item Attribute"
- if self.variant_based_on == 'Item Attribute':
+ if self.variant_based_on == "Item Attribute":
attributes = []
if not self.attributes:
frappe.throw(_("Attribute table is mandatory"))
for d in self.attributes:
if d.attribute in attributes:
frappe.throw(
- _("Attribute {0} selected multiple times in Attributes Table").format(d.attribute))
+ _("Attribute {0} selected multiple times in Attributes Table").format(d.attribute)
+ )
else:
attributes.append(d.attribute)
def validate_variant_attributes(self):
- if self.is_new() and self.variant_of and self.variant_based_on == 'Item Attribute':
+ if self.is_new() and self.variant_of and self.variant_based_on == "Item Attribute":
# remove attributes with no attribute_value set
self.attributes = [d for d in self.attributes if cstr(d.attribute_value).strip()]
@@ -739,8 +874,9 @@ class Item(Document):
variant = get_variant(self.variant_of, args, self.name)
if variant:
- frappe.throw(_("Item variant {0} exists with same attributes")
- .format(variant), ItemVariantExistsError)
+ frappe.throw(
+ _("Item variant {0} exists with same attributes").format(variant), ItemVariantExistsError
+ )
validate_item_variant_attributes(self, args)
@@ -755,31 +891,52 @@ class Item(Document):
fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no")
values = frappe.db.get_value("Item", self.name, fields, as_dict=True)
- if not values.get('valuation_method') and self.get('valuation_method'):
- values['valuation_method'] = frappe.db.get_single_value("Stock Settings", "valuation_method") or "FIFO"
+ if not values.get("valuation_method") and self.get("valuation_method"):
+ values["valuation_method"] = (
+ frappe.db.get_single_value("Stock Settings", "valuation_method") or "FIFO"
+ )
if values:
for field in fields:
if cstr(self.get(field)) != cstr(values.get(field)):
if self.check_if_linked_document_exists(field):
- frappe.throw(_("As there are existing transactions against item {0}, you can not change the value of {1}").format(self.name, frappe.bold(self.meta.get_label(field))))
+ frappe.throw(
+ _(
+ "As there are existing transactions against item {0}, you can not change the value of {1}"
+ ).format(self.name, frappe.bold(self.meta.get_label(field)))
+ )
def check_if_linked_document_exists(self, field):
- linked_doctypes = ["Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Receipt Item",
- "Purchase Invoice Item", "Stock Entry Detail", "Stock Reconciliation Item"]
+ linked_doctypes = [
+ "Delivery Note Item",
+ "Sales Invoice Item",
+ "POS Invoice Item",
+ "Purchase Receipt Item",
+ "Purchase Invoice Item",
+ "Stock Entry Detail",
+ "Stock Reconciliation Item",
+ ]
# For "Is Stock Item", following doctypes is important
# because reserved_qty, ordered_qty and requested_qty updated from these doctypes
if field == "is_stock_item":
- linked_doctypes += ["Sales Order Item", "Purchase Order Item", "Material Request Item", "Product Bundle"]
+ linked_doctypes += [
+ "Sales Order Item",
+ "Purchase Order Item",
+ "Material Request Item",
+ "Product Bundle",
+ ]
for doctype in linked_doctypes:
- filters={"item_code": self.name, "docstatus": 1}
+ filters = {"item_code": self.name, "docstatus": 1}
if doctype == "Product Bundle":
- filters={"new_item_code": self.name}
+ filters = {"new_item_code": self.name}
- if doctype in ("Purchase Invoice Item", "Sales Invoice Item",):
+ if doctype in (
+ "Purchase Invoice Item",
+ "Sales Invoice Item",
+ ):
# If Invoice has Stock impact, only then consider it.
if self.stock_ledger_created():
return True
@@ -789,37 +946,48 @@ class Item(Document):
def validate_auto_reorder_enabled_in_stock_settings(self):
if self.reorder_levels:
- enabled = frappe.db.get_single_value('Stock Settings', 'auto_indent')
+ enabled = frappe.db.get_single_value("Stock Settings", "auto_indent")
if not enabled:
- frappe.msgprint(msg=_("You have to enable auto re-order in Stock Settings to maintain re-order levels."), title=_("Enable Auto Re-Order"), indicator="orange")
+ frappe.msgprint(
+ msg=_("You have to enable auto re-order in Stock Settings to maintain re-order levels."),
+ title=_("Enable Auto Re-Order"),
+ indicator="orange",
+ )
def make_item_price(item, price_list_name, item_price):
- frappe.get_doc({
- 'doctype': 'Item Price',
- 'price_list': price_list_name,
- 'item_code': item,
- 'price_list_rate': item_price
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Item Price",
+ "price_list": price_list_name,
+ "item_code": item,
+ "price_list_rate": item_price,
+ }
+ ).insert()
+
def get_timeline_data(doctype, name):
"""get timeline data based on Stock Ledger Entry. This is displayed as heatmap on the item page."""
- items = frappe.db.sql("""select unix_timestamp(posting_date), count(*)
+ items = frappe.db.sql(
+ """select unix_timestamp(posting_date), count(*)
from `tabStock Ledger Entry`
where item_code=%s and posting_date > date_sub(curdate(), interval 1 year)
- group by posting_date""", name)
+ group by posting_date""",
+ name,
+ )
return dict(items)
-
def validate_end_of_life(item_code, end_of_life=None, disabled=None):
if (not end_of_life) or (disabled is None):
end_of_life, disabled = frappe.db.get_value("Item", item_code, ["end_of_life", "disabled"])
if end_of_life and end_of_life != "0000-00-00" and getdate(end_of_life) <= now_datetime().date():
- frappe.throw(_("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life)))
+ frappe.throw(
+ _("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life))
+ )
if disabled:
frappe.throw(_("Item {0} is disabled").format(item_code))
@@ -840,11 +1008,13 @@ def validate_cancelled_item(item_code, docstatus=None):
if docstatus == 2:
frappe.throw(_("Item {0} is cancelled").format(item_code))
+
def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
"""returns last purchase details in stock uom"""
# get last purchase order item details
- last_purchase_order = frappe.db.sql("""\
+ last_purchase_order = frappe.db.sql(
+ """\
select po.name, po.transaction_date, po.conversion_rate,
po_item.conversion_factor, po_item.base_price_list_rate,
po_item.discount_percentage, po_item.base_rate, po_item.base_net_rate
@@ -852,11 +1022,14 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
where po.docstatus = 1 and po_item.item_code = %s and po.name != %s and
po.name = po_item.parent
order by po.transaction_date desc, po.name desc
- limit 1""", (item_code, cstr(doc_name)), as_dict=1)
-
+ limit 1""",
+ (item_code, cstr(doc_name)),
+ as_dict=1,
+ )
# get last purchase receipt item details
- last_purchase_receipt = frappe.db.sql("""\
+ last_purchase_receipt = frappe.db.sql(
+ """\
select pr.name, pr.posting_date, pr.posting_time, pr.conversion_rate,
pr_item.conversion_factor, pr_item.base_price_list_rate, pr_item.discount_percentage,
pr_item.base_rate, pr_item.base_net_rate
@@ -864,20 +1037,29 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
where pr.docstatus = 1 and pr_item.item_code = %s and pr.name != %s and
pr.name = pr_item.parent
order by pr.posting_date desc, pr.posting_time desc, pr.name desc
- limit 1""", (item_code, cstr(doc_name)), as_dict=1)
+ limit 1""",
+ (item_code, cstr(doc_name)),
+ as_dict=1,
+ )
- purchase_order_date = getdate(last_purchase_order and last_purchase_order[0].transaction_date
- or "1900-01-01")
- purchase_receipt_date = getdate(last_purchase_receipt and
- last_purchase_receipt[0].posting_date or "1900-01-01")
+ purchase_order_date = getdate(
+ last_purchase_order and last_purchase_order[0].transaction_date or "1900-01-01"
+ )
+ purchase_receipt_date = getdate(
+ last_purchase_receipt and last_purchase_receipt[0].posting_date or "1900-01-01"
+ )
- if last_purchase_order and (purchase_order_date >= purchase_receipt_date or not last_purchase_receipt):
+ if last_purchase_order and (
+ purchase_order_date >= purchase_receipt_date or not last_purchase_receipt
+ ):
# use purchase order
last_purchase = last_purchase_order[0]
purchase_date = purchase_order_date
- elif last_purchase_receipt and (purchase_receipt_date > purchase_order_date or not last_purchase_order):
+ elif last_purchase_receipt and (
+ purchase_receipt_date > purchase_order_date or not last_purchase_order
+ ):
# use purchase receipt
last_purchase = last_purchase_receipt[0]
purchase_date = purchase_receipt_date
@@ -886,22 +1068,25 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
return frappe._dict()
conversion_factor = flt(last_purchase.conversion_factor)
- out = frappe._dict({
- "base_price_list_rate": flt(last_purchase.base_price_list_rate) / conversion_factor,
- "base_rate": flt(last_purchase.base_rate) / conversion_factor,
- "base_net_rate": flt(last_purchase.base_net_rate) / conversion_factor,
- "discount_percentage": flt(last_purchase.discount_percentage),
- "purchase_date": purchase_date
- })
-
+ out = frappe._dict(
+ {
+ "base_price_list_rate": flt(last_purchase.base_price_list_rate) / conversion_factor,
+ "base_rate": flt(last_purchase.base_rate) / conversion_factor,
+ "base_net_rate": flt(last_purchase.base_net_rate) / conversion_factor,
+ "discount_percentage": flt(last_purchase.discount_percentage),
+ "purchase_date": purchase_date,
+ }
+ )
conversion_rate = flt(conversion_rate) or 1.0
- out.update({
- "price_list_rate": out.base_price_list_rate / conversion_rate,
- "rate": out.base_rate / conversion_rate,
- "base_rate": out.base_rate,
- "base_net_rate": out.base_net_rate
- })
+ out.update(
+ {
+ "price_list_rate": out.base_price_list_rate / conversion_rate,
+ "rate": out.base_rate / conversion_rate,
+ "base_rate": out.base_rate,
+ "base_net_rate": out.base_net_rate,
+ }
+ )
return out
@@ -924,39 +1109,51 @@ def invalidate_item_variants_cache_for_website(doc):
is_web_item = doc.get("published_in_website") or doc.get("published")
if doc.has_variants and is_web_item:
item_code = doc.item_code
- elif doc.variant_of and frappe.db.get_value('Item', doc.variant_of, 'published_in_website'):
+ elif doc.variant_of and frappe.db.get_value("Item", doc.variant_of, "published_in_website"):
item_code = doc.variant_of
if item_code:
item_cache = ItemVariantsCacheManager(item_code)
item_cache.rebuild_cache()
+
def check_stock_uom_with_bin(item, stock_uom):
if stock_uom == frappe.db.get_value("Item", item, "stock_uom"):
return
- ref_uom = frappe.db.get_value("Stock Ledger Entry",
- {"item_code": item}, "stock_uom")
+ ref_uom = frappe.db.get_value("Stock Ledger Entry", {"item_code": item}, "stock_uom")
if ref_uom:
if cstr(ref_uom) != cstr(stock_uom):
- frappe.throw(_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.").format(item))
+ frappe.throw(
+ _(
+ "Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM."
+ ).format(item)
+ )
- bin_list = frappe.db.sql("""
+ bin_list = frappe.db.sql(
+ """
select * from tabBin where item_code = %s
and (reserved_qty > 0 or ordered_qty > 0 or indented_qty > 0 or planned_qty > 0)
and stock_uom != %s
- """, (item, stock_uom), as_dict=1)
+ """,
+ (item, stock_uom),
+ as_dict=1,
+ )
if bin_list:
- frappe.throw(_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item.").format(item))
+ frappe.throw(
+ _(
+ "Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item."
+ ).format(item)
+ )
# No SLE or documents against item. Bin UOM can be changed safely.
frappe.db.sql("""update tabBin set stock_uom=%s where item_code=%s""", (stock_uom, item))
def get_item_defaults(item_code, company):
- item = frappe.get_cached_doc('Item', item_code)
+ item = frappe.get_cached_doc("Item", item_code)
out = item.as_dict()
@@ -967,8 +1164,9 @@ def get_item_defaults(item_code, company):
out.update(row)
return out
+
def set_item_default(item_code, company, fieldname, value):
- item = frappe.get_cached_doc('Item', item_code)
+ item = frappe.get_cached_doc("Item", item_code)
for d in item.item_defaults:
if d.company == company:
@@ -977,10 +1175,11 @@ def set_item_default(item_code, company, fieldname, value):
return
# no row found, add a new row for the company
- d = item.append('item_defaults', {fieldname: value, "company": company})
+ d = item.append("item_defaults", {fieldname: value, "company": company})
d.db_insert()
item.clear_cache()
+
@frappe.whitelist()
def get_item_details(item_code, company=None):
out = frappe._dict()
@@ -992,30 +1191,36 @@ def get_item_details(item_code, company=None):
return out
+
@frappe.whitelist()
def get_uom_conv_factor(uom, stock_uom):
- """ Get UOM conversion factor from uom to stock_uom
- e.g. uom = "Kg", stock_uom = "Gram" then returns 1000.0
+ """Get UOM conversion factor from uom to stock_uom
+ e.g. uom = "Kg", stock_uom = "Gram" then returns 1000.0
"""
if uom == stock_uom:
return 1.0
- from_uom, to_uom = uom, stock_uom # renaming for readability
+ from_uom, to_uom = uom, stock_uom # renaming for readability
- exact_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": to_uom, "from_uom": from_uom}, ["value"], as_dict=1)
+ exact_match = frappe.db.get_value(
+ "UOM Conversion Factor", {"to_uom": to_uom, "from_uom": from_uom}, ["value"], as_dict=1
+ )
if exact_match:
return exact_match.value
- inverse_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": from_uom, "from_uom": to_uom}, ["value"], as_dict=1)
+ inverse_match = frappe.db.get_value(
+ "UOM Conversion Factor", {"to_uom": from_uom, "from_uom": to_uom}, ["value"], as_dict=1
+ )
if inverse_match:
return 1 / inverse_match.value
# This attempts to try and get conversion from intermediate UOM.
# case:
- # g -> mg = 1000
- # g -> kg = 0.001
- # therefore kg -> mg = 1000 / 0.001 = 1,000,000
- intermediate_match = frappe.db.sql("""
+ # g -> mg = 1000
+ # g -> kg = 0.001
+ # therefore kg -> mg = 1000 / 0.001 = 1,000,000
+ intermediate_match = frappe.db.sql(
+ """
select (first.value / second.value) as value
from `tabUOM Conversion Factor` first
join `tabUOM Conversion Factor` second
@@ -1024,7 +1229,10 @@ def get_uom_conv_factor(uom, stock_uom):
first.to_uom = %(to_uom)s
and second.to_uom = %(from_uom)s
limit 1
- """, {"to_uom": to_uom, "from_uom": from_uom}, as_dict=1)
+ """,
+ {"to_uom": to_uom, "from_uom": from_uom},
+ as_dict=1,
+ )
if intermediate_match:
return intermediate_match[0].value
@@ -1036,8 +1244,12 @@ def get_item_attribute(parent, attribute_value=""):
if not frappe.has_permission("Item"):
frappe.throw(_("No Permission"))
- return frappe.get_all("Item Attribute Value", fields = ["attribute_value"],
- filters = {'parent': parent, 'attribute_value': ("like", f"%{attribute_value}%")})
+ return frappe.get_all(
+ "Item Attribute Value",
+ fields=["attribute_value"],
+ filters={"parent": parent, "attribute_value": ("like", f"%{attribute_value}%")},
+ )
+
def update_variants(variants, template, publish_progress=True):
total = len(variants)
@@ -1048,6 +1260,7 @@ def update_variants(variants, template, publish_progress=True):
if publish_progress:
frappe.publish_progress(count / total * 100, title=_("Updating Variants..."))
+
@erpnext.allow_regional
def set_item_tax_from_hsn_code(item):
pass
@@ -1056,20 +1269,29 @@ def set_item_tax_from_hsn_code(item):
def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> None:
for item_default in item_defaults:
for doctype, field in [
- ['Warehouse', 'default_warehouse'],
- ['Cost Center', 'buying_cost_center'],
- ['Cost Center', 'selling_cost_center'],
- ['Account', 'expense_account'],
- ['Account', 'income_account']
+ ["Warehouse", "default_warehouse"],
+ ["Cost Center", "buying_cost_center"],
+ ["Cost Center", "selling_cost_center"],
+ ["Account", "expense_account"],
+ ["Account", "income_account"],
]:
if item_default.get(field):
- company = frappe.db.get_value(doctype, item_default.get(field), 'company', cache=True)
+ company = frappe.db.get_value(doctype, item_default.get(field), "company", cache=True)
if company and company != item_default.company:
- frappe.throw(_("Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.")
- .format(
+ frappe.throw(
+ _("Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.").format(
item_default.idx,
doctype,
frappe.bold(item_default.get(field)),
frappe.bold(item_default.company),
- frappe.bold(frappe.unscrub(field))
- ), title=_("Invalid Item Defaults"))
+ frappe.bold(frappe.unscrub(field)),
+ ),
+ title=_("Invalid Item Defaults"),
+ )
+
+
+@frappe.whitelist()
+def get_asset_naming_series():
+ from erpnext.assets.doctype.asset.asset import get_asset_naming_series
+
+ return get_asset_naming_series()
diff --git a/erpnext/stock/doctype/item/item_dashboard.py b/erpnext/stock/doctype/item/item_dashboard.py
index b0f1bbc2d52..3caed02d69b 100644
--- a/erpnext/stock/doctype/item/item_dashboard.py
+++ b/erpnext/stock/doctype/item/item_dashboard.py
@@ -1,48 +1,37 @@
-
from frappe import _
def get_data():
return {
- 'heatmap': True,
- 'heatmap_message': _('This is based on stock movement. See {0} for details')\
- .format('' + _('Stock Ledger') + ''),
- 'fieldname': 'item_code',
- 'non_standard_fieldnames': {
- 'Work Order': 'production_item',
- 'Product Bundle': 'new_item_code',
- 'BOM': 'item',
- 'Batch': 'item'
+ "heatmap": True,
+ "heatmap_message": _("This is based on stock movement. See {0} for details").format(
+ '' + _("Stock Ledger") + ""
+ ),
+ "fieldname": "item_code",
+ "non_standard_fieldnames": {
+ "Work Order": "production_item",
+ "Product Bundle": "new_item_code",
+ "BOM": "item",
+ "Batch": "item",
},
- 'transactions': [
+ "transactions": [
+ {"label": _("Groups"), "items": ["BOM", "Product Bundle", "Item Alternative"]},
+ {"label": _("Pricing"), "items": ["Item Price", "Pricing Rule"]},
+ {"label": _("Sell"), "items": ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]},
{
- 'label': _('Groups'),
- 'items': ['BOM', 'Product Bundle', 'Item Alternative']
+ "label": _("Buy"),
+ "items": [
+ "Material Request",
+ "Supplier Quotation",
+ "Request for Quotation",
+ "Purchase Order",
+ "Purchase Receipt",
+ "Purchase Invoice",
+ ],
},
- {
- 'label': _('Pricing'),
- 'items': ['Item Price', 'Pricing Rule']
- },
- {
- 'label': _('Sell'),
- 'items': ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice']
- },
- {
- 'label': _('Buy'),
- 'items': ['Material Request', 'Supplier Quotation', 'Request for Quotation',
- 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice']
- },
- {
- 'label': _('Manufacture'),
- 'items': ['Production Plan', 'Work Order', 'Item Manufacturer']
- },
- {
- 'label': _('Traceability'),
- 'items': ['Serial No', 'Batch']
- },
- {
- 'label': _('Move'),
- 'items': ['Stock Entry']
- }
- ]
+ {"label": _("Manufacture"), "items": ["Production Plan", "Work Order", "Item Manufacturer"]},
+ {"label": _("Traceability"), "items": ["Serial No", "Batch"]},
+ {"label": _("Move"), "items": ["Stock Entry"]},
+ {"label": _("E-commerce"), "items": ["Website Item"]},
+ ],
}
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index c912101a4ac..29d6bf2df8b 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -6,6 +6,7 @@ import json
import frappe
from frappe.test_runner import make_test_objects
+from frappe.tests.utils import FrappeTestCase, change_settings
from erpnext.controllers.item_variant import (
InvalidItemAttributeValueError,
@@ -24,22 +25,27 @@ from erpnext.stock.doctype.item.item import (
)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.get_item_details import get_item_details
-from erpnext.tests.utils import ERPNextTestCase, change_settings
test_ignore = ["BOM"]
test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"]
-def make_item(item_code, properties=None):
+
+def make_item(item_code=None, properties=None):
+ if not item_code:
+ item_code = frappe.generate_hash(length=16)
+
if frappe.db.exists("Item", item_code):
return frappe.get_doc("Item", item_code)
- item = frappe.get_doc({
- "doctype": "Item",
- "item_code": item_code,
- "item_name": item_code,
- "description": item_code,
- "item_group": "Products"
- })
+ item = frappe.get_doc(
+ {
+ "doctype": "Item",
+ "item_code": item_code,
+ "item_name": item_code,
+ "description": item_code,
+ "item_group": "Products",
+ }
+ )
if properties:
item.update(properties)
@@ -52,7 +58,8 @@ def make_item(item_code, properties=None):
return item
-class TestItem(ERPNextTestCase):
+
+class TestItem(FrappeTestCase):
def setUp(self):
super().setUp()
frappe.flags.attribute_values = None
@@ -94,56 +101,91 @@ class TestItem(ERPNextTestCase):
make_test_objects("Item Price")
company = "_Test Company"
- currency = frappe.get_cached_value("Company", company, "default_currency")
+ currency = frappe.get_cached_value("Company", company, "default_currency")
- details = get_item_details({
- "item_code": "_Test Item",
- "company": company,
- "price_list": "_Test Price List",
- "currency": currency,
- "doctype": "Sales Order",
- "conversion_rate": 1,
- "price_list_currency": currency,
- "plc_conversion_rate": 1,
- "order_type": "Sales",
- "customer": "_Test Customer",
- "conversion_factor": 1,
- "price_list_uom_dependant": 1,
- "ignore_pricing_rule": 1
- })
+ details = get_item_details(
+ {
+ "item_code": "_Test Item",
+ "company": company,
+ "price_list": "_Test Price List",
+ "currency": currency,
+ "doctype": "Sales Order",
+ "conversion_rate": 1,
+ "price_list_currency": currency,
+ "plc_conversion_rate": 1,
+ "order_type": "Sales",
+ "customer": "_Test Customer",
+ "conversion_factor": 1,
+ "price_list_uom_dependant": 1,
+ "ignore_pricing_rule": 1,
+ }
+ )
for key, value in to_check.items():
self.assertEqual(value, details.get(key))
def test_item_tax_template(self):
expected_item_tax_template = [
- {"item_code": "_Test Item With Item Tax Template", "tax_category": "",
- "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"},
- {"item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 1",
- "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"},
- {"item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 2",
- "item_tax_template": None},
-
- {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "",
- "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"},
- {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 1",
- "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"},
- {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 2",
- "item_tax_template": None},
-
- {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "",
- "item_tax_template": "_Test Account Excise Duty @ 15 - _TC"},
- {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 1",
- "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"},
- {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 2",
- "item_tax_template": None},
-
- {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "",
- "item_tax_template": "_Test Account Excise Duty @ 20 - _TC"},
- {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 1",
- "item_tax_template": "_Test Item Tax Template 1 - _TC"},
- {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 2",
- "item_tax_template": None},
+ {
+ "item_code": "_Test Item With Item Tax Template",
+ "tax_category": "",
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
+ },
+ {
+ "item_code": "_Test Item With Item Tax Template",
+ "tax_category": "_Test Tax Category 1",
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
+ },
+ {
+ "item_code": "_Test Item With Item Tax Template",
+ "tax_category": "_Test Tax Category 2",
+ "item_tax_template": None,
+ },
+ {
+ "item_code": "_Test Item Inherit Group Item Tax Template 1",
+ "tax_category": "",
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
+ },
+ {
+ "item_code": "_Test Item Inherit Group Item Tax Template 1",
+ "tax_category": "_Test Tax Category 1",
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
+ },
+ {
+ "item_code": "_Test Item Inherit Group Item Tax Template 1",
+ "tax_category": "_Test Tax Category 2",
+ "item_tax_template": None,
+ },
+ {
+ "item_code": "_Test Item Inherit Group Item Tax Template 2",
+ "tax_category": "",
+ "item_tax_template": "_Test Account Excise Duty @ 15 - _TC",
+ },
+ {
+ "item_code": "_Test Item Inherit Group Item Tax Template 2",
+ "tax_category": "_Test Tax Category 1",
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
+ },
+ {
+ "item_code": "_Test Item Inherit Group Item Tax Template 2",
+ "tax_category": "_Test Tax Category 2",
+ "item_tax_template": None,
+ },
+ {
+ "item_code": "_Test Item Override Group Item Tax Template",
+ "tax_category": "",
+ "item_tax_template": "_Test Account Excise Duty @ 20 - _TC",
+ },
+ {
+ "item_code": "_Test Item Override Group Item Tax Template",
+ "tax_category": "_Test Tax Category 1",
+ "item_tax_template": "_Test Item Tax Template 1 - _TC",
+ },
+ {
+ "item_code": "_Test Item Override Group Item Tax Template",
+ "tax_category": "_Test Tax Category 2",
+ "item_tax_template": None,
+ },
]
expected_item_tax_map = {
@@ -152,43 +194,55 @@ class TestItem(ERPNextTestCase):
"_Test Account Excise Duty @ 12 - _TC": {"_Test Account Excise Duty - _TC": 12},
"_Test Account Excise Duty @ 15 - _TC": {"_Test Account Excise Duty - _TC": 15},
"_Test Account Excise Duty @ 20 - _TC": {"_Test Account Excise Duty - _TC": 20},
- "_Test Item Tax Template 1 - _TC": {"_Test Account Excise Duty - _TC": 5, "_Test Account Education Cess - _TC": 10,
- "_Test Account S&H Education Cess - _TC": 15}
+ "_Test Item Tax Template 1 - _TC": {
+ "_Test Account Excise Duty - _TC": 5,
+ "_Test Account Education Cess - _TC": 10,
+ "_Test Account S&H Education Cess - _TC": 15,
+ },
}
for data in expected_item_tax_template:
- details = get_item_details({
- "item_code": data['item_code'],
- "tax_category": data['tax_category'],
- "company": "_Test Company",
- "price_list": "_Test Price List",
- "currency": "_Test Currency",
- "doctype": "Sales Order",
- "conversion_rate": 1,
- "price_list_currency": "_Test Currency",
- "plc_conversion_rate": 1,
- "order_type": "Sales",
- "customer": "_Test Customer",
- "conversion_factor": 1,
- "price_list_uom_dependant": 1,
- "ignore_pricing_rule": 1
- })
+ details = get_item_details(
+ {
+ "item_code": data["item_code"],
+ "tax_category": data["tax_category"],
+ "company": "_Test Company",
+ "price_list": "_Test Price List",
+ "currency": "_Test Currency",
+ "doctype": "Sales Order",
+ "conversion_rate": 1,
+ "price_list_currency": "_Test Currency",
+ "plc_conversion_rate": 1,
+ "order_type": "Sales",
+ "customer": "_Test Customer",
+ "conversion_factor": 1,
+ "price_list_uom_dependant": 1,
+ "ignore_pricing_rule": 1,
+ }
+ )
- self.assertEqual(details.item_tax_template, data['item_tax_template'])
- self.assertEqual(json.loads(details.item_tax_rate), expected_item_tax_map[details.item_tax_template])
+ self.assertEqual(details.item_tax_template, data["item_tax_template"])
+ self.assertEqual(
+ json.loads(details.item_tax_rate), expected_item_tax_map[details.item_tax_template]
+ )
def test_item_defaults(self):
frappe.delete_doc_if_exists("Item", "Test Item With Defaults", force=1)
- make_item("Test Item With Defaults", {
- "item_group": "_Test Item Group",
- "brand": "_Test Brand With Item Defaults",
- "item_defaults": [{
- "company": "_Test Company",
- "default_warehouse": "_Test Warehouse 2 - _TC", # no override
- "expense_account": "_Test Account Stock Expenses - _TC", # override brand default
- "buying_cost_center": "_Test Write Off Cost Center - _TC", # override item group default
- }]
- })
+ make_item(
+ "Test Item With Defaults",
+ {
+ "item_group": "_Test Item Group",
+ "brand": "_Test Brand With Item Defaults",
+ "item_defaults": [
+ {
+ "company": "_Test Company",
+ "default_warehouse": "_Test Warehouse 2 - _TC", # no override
+ "expense_account": "_Test Account Stock Expenses - _TC", # override brand default
+ "buying_cost_center": "_Test Write Off Cost Center - _TC", # override item group default
+ }
+ ],
+ },
+ )
sales_item_check = {
"item_code": "Test Item With Defaults",
@@ -197,17 +251,19 @@ class TestItem(ERPNextTestCase):
"expense_account": "_Test Account Stock Expenses - _TC", # from item
"cost_center": "_Test Cost Center 2 - _TC", # from item group
}
- sales_item_details = get_item_details({
- "item_code": "Test Item With Defaults",
- "company": "_Test Company",
- "price_list": "_Test Price List",
- "currency": "_Test Currency",
- "doctype": "Sales Invoice",
- "conversion_rate": 1,
- "price_list_currency": "_Test Currency",
- "plc_conversion_rate": 1,
- "customer": "_Test Customer",
- })
+ sales_item_details = get_item_details(
+ {
+ "item_code": "Test Item With Defaults",
+ "company": "_Test Company",
+ "price_list": "_Test Price List",
+ "currency": "_Test Currency",
+ "doctype": "Sales Invoice",
+ "conversion_rate": 1,
+ "price_list_currency": "_Test Currency",
+ "plc_conversion_rate": 1,
+ "customer": "_Test Customer",
+ }
+ )
for key, value in sales_item_check.items():
self.assertEqual(value, sales_item_details.get(key))
@@ -216,38 +272,47 @@ class TestItem(ERPNextTestCase):
"warehouse": "_Test Warehouse 2 - _TC", # from item
"expense_account": "_Test Account Stock Expenses - _TC", # from item
"income_account": "_Test Account Sales - _TC", # from brand
- "cost_center": "_Test Write Off Cost Center - _TC" # from item
+ "cost_center": "_Test Write Off Cost Center - _TC", # from item
}
- purchase_item_details = get_item_details({
- "item_code": "Test Item With Defaults",
- "company": "_Test Company",
- "price_list": "_Test Price List",
- "currency": "_Test Currency",
- "doctype": "Purchase Invoice",
- "conversion_rate": 1,
- "price_list_currency": "_Test Currency",
- "plc_conversion_rate": 1,
- "supplier": "_Test Supplier",
- })
+ purchase_item_details = get_item_details(
+ {
+ "item_code": "Test Item With Defaults",
+ "company": "_Test Company",
+ "price_list": "_Test Price List",
+ "currency": "_Test Currency",
+ "doctype": "Purchase Invoice",
+ "conversion_rate": 1,
+ "price_list_currency": "_Test Currency",
+ "plc_conversion_rate": 1,
+ "supplier": "_Test Supplier",
+ }
+ )
for key, value in purchase_item_check.items():
self.assertEqual(value, purchase_item_details.get(key))
def test_item_default_validations(self):
with self.assertRaises(frappe.ValidationError) as ve:
- make_item("Bad Item defaults", {
- "item_group": "_Test Item Group",
- "item_defaults": [{
- "company": "_Test Company 1",
- "default_warehouse": "_Test Warehouse - _TC",
- "expense_account": "Stock In Hand - _TC",
- "buying_cost_center": "_Test Cost Center - _TC",
- "selling_cost_center": "_Test Cost Center - _TC",
- }]
- })
+ make_item(
+ "Bad Item defaults",
+ {
+ "item_group": "_Test Item Group",
+ "item_defaults": [
+ {
+ "company": "_Test Company 1",
+ "default_warehouse": "_Test Warehouse - _TC",
+ "expense_account": "Stock In Hand - _TC",
+ "buying_cost_center": "_Test Cost Center - _TC",
+ "selling_cost_center": "_Test Cost Center - _TC",
+ }
+ ],
+ },
+ )
- self.assertTrue("belong to company" in str(ve.exception).lower(),
- msg="Mismatching company entities in item defaults should not be allowed.")
+ self.assertTrue(
+ "belong to company" in str(ve.exception).lower(),
+ msg="Mismatching company entities in item defaults should not be allowed.",
+ )
def test_item_attribute_change_after_variant(self):
frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1)
@@ -255,7 +320,7 @@ class TestItem(ERPNextTestCase):
variant = create_variant("_Test Variant Item", {"Test Size": "Large"})
variant.save()
- attribute = frappe.get_doc('Item Attribute', 'Test Size')
+ attribute = frappe.get_doc("Item Attribute", "Test Size")
attribute.item_attribute_values = []
# reset flags
@@ -278,20 +343,18 @@ class TestItem(ERPNextTestCase):
def test_copy_fields_from_template_to_variants(self):
frappe.delete_doc_if_exists("Item", "_Test Variant Item-XL", force=1)
- fields = [{'field_name': 'item_group'}, {'field_name': 'is_stock_item'}]
- allow_fields = [d.get('field_name') for d in fields]
+ fields = [{"field_name": "item_group"}, {"field_name": "is_stock_item"}]
+ allow_fields = [d.get("field_name") for d in fields]
set_item_variant_settings(fields)
- if not frappe.db.get_value('Item Attribute Value',
- {'parent': 'Test Size', 'attribute_value': 'Extra Large'}, 'name'):
- item_attribute = frappe.get_doc('Item Attribute', 'Test Size')
- item_attribute.append('item_attribute_values', {
- 'attribute_value' : 'Extra Large',
- 'abbr': 'XL'
- })
+ if not frappe.db.get_value(
+ "Item Attribute Value", {"parent": "Test Size", "attribute_value": "Extra Large"}, "name"
+ ):
+ item_attribute = frappe.get_doc("Item Attribute", "Test Size")
+ item_attribute.append("item_attribute_values", {"attribute_value": "Extra Large", "abbr": "XL"})
item_attribute.save()
- template = frappe.get_doc('Item', '_Test Variant Item')
+ template = frappe.get_doc("Item", "_Test Variant Item")
template.item_group = "_Test Item Group D"
template.save()
@@ -300,93 +363,94 @@ class TestItem(ERPNextTestCase):
variant.item_name = "_Test Variant Item-XL"
variant.save()
- variant = frappe.get_doc('Item', '_Test Variant Item-XL')
+ variant = frappe.get_doc("Item", "_Test Variant Item-XL")
for fieldname in allow_fields:
self.assertEqual(template.get(fieldname), variant.get(fieldname))
- template = frappe.get_doc('Item', '_Test Variant Item')
+ template = frappe.get_doc("Item", "_Test Variant Item")
template.item_group = "_Test Item Group Desktops"
template.save()
def test_make_item_variant_with_numeric_values(self):
# cleanup
- for d in frappe.db.get_all('Item', filters={'variant_of':
- '_Test Numeric Template Item'}):
+ for d in frappe.db.get_all("Item", filters={"variant_of": "_Test Numeric Template Item"}):
frappe.delete_doc_if_exists("Item", d.name)
frappe.delete_doc_if_exists("Item", "_Test Numeric Template Item")
frappe.delete_doc_if_exists("Item Attribute", "Test Item Length")
- frappe.db.sql('''delete from `tabItem Variant Attribute`
- where attribute="Test Item Length"''')
+ frappe.db.sql(
+ '''delete from `tabItem Variant Attribute`
+ where attribute="Test Item Length"'''
+ )
frappe.flags.attribute_values = None
# make item attribute
- frappe.get_doc({
- "doctype": "Item Attribute",
- "attribute_name": "Test Item Length",
- "numeric_values": 1,
- "from_range": 0.0,
- "to_range": 100.0,
- "increment": 0.5
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Item Attribute",
+ "attribute_name": "Test Item Length",
+ "numeric_values": 1,
+ "from_range": 0.0,
+ "to_range": 100.0,
+ "increment": 0.5,
+ }
+ ).insert()
# make template item
- make_item("_Test Numeric Template Item", {
- "attributes": [
- {
- "attribute": "Test Size"
- },
- {
- "attribute": "Test Item Length",
- "numeric_values": 1,
- "from_range": 0.0,
- "to_range": 100.0,
- "increment": 0.5
- }
- ],
- "item_defaults": [
- {
- "default_warehouse": "_Test Warehouse - _TC",
- "company": "_Test Company"
- }
- ],
- "has_variants": 1
- })
+ make_item(
+ "_Test Numeric Template Item",
+ {
+ "attributes": [
+ {"attribute": "Test Size"},
+ {
+ "attribute": "Test Item Length",
+ "numeric_values": 1,
+ "from_range": 0.0,
+ "to_range": 100.0,
+ "increment": 0.5,
+ },
+ ],
+ "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}],
+ "has_variants": 1,
+ },
+ )
- variant = create_variant("_Test Numeric Template Item",
- {"Test Size": "Large", "Test Item Length": 1.1})
+ variant = create_variant(
+ "_Test Numeric Template Item", {"Test Size": "Large", "Test Item Length": 1.1}
+ )
self.assertEqual(variant.item_code, "_Test Numeric Template Item-L-1.1")
variant.item_code = "_Test Numeric Variant-L-1.1"
variant.item_name = "_Test Numeric Variant Large 1.1m"
self.assertRaises(InvalidItemAttributeValueError, variant.save)
- variant = create_variant("_Test Numeric Template Item",
- {"Test Size": "Large", "Test Item Length": 1.5})
+ variant = create_variant(
+ "_Test Numeric Template Item", {"Test Size": "Large", "Test Item Length": 1.5}
+ )
self.assertEqual(variant.item_code, "_Test Numeric Template Item-L-1.5")
variant.item_code = "_Test Numeric Variant-L-1.5"
variant.item_name = "_Test Numeric Variant Large 1.5m"
variant.save()
def test_item_merging(self):
- create_item("Test Item for Merging 1")
- create_item("Test Item for Merging 2")
+ old = create_item(frappe.generate_hash(length=20)).name
+ new = create_item(frappe.generate_hash(length=20)).name
- make_stock_entry(item_code="Test Item for Merging 1", target="_Test Warehouse - _TC",
- qty=1, rate=100)
- make_stock_entry(item_code="Test Item for Merging 2", target="_Test Warehouse 1 - _TC",
- qty=1, rate=100)
+ make_stock_entry(item_code=old, target="_Test Warehouse - _TC", qty=1, rate=100)
+ make_stock_entry(item_code=old, target="_Test Warehouse 1 - _TC", qty=1, rate=100)
+ make_stock_entry(item_code=new, target="_Test Warehouse 1 - _TC", qty=1, rate=100)
- frappe.rename_doc("Item", "Test Item for Merging 1", "Test Item for Merging 2", merge=True)
+ frappe.rename_doc("Item", old, new, merge=True)
- self.assertFalse(frappe.db.exists("Item", "Test Item for Merging 1"))
+ self.assertFalse(frappe.db.exists("Item", old))
- self.assertTrue(frappe.db.get_value("Bin",
- {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse - _TC"}))
-
- self.assertTrue(frappe.db.get_value("Bin",
- {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"}))
+ self.assertTrue(
+ frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse - _TC"})
+ )
+ self.assertTrue(
+ frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse 1 - _TC"})
+ )
def test_item_merging_with_product_bundle(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
@@ -409,13 +473,12 @@ class TestItem(ERPNextTestCase):
self.assertFalse(frappe.db.exists("Item", "Test Item Bundle Item 1"))
def test_uom_conversion_factor(self):
- if frappe.db.exists('Item', 'Test Item UOM'):
- frappe.delete_doc('Item', 'Test Item UOM')
+ if frappe.db.exists("Item", "Test Item UOM"):
+ frappe.delete_doc("Item", "Test Item UOM")
- item_doc = make_item("Test Item UOM", {
- "stock_uom": "Gram",
- "uoms": [dict(uom='Carat'), dict(uom='Kg')]
- })
+ item_doc = make_item(
+ "Test Item UOM", {"stock_uom": "Gram", "uoms": [dict(uom="Carat"), dict(uom="Kg")]}
+ )
for d in item_doc.uoms:
value = get_uom_conv_factor(d.uom, item_doc.stock_uom)
@@ -435,48 +498,46 @@ class TestItem(ERPNextTestCase):
self.assertEqual(factor, 1.0)
def test_item_variant_by_manufacturer(self):
- fields = [{'field_name': 'description'}, {'field_name': 'variant_based_on'}]
+ fields = [{"field_name": "description"}, {"field_name": "variant_based_on"}]
set_item_variant_settings(fields)
- if frappe.db.exists('Item', '_Test Variant Mfg'):
- frappe.delete_doc('Item', '_Test Variant Mfg')
- if frappe.db.exists('Item', '_Test Variant Mfg-1'):
- frappe.delete_doc('Item', '_Test Variant Mfg-1')
- if frappe.db.exists('Manufacturer', 'MSG1'):
- frappe.delete_doc('Manufacturer', 'MSG1')
+ if frappe.db.exists("Item", "_Test Variant Mfg"):
+ frappe.delete_doc("Item", "_Test Variant Mfg")
+ if frappe.db.exists("Item", "_Test Variant Mfg-1"):
+ frappe.delete_doc("Item", "_Test Variant Mfg-1")
+ if frappe.db.exists("Manufacturer", "MSG1"):
+ frappe.delete_doc("Manufacturer", "MSG1")
- template = frappe.get_doc(dict(
- doctype='Item',
- item_code='_Test Variant Mfg',
- has_variant=1,
- item_group='Products',
- variant_based_on='Manufacturer'
- )).insert()
+ template = frappe.get_doc(
+ dict(
+ doctype="Item",
+ item_code="_Test Variant Mfg",
+ has_variant=1,
+ item_group="Products",
+ variant_based_on="Manufacturer",
+ )
+ ).insert()
- manufacturer = frappe.get_doc(dict(
- doctype='Manufacturer',
- short_name='MSG1'
- )).insert()
+ manufacturer = frappe.get_doc(dict(doctype="Manufacturer", short_name="MSG1")).insert()
variant = get_variant(template.name, manufacturer=manufacturer.name)
- self.assertEqual(variant.item_code, '_Test Variant Mfg-1')
- self.assertEqual(variant.description, '_Test Variant Mfg')
- self.assertEqual(variant.manufacturer, 'MSG1')
+ self.assertEqual(variant.item_code, "_Test Variant Mfg-1")
+ self.assertEqual(variant.description, "_Test Variant Mfg")
+ self.assertEqual(variant.manufacturer, "MSG1")
variant.insert()
- variant = get_variant(template.name, manufacturer=manufacturer.name,
- manufacturer_part_no='007')
- self.assertEqual(variant.item_code, '_Test Variant Mfg-2')
- self.assertEqual(variant.description, '_Test Variant Mfg')
- self.assertEqual(variant.manufacturer, 'MSG1')
- self.assertEqual(variant.manufacturer_part_no, '007')
+ variant = get_variant(template.name, manufacturer=manufacturer.name, manufacturer_part_no="007")
+ self.assertEqual(variant.item_code, "_Test Variant Mfg-2")
+ self.assertEqual(variant.description, "_Test Variant Mfg")
+ self.assertEqual(variant.manufacturer, "MSG1")
+ self.assertEqual(variant.manufacturer_part_no, "007")
def test_stock_exists_against_template_item(self):
- stock_item = frappe.get_all('Stock Ledger Entry', fields = ["item_code"], limit=1)
+ stock_item = frappe.get_all("Stock Ledger Entry", fields=["item_code"], limit=1)
if stock_item:
item_code = stock_item[0].item_code
- item_doc = frappe.get_doc('Item', item_code)
+ item_doc = frappe.get_doc("Item", item_code)
item_doc.has_variants = 1
self.assertRaises(StockExistsForTemplate, item_doc.save)
@@ -489,37 +550,27 @@ class TestItem(ERPNextTestCase):
# Create new item and add barcodes
barcode_properties_list = [
- {
- "barcode": "0012345678905",
- "barcode_type": "EAN"
- },
- {
- "barcode": "012345678905",
- "barcode_type": "UAN"
- },
+ {"barcode": "0012345678905", "barcode_type": "EAN"},
+ {"barcode": "012345678905", "barcode_type": "UAN"},
{
"barcode": "ARBITRARY_TEXT",
- }
+ },
]
create_item(item_code)
for barcode_properties in barcode_properties_list:
- item_doc = frappe.get_doc('Item', item_code)
- new_barcode = item_doc.append('barcodes')
+ item_doc = frappe.get_doc("Item", item_code)
+ new_barcode = item_doc.append("barcodes")
new_barcode.update(barcode_properties)
item_doc.save()
# Check values saved correctly
barcodes = frappe.get_all(
- 'Item Barcode',
- fields=['barcode', 'barcode_type'],
- filters={'parent': item_code})
+ "Item Barcode", fields=["barcode", "barcode_type"], filters={"parent": item_code}
+ )
for barcode_properties in barcode_properties_list:
- barcode_to_find = barcode_properties['barcode']
- matching_barcodes = [
- x for x in barcodes
- if x['barcode'] == barcode_to_find
- ]
+ barcode_to_find = barcode_properties["barcode"]
+ matching_barcodes = [x for x in barcodes if x["barcode"] == barcode_to_find]
self.assertEqual(len(matching_barcodes), 1)
details = matching_barcodes[0]
@@ -527,20 +578,21 @@ class TestItem(ERPNextTestCase):
self.assertEqual(value, details.get(key))
# Add barcode again - should cause DuplicateEntryError
- item_doc = frappe.get_doc('Item', item_code)
- new_barcode = item_doc.append('barcodes')
+ item_doc = frappe.get_doc("Item", item_code)
+ new_barcode = item_doc.append("barcodes")
new_barcode.update(barcode_properties_list[0])
self.assertRaises(frappe.UniqueValidationError, item_doc.save)
# Add invalid barcode - should cause InvalidBarcode
- item_doc = frappe.get_doc('Item', item_code)
- new_barcode = item_doc.append('barcodes')
- new_barcode.barcode = '9999999999999'
- new_barcode.barcode_type = 'EAN'
+ item_doc = frappe.get_doc("Item", item_code)
+ new_barcode = item_doc.append("barcodes")
+ new_barcode.barcode = "9999999999999"
+ new_barcode.barcode_type = "EAN"
self.assertRaises(InvalidBarcode, item_doc.save)
def test_heatmap_data(self):
import time
+
data = get_timeline_data("Item", "_Test Item")
self.assertTrue(isinstance(data, dict))
@@ -572,20 +624,17 @@ class TestItem(ERPNextTestCase):
def test_check_stock_uom_with_bin_no_sle(self):
from erpnext.stock.stock_balance import update_bin_qty
+
item = create_item("_Item with bin qty")
item.stock_uom = "Gram"
item.save()
- update_bin_qty(item.item_code, "_Test Warehouse - _TC", {
- "reserved_qty": 10
- })
+ update_bin_qty(item.item_code, "_Test Warehouse - _TC", {"reserved_qty": 10})
item.stock_uom = "Kilometer"
self.assertRaises(frappe.ValidationError, item.save)
- update_bin_qty(item.item_code, "_Test Warehouse - _TC", {
- "reserved_qty": 0
- })
+ update_bin_qty(item.item_code, "_Test Warehouse - _TC", {"reserved_qty": 0})
item.load_from_db()
item.stock_uom = "Kilometer"
@@ -618,12 +667,36 @@ class TestItem(ERPNextTestCase):
item.item_group = "All Item Groups"
item.save() # if item code saved without item_code then series worked
+ @change_settings("Stock Settings", {"sample_retention_warehouse": "_Test Warehouse - _TC"})
+ def test_retain_sample(self):
+ item = make_item(
+ "_TestRetainSample", {"has_batch_no": 1, "retain_sample": 1, "sample_quantity": 1}
+ )
+
+ self.assertEqual(item.has_batch_no, 1)
+ self.assertEqual(item.retain_sample, 1)
+ self.assertEqual(item.sample_quantity, 1)
+
+ item.has_batch_no = None
+ item.save()
+ self.assertEqual(item.retain_sample, None)
+ self.assertEqual(item.sample_quantity, None)
+ item.delete()
+
+ def test_empty_description(self):
+ item = make_item(properties={"description": ""})
+ self.assertEqual(item.description, item.item_name)
+ item.description = ""
+ item.save()
+ self.assertEqual(item.description, item.item_name)
+
def set_item_variant_settings(fields):
- doc = frappe.get_doc('Item Variant Settings')
- doc.set('fields', fields)
+ doc = frappe.get_doc("Item Variant Settings")
+ doc.set("fields", fields)
doc.save()
+
def make_item_variant():
if not frappe.db.exists("Item", "_Test Variant Item-S"):
variant = create_variant("_Test Variant Item", """{"Test Size": "Small"}""")
@@ -631,11 +704,23 @@ def make_item_variant():
variant.item_name = "_Test Variant Item-S"
variant.save()
-test_records = frappe.get_test_records('Item')
-def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test Warehouse - _TC",
- is_customer_provided_item=None, customer=None, is_purchase_item=None, opening_stock=0, is_fixed_asset=0,
- asset_category=None, company="_Test Company"):
+test_records = frappe.get_test_records("Item")
+
+
+def create_item(
+ item_code,
+ is_stock_item=1,
+ valuation_rate=0,
+ warehouse="_Test Warehouse - _TC",
+ is_customer_provided_item=None,
+ customer=None,
+ is_purchase_item=None,
+ opening_stock=0,
+ is_fixed_asset=0,
+ asset_category=None,
+ company="_Test Company",
+):
if not frappe.db.exists("Item", item_code):
item = frappe.new_doc("Item")
item.item_code = item_code
@@ -649,11 +734,8 @@ def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test W
item.valuation_rate = valuation_rate
item.is_purchase_item = is_purchase_item
item.is_customer_provided_item = is_customer_provided_item
- item.customer = customer or ''
- item.append("item_defaults", {
- "default_warehouse": warehouse,
- "company": company
- })
+ item.customer = customer or ""
+ item.append("item_defaults", {"default_warehouse": warehouse, "company": company})
item.save()
else:
item = frappe.get_doc("Item", item_code)
diff --git a/erpnext/stock/doctype/item_alternative/item_alternative.py b/erpnext/stock/doctype/item_alternative/item_alternative.py
index 766647b32e5..0f93bb9e95b 100644
--- a/erpnext/stock/doctype/item_alternative/item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/item_alternative.py
@@ -14,8 +14,7 @@ class ItemAlternative(Document):
self.validate_duplicate()
def has_alternative_item(self):
- if (self.item_code and
- not frappe.db.get_value('Item', self.item_code, 'allow_alternative_item')):
+ if self.item_code and not frappe.db.get_value("Item", self.item_code, "allow_alternative_item"):
frappe.throw(_("Not allow to set alternative item for the item {0}").format(self.item_code))
def validate_alternative_item(self):
@@ -23,19 +22,32 @@ class ItemAlternative(Document):
frappe.throw(_("Alternative item must not be same as item code"))
item_meta = frappe.get_meta("Item")
- fields = ["is_stock_item", "include_item_in_manufacturing","has_serial_no", "has_batch_no", "allow_alternative_item"]
+ fields = [
+ "is_stock_item",
+ "include_item_in_manufacturing",
+ "has_serial_no",
+ "has_batch_no",
+ "allow_alternative_item",
+ ]
item_data = frappe.db.get_value("Item", self.item_code, fields, as_dict=1)
- alternative_item_data = frappe.db.get_value("Item", self.alternative_item_code, fields, as_dict=1)
+ alternative_item_data = frappe.db.get_value(
+ "Item", self.alternative_item_code, fields, as_dict=1
+ )
for field in fields:
- if item_data.get(field) != alternative_item_data.get(field):
+ if item_data.get(field) != alternative_item_data.get(field):
raise_exception, alert = [1, False] if field == "is_stock_item" else [0, True]
- frappe.msgprint(_("The value of {0} differs between Items {1} and {2}") \
- .format(frappe.bold(item_meta.get_label(field)),
- frappe.bold(self.alternative_item_code),
- frappe.bold(self.item_code)),
- alert=alert, raise_exception=raise_exception, indicator="Orange")
+ frappe.msgprint(
+ _("The value of {0} differs between Items {1} and {2}").format(
+ frappe.bold(item_meta.get_label(field)),
+ frappe.bold(self.alternative_item_code),
+ frappe.bold(self.item_code),
+ ),
+ alert=alert,
+ raise_exception=raise_exception,
+ indicator="Orange",
+ )
alternate_item_check_msg = _("Allow Alternative Item must be checked on Item {}")
@@ -44,24 +56,30 @@ class ItemAlternative(Document):
if self.two_way and not alternative_item_data.allow_alternative_item:
frappe.throw(alternate_item_check_msg.format(self.item_code))
-
-
-
def validate_duplicate(self):
- if frappe.db.get_value("Item Alternative", {'item_code': self.item_code,
- 'alternative_item_code': self.alternative_item_code, 'name': ('!=', self.name)}):
+ if frappe.db.get_value(
+ "Item Alternative",
+ {
+ "item_code": self.item_code,
+ "alternative_item_code": self.alternative_item_code,
+ "name": ("!=", self.name),
+ },
+ ):
frappe.throw(_("Already record exists for the item {0}").format(self.item_code))
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_alternative_items(doctype, txt, searchfield, start, page_len, filters):
- return frappe.db.sql(""" (select alternative_item_code from `tabItem Alternative`
+ return frappe.db.sql(
+ """ (select alternative_item_code from `tabItem Alternative`
where item_code = %(item_code)s and alternative_item_code like %(txt)s)
union
(select item_code from `tabItem Alternative`
where alternative_item_code = %(item_code)s and item_code like %(txt)s
and two_way = 1) limit {0}, {1}
- """.format(start, page_len), {
- "item_code": filters.get('item_code'),
- "txt": '%' + txt + '%'
- })
+ """.format(
+ start, page_len
+ ),
+ {"item_code": filters.get("item_code"), "txt": "%" + txt + "%"},
+ )
diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
index 3976af4e88c..d829b2cbf39 100644
--- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
@@ -4,6 +4,7 @@
import json
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt
from erpnext.buying.doctype.purchase_order.purchase_order import (
@@ -18,130 +19,189 @@ from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
-from erpnext.tests.utils import ERPNextTestCase
-class TestItemAlternative(ERPNextTestCase):
+class TestItemAlternative(FrappeTestCase):
def setUp(self):
super().setUp()
make_items()
def test_alternative_item_for_subcontract_rm(self):
- frappe.db.set_value('Buying Settings', None,
- 'backflush_raw_materials_of_subcontract_based_on', 'BOM')
+ frappe.db.set_value(
+ "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM"
+ )
- create_stock_reconciliation(item_code='Alternate Item For A RW 1', warehouse='_Test Warehouse - _TC',
- qty=5, rate=2000)
- create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC',
- qty=5, rate=2000)
+ create_stock_reconciliation(
+ item_code="Alternate Item For A RW 1", warehouse="_Test Warehouse - _TC", qty=5, rate=2000
+ )
+ create_stock_reconciliation(
+ item_code="Test FG A RW 2", warehouse="_Test Warehouse - _TC", qty=5, rate=2000
+ )
supplier_warehouse = "Test Supplier Warehouse - _TC"
- po = create_purchase_order(item = "Test Finished Goods - A",
- is_subcontracted='Yes', qty=5, rate=3000, supplier_warehouse=supplier_warehouse)
+ po = create_purchase_order(
+ item="Test Finished Goods - A",
+ is_subcontracted="Yes",
+ qty=5,
+ rate=3000,
+ supplier_warehouse=supplier_warehouse,
+ )
- rm_item = [{"item_code": "Test Finished Goods - A", "rm_item_code": "Test FG A RW 1", "item_name":"Test FG A RW 1",
- "qty":5, "warehouse":"_Test Warehouse - _TC", "rate":2000, "amount":10000, "stock_uom":"Nos"},
- {"item_code": "Test Finished Goods - A", "rm_item_code": "Test FG A RW 2", "item_name":"Test FG A RW 2",
- "qty":5, "warehouse":"_Test Warehouse - _TC", "rate":2000, "amount":10000, "stock_uom":"Nos"}]
+ rm_item = [
+ {
+ "item_code": "Test Finished Goods - A",
+ "rm_item_code": "Test FG A RW 1",
+ "item_name": "Test FG A RW 1",
+ "qty": 5,
+ "warehouse": "_Test Warehouse - _TC",
+ "rate": 2000,
+ "amount": 10000,
+ "stock_uom": "Nos",
+ },
+ {
+ "item_code": "Test Finished Goods - A",
+ "rm_item_code": "Test FG A RW 2",
+ "item_name": "Test FG A RW 2",
+ "qty": 5,
+ "warehouse": "_Test Warehouse - _TC",
+ "rate": 2000,
+ "amount": 10000,
+ "stock_uom": "Nos",
+ },
+ ]
rm_item_string = json.dumps(rm_item)
- reserved_qty_for_sub_contract = frappe.db.get_value('Bin',
- {'item_code': 'Test FG A RW 1', 'warehouse': '_Test Warehouse - _TC'}, 'reserved_qty_for_sub_contract')
+ reserved_qty_for_sub_contract = frappe.db.get_value(
+ "Bin",
+ {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"},
+ "reserved_qty_for_sub_contract",
+ )
se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
se.to_warehouse = supplier_warehouse
se.insert()
- doc = frappe.get_doc('Stock Entry', se.name)
+ doc = frappe.get_doc("Stock Entry", se.name)
for item in doc.items:
- if item.item_code == 'Test FG A RW 1':
- item.item_code = 'Alternate Item For A RW 1'
- item.item_name = 'Alternate Item For A RW 1'
- item.description = 'Alternate Item For A RW 1'
- item.original_item = 'Test FG A RW 1'
+ if item.item_code == "Test FG A RW 1":
+ item.item_code = "Alternate Item For A RW 1"
+ item.item_name = "Alternate Item For A RW 1"
+ item.description = "Alternate Item For A RW 1"
+ item.original_item = "Test FG A RW 1"
doc.save()
doc.submit()
- after_transfer_reserved_qty_for_sub_contract = frappe.db.get_value('Bin',
- {'item_code': 'Test FG A RW 1', 'warehouse': '_Test Warehouse - _TC'}, 'reserved_qty_for_sub_contract')
+ after_transfer_reserved_qty_for_sub_contract = frappe.db.get_value(
+ "Bin",
+ {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"},
+ "reserved_qty_for_sub_contract",
+ )
- self.assertEqual(after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5))
+ self.assertEqual(
+ after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5)
+ )
pr = make_purchase_receipt(po.name)
pr.save()
- pr = frappe.get_doc('Purchase Receipt', pr.name)
+ pr = frappe.get_doc("Purchase Receipt", pr.name)
status = False
for d in pr.supplied_items:
- if d.rm_item_code == 'Alternate Item For A RW 1':
+ if d.rm_item_code == "Alternate Item For A RW 1":
status = True
self.assertEqual(status, True)
- frappe.db.set_value('Buying Settings', None,
- 'backflush_raw_materials_of_subcontract_based_on', 'Material Transferred for Subcontract')
+ frappe.db.set_value(
+ "Buying Settings",
+ None,
+ "backflush_raw_materials_of_subcontract_based_on",
+ "Material Transferred for Subcontract",
+ )
def test_alternative_item_for_production_rm(self):
- create_stock_reconciliation(item_code='Alternate Item For A RW 1',
- warehouse='_Test Warehouse - _TC',qty=5, rate=2000)
- create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC',
- qty=5, rate=2000)
- pro_order = make_wo_order_test_record(production_item='Test Finished Goods - A',
- qty=5, source_warehouse='_Test Warehouse - _TC', wip_warehouse='Test Supplier Warehouse - _TC')
+ create_stock_reconciliation(
+ item_code="Alternate Item For A RW 1", warehouse="_Test Warehouse - _TC", qty=5, rate=2000
+ )
+ create_stock_reconciliation(
+ item_code="Test FG A RW 2", warehouse="_Test Warehouse - _TC", qty=5, rate=2000
+ )
+ pro_order = make_wo_order_test_record(
+ production_item="Test Finished Goods - A",
+ qty=5,
+ source_warehouse="_Test Warehouse - _TC",
+ wip_warehouse="Test Supplier Warehouse - _TC",
+ )
- reserved_qty_for_production = frappe.db.get_value('Bin',
- {'item_code': 'Test FG A RW 1', 'warehouse': '_Test Warehouse - _TC'}, 'reserved_qty_for_production')
+ reserved_qty_for_production = frappe.db.get_value(
+ "Bin",
+ {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"},
+ "reserved_qty_for_production",
+ )
ste = frappe.get_doc(make_stock_entry(pro_order.name, "Material Transfer for Manufacture", 5))
ste.insert()
for item in ste.items:
- if item.item_code == 'Test FG A RW 1':
- item.item_code = 'Alternate Item For A RW 1'
- item.item_name = 'Alternate Item For A RW 1'
- item.description = 'Alternate Item For A RW 1'
- item.original_item = 'Test FG A RW 1'
+ if item.item_code == "Test FG A RW 1":
+ item.item_code = "Alternate Item For A RW 1"
+ item.item_name = "Alternate Item For A RW 1"
+ item.description = "Alternate Item For A RW 1"
+ item.original_item = "Test FG A RW 1"
ste.submit()
- reserved_qty_for_production_after_transfer = frappe.db.get_value('Bin',
- {'item_code': 'Test FG A RW 1', 'warehouse': '_Test Warehouse - _TC'}, 'reserved_qty_for_production')
+ reserved_qty_for_production_after_transfer = frappe.db.get_value(
+ "Bin",
+ {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"},
+ "reserved_qty_for_production",
+ )
- self.assertEqual(reserved_qty_for_production_after_transfer, flt(reserved_qty_for_production - 5))
+ self.assertEqual(
+ reserved_qty_for_production_after_transfer, flt(reserved_qty_for_production - 5)
+ )
ste1 = frappe.get_doc(make_stock_entry(pro_order.name, "Manufacture", 5))
status = False
for d in ste1.items:
- if d.item_code == 'Alternate Item For A RW 1':
+ if d.item_code == "Alternate Item For A RW 1":
status = True
self.assertEqual(status, True)
ste1.submit()
+
def make_items():
- items = ['Test Finished Goods - A', 'Test FG A RW 1', 'Test FG A RW 2', 'Alternate Item For A RW 1']
+ items = [
+ "Test Finished Goods - A",
+ "Test FG A RW 1",
+ "Test FG A RW 2",
+ "Alternate Item For A RW 1",
+ ]
for item_code in items:
- if not frappe.db.exists('Item', item_code):
+ if not frappe.db.exists("Item", item_code):
create_item(item_code)
- create_stock_reconciliation(item_code="Test FG A RW 1",
- warehouse='_Test Warehouse - _TC', qty=10, rate=2000)
+ create_stock_reconciliation(
+ item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000
+ )
- if frappe.db.exists('Item', 'Test FG A RW 1'):
- doc = frappe.get_doc('Item', 'Test FG A RW 1')
+ if frappe.db.exists("Item", "Test FG A RW 1"):
+ doc = frappe.get_doc("Item", "Test FG A RW 1")
doc.allow_alternative_item = 1
doc.save()
- if frappe.db.exists('Item', 'Test Finished Goods - A'):
- doc = frappe.get_doc('Item', 'Test Finished Goods - A')
+ if frappe.db.exists("Item", "Test Finished Goods - A"):
+ doc = frappe.get_doc("Item", "Test Finished Goods - A")
doc.is_sub_contracted_item = 1
doc.save()
- if not frappe.db.get_value('BOM',
- {'item': 'Test Finished Goods - A', 'docstatus': 1}):
- make_bom(item = 'Test Finished Goods - A', raw_materials = ['Test FG A RW 1', 'Test FG A RW 2'])
+ if not frappe.db.get_value("BOM", {"item": "Test Finished Goods - A", "docstatus": 1}):
+ make_bom(item="Test Finished Goods - A", raw_materials=["Test FG A RW 1", "Test FG A RW 2"])
- if not frappe.db.get_value('Warehouse', {'warehouse_name': 'Test Supplier Warehouse'}):
- frappe.get_doc({
- 'doctype': 'Warehouse',
- 'warehouse_name': 'Test Supplier Warehouse',
- 'company': '_Test Company'
- }).insert(ignore_permissions=True)
+ if not frappe.db.get_value("Warehouse", {"warehouse_name": "Test Supplier Warehouse"}):
+ frappe.get_doc(
+ {
+ "doctype": "Warehouse",
+ "warehouse_name": "Test Supplier Warehouse",
+ "company": "_Test Company",
+ }
+ ).insert(ignore_permissions=True)
diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py
index 5a28a9e231c..391ff06918a 100644
--- a/erpnext/stock/doctype/item_attribute/item_attribute.py
+++ b/erpnext/stock/doctype/item_attribute/item_attribute.py
@@ -14,7 +14,9 @@ from erpnext.controllers.item_variant import (
)
-class ItemAttributeIncrementError(frappe.ValidationError): pass
+class ItemAttributeIncrementError(frappe.ValidationError):
+ pass
+
class ItemAttribute(Document):
def __setup__(self):
@@ -29,11 +31,12 @@ class ItemAttribute(Document):
self.validate_exising_items()
def validate_exising_items(self):
- '''Validate that if there are existing items with attributes, they are valid'''
+ """Validate that if there are existing items with attributes, they are valid"""
attributes_list = [d.attribute_value for d in self.item_attribute_values]
# Get Item Variant Attribute details of variant items
- items = frappe.db.sql("""
+ items = frappe.db.sql(
+ """
select
i.name, iva.attribute_value as value
from
@@ -41,13 +44,18 @@ class ItemAttribute(Document):
where
iva.attribute = %(attribute)s
and iva.parent = i.name and
- i.variant_of is not null and i.variant_of != ''""", {"attribute" : self.name}, as_dict=1)
+ i.variant_of is not null and i.variant_of != ''""",
+ {"attribute": self.name},
+ as_dict=1,
+ )
for item in items:
if self.numeric_values:
validate_is_incremental(self, self.name, item.value, item.name)
else:
- validate_item_attribute_value(attributes_list, self.name, item.value, item.name, from_variant=False)
+ validate_item_attribute_value(
+ attributes_list, self.name, item.value, item.name, from_variant=False
+ )
def validate_numeric(self):
if self.numeric_values:
diff --git a/erpnext/stock/doctype/item_attribute/test_item_attribute.py b/erpnext/stock/doctype/item_attribute/test_item_attribute.py
index 0b7ca257151..a30f0e999f7 100644
--- a/erpnext/stock/doctype/item_attribute/test_item_attribute.py
+++ b/erpnext/stock/doctype/item_attribute/test_item_attribute.py
@@ -4,27 +4,30 @@
import frappe
-test_records = frappe.get_test_records('Item Attribute')
+test_records = frappe.get_test_records("Item Attribute")
+
+from frappe.tests.utils import FrappeTestCase
from erpnext.stock.doctype.item_attribute.item_attribute import ItemAttributeIncrementError
-from erpnext.tests.utils import ERPNextTestCase
-class TestItemAttribute(ERPNextTestCase):
+class TestItemAttribute(FrappeTestCase):
def setUp(self):
super().setUp()
if frappe.db.exists("Item Attribute", "_Test_Length"):
frappe.delete_doc("Item Attribute", "_Test_Length")
def test_numeric_item_attribute(self):
- item_attribute = frappe.get_doc({
- "doctype": "Item Attribute",
- "attribute_name": "_Test_Length",
- "numeric_values": 1,
- "from_range": 0.0,
- "to_range": 100.0,
- "increment": 0
- })
+ item_attribute = frappe.get_doc(
+ {
+ "doctype": "Item Attribute",
+ "attribute_name": "_Test_Length",
+ "numeric_values": 1,
+ "from_range": 0.0,
+ "to_range": 100.0,
+ "increment": 0,
+ }
+ )
self.assertRaises(ItemAttributeIncrementError, item_attribute.save)
diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.json b/erpnext/stock/doctype/item_barcode/item_barcode.json
index d89ca55a4f3..eef70c95d05 100644
--- a/erpnext/stock/doctype/item_barcode/item_barcode.json
+++ b/erpnext/stock/doctype/item_barcode/item_barcode.json
@@ -1,109 +1,42 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:barcode",
- "beta": 0,
- "creation": "2017-12-09 18:54:50.562438",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "autoname": "hash",
+ "creation": "2022-02-11 11:26:22.155183",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "barcode",
+ "barcode_type"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "barcode",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 1,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Barcode",
- "length": 0,
- "no_copy": 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,
+ "fieldname": "barcode",
+ "fieldtype": "Data",
+ "in_global_search": 1,
+ "in_list_view": 1,
+ "label": "Barcode",
+ "no_copy": 1,
"unique": 1
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "barcode_type",
- "fieldtype": "Select",
- "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": "Barcode Type",
- "length": 0,
- "no_copy": 0,
- "options": "\nEAN\nUPC-A",
- "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
+ "fieldname": "barcode_type",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Barcode Type",
+ "options": "\nEAN\nUPC-A"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-11-13 06:03:09.814357",
- "modified_by": "Administrator",
- "module": "Stock",
- "name": "Item Barcode",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2022-04-01 05:54:27.314030",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Item Barcode",
+ "naming_rule": "Random",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.py b/erpnext/stock/doctype/item_barcode/item_barcode.py
index 64c39dabde1..c2c042143ea 100644
--- a/erpnext/stock/doctype/item_barcode/item_barcode.py
+++ b/erpnext/stock/doctype/item_barcode/item_barcode.py
@@ -6,4 +6,4 @@ from frappe.model.document import Document
class ItemBarcode(Document):
- pass
+ pass
diff --git a/erpnext/stock/doctype/item_default/item_default.json b/erpnext/stock/doctype/item_default/item_default.json
index bc171604f43..042d398256a 100644
--- a/erpnext/stock/doctype/item_default/item_default.json
+++ b/erpnext/stock/doctype/item_default/item_default.json
@@ -15,6 +15,7 @@
"default_supplier",
"column_break_8",
"expense_account",
+ "default_provisional_account",
"selling_defaults",
"selling_cost_center",
"column_break_12",
@@ -101,11 +102,17 @@
"fieldtype": "Link",
"label": "Default Discount Account",
"options": "Account"
+ },
+ {
+ "fieldname": "default_provisional_account",
+ "fieldtype": "Link",
+ "label": "Default Provisional Account",
+ "options": "Account"
}
],
"istable": 1,
"links": [],
- "modified": "2021-07-13 01:26:03.860065",
+ "modified": "2022-04-10 20:18:54.148195",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Default",
@@ -114,5 +121,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py
index 469ccd8f2df..b65ba98a8bf 100644
--- a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py
+++ b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py
@@ -18,14 +18,17 @@ class ItemManufacturer(Document):
def validate_duplicate_entry(self):
if self.is_new():
filters = {
- 'item_code': self.item_code,
- 'manufacturer': self.manufacturer,
- 'manufacturer_part_no': self.manufacturer_part_no
+ "item_code": self.item_code,
+ "manufacturer": self.manufacturer,
+ "manufacturer_part_no": self.manufacturer_part_no,
}
if frappe.db.exists("Item Manufacturer", filters):
- frappe.throw(_("Duplicate entry against the item code {0} and manufacturer {1}")
- .format(self.item_code, self.manufacturer))
+ frappe.throw(
+ _("Duplicate entry against the item code {0} and manufacturer {1}").format(
+ self.item_code, self.manufacturer
+ )
+ )
def manage_default_item_manufacturer(self, delete=False):
from frappe.model.utils import set_default
@@ -37,11 +40,9 @@ class ItemManufacturer(Document):
if not self.is_default:
# if unchecked and default in Item master, clear it.
if default_manufacturer == self.manufacturer and default_part_no == self.manufacturer_part_no:
- frappe.db.set_value("Item", item.name,
- {
- "default_item_manufacturer": None,
- "default_manufacturer_part_no": None
- })
+ frappe.db.set_value(
+ "Item", item.name, {"default_item_manufacturer": None, "default_manufacturer_part_no": None}
+ )
elif self.is_default:
set_default(self, "item_code")
@@ -50,18 +51,26 @@ class ItemManufacturer(Document):
if delete:
manufacturer, manufacturer_part_no = None, None
- elif (default_manufacturer != self.manufacturer) or \
- (default_manufacturer == self.manufacturer and default_part_no != self.manufacturer_part_no):
+ elif (default_manufacturer != self.manufacturer) or (
+ default_manufacturer == self.manufacturer and default_part_no != self.manufacturer_part_no
+ ):
manufacturer = self.manufacturer
manufacturer_part_no = self.manufacturer_part_no
- frappe.db.set_value("Item", item.name,
- {
- "default_item_manufacturer": manufacturer,
- "default_manufacturer_part_no": manufacturer_part_no
- })
+ frappe.db.set_value(
+ "Item",
+ item.name,
+ {
+ "default_item_manufacturer": manufacturer,
+ "default_manufacturer_part_no": manufacturer_part_no,
+ },
+ )
+
@frappe.whitelist()
def get_item_manufacturer_part_no(item_code, manufacturer):
- return frappe.db.get_value("Item Manufacturer",
- {'item_code': item_code, 'manufacturer': manufacturer}, 'manufacturer_part_no')
+ return frappe.db.get_value(
+ "Item Manufacturer",
+ {"item_code": item_code, "manufacturer": manufacturer},
+ "manufacturer_part_no",
+ )
diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py
index 010e01a78ba..562f7b9e12f 100644
--- a/erpnext/stock/doctype/item_price/item_price.py
+++ b/erpnext/stock/doctype/item_price/item_price.py
@@ -13,7 +13,6 @@ class ItemPriceDuplicateItem(frappe.ValidationError):
class ItemPrice(Document):
-
def validate(self):
self.validate_item()
self.validate_dates()
@@ -32,22 +31,26 @@ class ItemPrice(Document):
def update_price_list_details(self):
if self.price_list:
- price_list_details = frappe.db.get_value("Price List",
- {"name": self.price_list, "enabled": 1},
- ["buying", "selling", "currency"])
+ price_list_details = frappe.db.get_value(
+ "Price List", {"name": self.price_list, "enabled": 1}, ["buying", "selling", "currency"]
+ )
if not price_list_details:
- link = frappe.utils.get_link_to_form('Price List', self.price_list)
+ link = frappe.utils.get_link_to_form("Price List", self.price_list)
frappe.throw("The price list {0} does not exist or is disabled".format(link))
self.buying, self.selling, self.currency = price_list_details
def update_item_details(self):
if self.item_code:
- self.item_name, self.item_description = frappe.db.get_value("Item", self.item_code,["item_name", "description"])
+ self.item_name, self.item_description = frappe.db.get_value(
+ "Item", self.item_code, ["item_name", "description"]
+ )
def check_duplicates(self):
- conditions = """where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s"""
+ conditions = (
+ """where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s"""
+ )
for field in [
"uom",
@@ -56,21 +59,31 @@ class ItemPrice(Document):
"packing_unit",
"customer",
"supplier",
- "batch_no"]:
+ "batch_no",
+ ]:
if self.get(field):
conditions += " and {0} = %({0})s ".format(field)
else:
conditions += "and (isnull({0}) or {0} = '')".format(field)
- price_list_rate = frappe.db.sql("""
+ price_list_rate = frappe.db.sql(
+ """
select price_list_rate
from `tabItem Price`
{conditions}
- """.format(conditions=conditions),
- self.as_dict(),)
+ """.format(
+ conditions=conditions
+ ),
+ self.as_dict(),
+ )
if price_list_rate:
- frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, Batch, UOM, Qty, and Dates."), ItemPriceDuplicateItem,)
+ frappe.throw(
+ _(
+ "Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, Batch, UOM, Qty, and Dates."
+ ),
+ ItemPriceDuplicateItem,
+ )
def before_save(self):
if self.selling:
diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py
index f81770e487d..30d933e247d 100644
--- a/erpnext/stock/doctype/item_price/test_item_price.py
+++ b/erpnext/stock/doctype/item_price/test_item_price.py
@@ -4,13 +4,13 @@
import frappe
from frappe.test_runner import make_test_records_for_doctype
+from frappe.tests.utils import FrappeTestCase
from erpnext.stock.doctype.item_price.item_price import ItemPriceDuplicateItem
from erpnext.stock.get_item_details import get_price_list_rate_for, process_args
-from erpnext.tests.utils import ERPNextTestCase
-class TestItemPrice(ERPNextTestCase):
+class TestItemPrice(FrappeTestCase):
def setUp(self):
super().setUp()
frappe.db.sql("delete from `tabItem Price`")
@@ -23,8 +23,14 @@ class TestItemPrice(ERPNextTestCase):
def test_addition_of_new_fields(self):
# Based on https://github.com/frappe/erpnext/issues/8456
test_fields_existance = [
- 'supplier', 'customer', 'uom', 'lead_time_days',
- 'packing_unit', 'valid_from', 'valid_upto', 'note'
+ "supplier",
+ "customer",
+ "uom",
+ "lead_time_days",
+ "packing_unit",
+ "valid_from",
+ "valid_upto",
+ "note",
]
doc_fields = frappe.copy_doc(test_records[1]).__dict__.keys()
@@ -45,10 +51,10 @@ class TestItemPrice(ERPNextTestCase):
args = {
"price_list": doc.price_list,
- "customer": doc.customer,
- "uom": "_Test UOM",
- "transaction_date": '2017-04-18',
- "qty": 10
+ "customer": doc.customer,
+ "uom": "_Test UOM",
+ "transaction_date": "2017-04-18",
+ "qty": 10,
}
price = get_price_list_rate_for(process_args(args), doc.item_code)
@@ -61,13 +67,12 @@ class TestItemPrice(ERPNextTestCase):
"price_list": doc.price_list,
"customer": doc.customer,
"uom": "_Test UOM",
- "transaction_date": '2017-04-18',
+ "transaction_date": "2017-04-18",
}
price = get_price_list_rate_for(args, doc.item_code)
self.assertEqual(price, None)
-
def test_prices_at_date(self):
# Check correct price at first date
doc = frappe.copy_doc(test_records[2])
@@ -76,35 +81,35 @@ class TestItemPrice(ERPNextTestCase):
"price_list": doc.price_list,
"customer": "_Test Customer",
"uom": "_Test UOM",
- "transaction_date": '2017-04-18',
- "qty": 7
+ "transaction_date": "2017-04-18",
+ "qty": 7,
}
price = get_price_list_rate_for(args, doc.item_code)
self.assertEqual(price, 20)
def test_prices_at_invalid_date(self):
- #Check correct price at invalid date
+ # Check correct price at invalid date
doc = frappe.copy_doc(test_records[3])
args = {
"price_list": doc.price_list,
"qty": 7,
"uom": "_Test UOM",
- "transaction_date": "01-15-2019"
+ "transaction_date": "01-15-2019",
}
price = get_price_list_rate_for(args, doc.item_code)
self.assertEqual(price, None)
def test_prices_outside_of_date(self):
- #Check correct price when outside of the date
+ # Check correct price when outside of the date
doc = frappe.copy_doc(test_records[4])
args = {
"price_list": doc.price_list,
- "customer": "_Test Customer",
- "uom": "_Test UOM",
+ "customer": "_Test Customer",
+ "uom": "_Test UOM",
"transaction_date": "2017-04-25",
"qty": 7,
}
@@ -113,7 +118,7 @@ class TestItemPrice(ERPNextTestCase):
self.assertEqual(price, None)
def test_lowest_price_when_no_date_provided(self):
- #Check lowest price when no date provided
+ # Check lowest price when no date provided
doc = frappe.copy_doc(test_records[1])
args = {
@@ -125,7 +130,6 @@ class TestItemPrice(ERPNextTestCase):
price = get_price_list_rate_for(args, doc.item_code)
self.assertEqual(price, 10)
-
def test_invalid_item(self):
doc = frappe.copy_doc(test_records[1])
# Enter invalid item code
@@ -150,8 +154,8 @@ class TestItemPrice(ERPNextTestCase):
args = {
"price_list": doc.price_list,
"uom": "_Test UOM",
- "transaction_date": '2017-04-18',
- "qty": 7
+ "transaction_date": "2017-04-18",
+ "qty": 7,
}
price = get_price_list_rate_for(args, doc.item_code)
@@ -159,4 +163,5 @@ class TestItemPrice(ERPNextTestCase):
self.assertEqual(price, 21)
-test_records = frappe.get_test_records('Item Price')
+
+test_records = frappe.get_test_records("Item Price")
diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py
index 62bf842be83..49f311684c2 100644
--- a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py
+++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py
@@ -8,29 +8,46 @@ from frappe.model.document import Document
class ItemVariantSettings(Document):
- invalid_fields_for_copy_fields_in_variants = ['barcodes']
+ invalid_fields_for_copy_fields_in_variants = ["barcodes"]
def set_default_fields(self):
self.fields = []
- fields = frappe.get_meta('Item').fields
- exclude_fields = {"naming_series", "item_code", "item_name", "published_in_website",
- "standard_rate", "opening_stock", "image", "description",
- "variant_of", "valuation_rate", "description", "barcodes",
- "has_variants", "attributes"}
+ fields = frappe.get_meta("Item").fields
+ exclude_fields = {
+ "naming_series",
+ "item_code",
+ "item_name",
+ "published_in_website",
+ "standard_rate",
+ "opening_stock",
+ "image",
+ "description",
+ "variant_of",
+ "valuation_rate",
+ "description",
+ "barcodes",
+ "has_variants",
+ "attributes",
+ }
for d in fields:
- if not d.no_copy and d.fieldname not in exclude_fields and \
- d.fieldtype not in ['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only']:
- self.append('fields', {
- 'field_name': d.fieldname
- })
+ if (
+ not d.no_copy
+ and d.fieldname not in exclude_fields
+ and d.fieldtype not in ["HTML", "Section Break", "Column Break", "Button", "Read Only"]
+ ):
+ self.append("fields", {"field_name": d.fieldname})
def remove_invalid_fields_for_copy_fields_in_variants(self):
- fields = [row for row in self.fields if row.field_name not in self.invalid_fields_for_copy_fields_in_variants]
+ fields = [
+ row
+ for row in self.fields
+ if row.field_name not in self.invalid_fields_for_copy_fields_in_variants
+ ]
self.fields = fields
self.save()
def validate(self):
for d in self.fields:
if d.field_name in self.invalid_fields_for_copy_fields_in_variants:
- frappe.throw(_('Cannot set the field {0} for copying in variants').format(d.field_name))
+ frappe.throw(_("Cannot set the field {0} for copying in variants").format(d.field_name))
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index 7aff95d1e81..b3af309359a 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -19,13 +19,19 @@ class LandedCostVoucher(Document):
self.set("items", [])
for pr in self.get("purchase_receipts"):
if pr.receipt_document_type and pr.receipt_document:
- pr_items = frappe.db.sql("""select pr_item.item_code, pr_item.description,
+ pr_items = frappe.db.sql(
+ """select pr_item.item_code, pr_item.description,
pr_item.qty, pr_item.base_rate, pr_item.base_amount, pr_item.name,
pr_item.cost_center, pr_item.is_fixed_asset
from `tab{doctype} Item` pr_item where parent = %s
and exists(select name from tabItem
where name = pr_item.item_code and (is_stock_item = 1 or is_fixed_asset=1))
- """.format(doctype=pr.receipt_document_type), pr.receipt_document, as_dict=True)
+ """.format(
+ doctype=pr.receipt_document_type
+ ),
+ pr.receipt_document,
+ as_dict=True,
+ )
for d in pr_items:
item = self.append("items")
@@ -33,8 +39,7 @@ class LandedCostVoucher(Document):
item.description = d.description
item.qty = d.qty
item.rate = d.base_rate
- item.cost_center = d.cost_center or \
- erpnext.get_default_cost_center(self.company)
+ item.cost_center = d.cost_center or erpnext.get_default_cost_center(self.company)
item.amount = d.base_amount
item.receipt_document_type = pr.receipt_document_type
item.receipt_document = pr.receipt_document
@@ -52,26 +57,30 @@ class LandedCostVoucher(Document):
self.set_applicable_charges_on_item()
self.validate_applicable_charges_for_item()
-
def check_mandatory(self):
if not self.get("purchase_receipts"):
frappe.throw(_("Please enter Receipt Document"))
-
def validate_receipt_documents(self):
receipt_documents = []
for d in self.get("purchase_receipts"):
docstatus = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus")
if docstatus != 1:
- msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted"
+ msg = (
+ f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted"
+ )
frappe.throw(_(msg), title=_("Invalid Document"))
if d.receipt_document_type == "Purchase Invoice":
update_stock = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "update_stock")
if not update_stock:
- msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format(d.idx, frappe.bold(d.receipt_document))
- msg += " " + _("Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled.")
+ msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format(
+ d.idx, frappe.bold(d.receipt_document)
+ )
+ msg += " " + _(
+ "Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled."
+ )
frappe.throw(msg, title=_("Incorrect Invoice"))
receipt_documents.append(d.receipt_document)
@@ -81,52 +90,64 @@ class LandedCostVoucher(Document):
frappe.throw(_("Item must be added using 'Get Items from Purchase Receipts' button"))
elif item.receipt_document not in receipt_documents:
- frappe.throw(_("Item Row {0}: {1} {2} does not exist in above '{1}' table")
- .format(item.idx, item.receipt_document_type, item.receipt_document))
+ frappe.throw(
+ _("Item Row {0}: {1} {2} does not exist in above '{1}' table").format(
+ item.idx, item.receipt_document_type, item.receipt_document
+ )
+ )
if not item.cost_center:
- frappe.throw(_("Row {0}: Cost center is required for an item {1}")
- .format(item.idx, item.item_code))
+ frappe.throw(
+ _("Row {0}: Cost center is required for an item {1}").format(item.idx, item.item_code)
+ )
def set_total_taxes_and_charges(self):
self.total_taxes_and_charges = sum(flt(d.base_amount) for d in self.get("taxes"))
def set_applicable_charges_on_item(self):
- if self.get('taxes') and self.distribute_charges_based_on != 'Distribute Manually':
+ if self.get("taxes") and self.distribute_charges_based_on != "Distribute Manually":
total_item_cost = 0.0
total_charges = 0.0
item_count = 0
based_on_field = frappe.scrub(self.distribute_charges_based_on)
- for item in self.get('items'):
+ for item in self.get("items"):
total_item_cost += item.get(based_on_field)
- for item in self.get('items'):
- item.applicable_charges = flt(flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)),
- item.precision('applicable_charges'))
+ for item in self.get("items"):
+ item.applicable_charges = flt(
+ flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)),
+ item.precision("applicable_charges"),
+ )
total_charges += item.applicable_charges
item_count += 1
if total_charges != self.total_taxes_and_charges:
diff = self.total_taxes_and_charges - total_charges
- self.get('items')[item_count - 1].applicable_charges += diff
+ self.get("items")[item_count - 1].applicable_charges += diff
def validate_applicable_charges_for_item(self):
based_on = self.distribute_charges_based_on.lower()
- if based_on != 'distribute manually':
+ if based_on != "distribute manually":
total = sum(flt(d.get(based_on)) for d in self.get("items"))
else:
# consider for proportion while distributing manually
- total = sum(flt(d.get('applicable_charges')) for d in self.get("items"))
+ total = sum(flt(d.get("applicable_charges")) for d in self.get("items"))
if not total:
- frappe.throw(_("Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'").format(based_on))
+ frappe.throw(
+ _(
+ "Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'"
+ ).format(based_on)
+ )
total_applicable_charges = sum(flt(d.applicable_charges) for d in self.get("items"))
- precision = get_field_precision(frappe.get_meta("Landed Cost Item").get_field("applicable_charges"),
- currency=frappe.get_cached_value('Company', self.company, "default_currency"))
+ precision = get_field_precision(
+ frappe.get_meta("Landed Cost Item").get_field("applicable_charges"),
+ currency=frappe.get_cached_value("Company", self.company, "default_currency"),
+ )
diff = flt(self.total_taxes_and_charges) - flt(total_applicable_charges)
diff = flt(diff, precision)
@@ -134,7 +155,11 @@ class LandedCostVoucher(Document):
if abs(diff) < (2.0 / (10**precision)):
self.items[-1].applicable_charges += diff
else:
- frappe.throw(_("Total Applicable Charges in Purchase Receipt Items table must be same as Total Taxes and Charges"))
+ frappe.throw(
+ _(
+ "Total Applicable Charges in Purchase Receipt Items table must be same as Total Taxes and Charges"
+ )
+ )
def on_submit(self):
self.update_landed_cost()
@@ -177,25 +202,41 @@ class LandedCostVoucher(Document):
doc.repost_future_sle_and_gle()
def validate_asset_qty_and_status(self, receipt_document_type, receipt_document):
- for item in self.get('items'):
+ for item in self.get("items"):
if item.is_fixed_asset:
- receipt_document_type = 'purchase_invoice' if item.receipt_document_type == 'Purchase Invoice' \
- else 'purchase_receipt'
- docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document,
- 'item_code': item.item_code }, fields=['name', 'docstatus'])
+ receipt_document_type = (
+ "purchase_invoice" if item.receipt_document_type == "Purchase Invoice" else "purchase_receipt"
+ )
+ docs = frappe.db.get_all(
+ "Asset",
+ filters={receipt_document_type: item.receipt_document, "item_code": item.item_code},
+ fields=["name", "docstatus"],
+ )
if not docs or len(docs) != item.qty:
- frappe.throw(_('There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document.').format(
- item.receipt_document, item.qty))
+ frappe.throw(
+ _(
+ "There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document."
+ ).format(item.receipt_document, item.qty)
+ )
if docs:
for d in docs:
if d.docstatus == 1:
- frappe.throw(_('{2} {0} has submitted Assets. Remove Item {1} from table to continue.').format(
- item.receipt_document, item.item_code, item.receipt_document_type))
+ frappe.throw(
+ _(
+ "{2} {0} has submitted Assets. Remove Item {1} from table to continue."
+ ).format(
+ item.receipt_document, item.item_code, item.receipt_document_type
+ )
+ )
def update_rate_in_serial_no_for_non_asset_items(self, receipt_document):
for item in receipt_document.get("items"):
if not item.is_fixed_asset and item.serial_no:
serial_nos = get_serial_nos(item.serial_no)
if serial_nos:
- frappe.db.sql("update `tabSerial No` set purchase_rate=%s where name in ({0})"
- .format(", ".join(["%s"]*len(serial_nos))), tuple([item.valuation_rate] + serial_nos))
+ frappe.db.sql(
+ "update `tabSerial No` set purchase_rate=%s where name in ({0})".format(
+ ", ".join(["%s"] * len(serial_nos))
+ ),
+ tuple([item.valuation_rate] + serial_nos),
+ )
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index 1ea0596d333..1af99534516 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -2,8 +2,8 @@
# License: GNU General Public License v3. See license.txt
-
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_to_date, flt, now
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
@@ -15,41 +15,56 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_gl_entries,
make_purchase_receipt,
)
-from erpnext.tests.utils import ERPNextTestCase
-class TestLandedCostVoucher(ERPNextTestCase):
+class TestLandedCostVoucher(FrappeTestCase):
def test_landed_cost_voucher(self):
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1",
- get_multiple_items = True, get_taxes_and_charges = True)
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ supplier_warehouse="Work in Progress - TCP1",
+ get_multiple_items=True,
+ get_taxes_and_charges=True,
+ )
- last_sle = frappe.db.get_value("Stock Ledger Entry", {
+ last_sle = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
"voucher_type": pr.doctype,
"voucher_no": pr.name,
"item_code": "_Test Item",
"warehouse": "Stores - TCP1",
"is_cancelled": 0,
},
- fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
+ fieldname=["qty_after_transaction", "stock_value"],
+ as_dict=1,
+ )
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
- pr_lc_value = frappe.db.get_value("Purchase Receipt Item", {"parent": pr.name}, "landed_cost_voucher_amount")
+ pr_lc_value = frappe.db.get_value(
+ "Purchase Receipt Item", {"parent": pr.name}, "landed_cost_voucher_amount"
+ )
self.assertEqual(pr_lc_value, 25.0)
- last_sle_after_landed_cost = frappe.db.get_value("Stock Ledger Entry", {
+ last_sle_after_landed_cost = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
"voucher_type": pr.doctype,
"voucher_no": pr.name,
"item_code": "_Test Item",
"warehouse": "Stores - TCP1",
"is_cancelled": 0,
},
- fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
+ fieldname=["qty_after_transaction", "stock_value"],
+ as_dict=1,
+ )
- self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction)
+ self.assertEqual(
+ last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction
+ )
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 25.0)
# assert after submit
@@ -57,24 +72,20 @@ class TestLandedCostVoucher(ERPNextTestCase):
# Mess up cancelled SLE modified timestamp to check
# if they aren't effective in any business logic.
- frappe.db.set_value("Stock Ledger Entry",
- {
- "is_cancelled": 1,
- "voucher_type": pr.doctype,
- "voucher_no": pr.name
- },
- "is_cancelled", 1,
- modified=add_to_date(now(), hours=1, as_datetime=True, as_string=True)
+ frappe.db.set_value(
+ "Stock Ledger Entry",
+ {"is_cancelled": 1, "voucher_type": pr.doctype, "voucher_no": pr.name},
+ "is_cancelled",
+ 1,
+ modified=add_to_date(now(), hours=1, as_datetime=True, as_string=True),
)
items, warehouses = pr.get_items_and_warehouses()
- update_gl_entries_after(pr.posting_date, pr.posting_time,
- warehouses, items, company=pr.company)
+ update_gl_entries_after(pr.posting_date, pr.posting_time, warehouses, items, company=pr.company)
# reassert after reposting
self.assertPurchaseReceiptLCVGLEntries(pr)
-
def assertPurchaseReceiptLCVGLEntries(self, pr):
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
@@ -90,54 +101,74 @@ class TestLandedCostVoucher(ERPNextTestCase):
"Stock Received But Not Billed - TCP1": [0.0, 500.0],
"Expenses Included In Valuation - TCP1": [0.0, 50.0],
"_Test Account Customs Duty - TCP1": [0.0, 150],
- "_Test Account Shipping Charges - TCP1": [0.0, 100.00]
+ "_Test Account Shipping Charges - TCP1": [0.0, 100.00],
}
else:
expected_values = {
stock_in_hand_account: [400.0, 0.0],
fixed_asset_account: [400.0, 0.0],
"Stock Received But Not Billed - TCP1": [0.0, 500.0],
- "Expenses Included In Valuation - TCP1": [0.0, 300.0]
+ "Expenses Included In Valuation - TCP1": [0.0, 300.0],
}
for gle in gl_entries:
- if not gle.get('is_cancelled'):
- self.assertEqual(expected_values[gle.account][0], gle.debit, msg=f"incorrect debit for {gle.account}")
- self.assertEqual(expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}")
-
+ if not gle.get("is_cancelled"):
+ self.assertEqual(
+ expected_values[gle.account][0], gle.debit, msg=f"incorrect debit for {gle.account}"
+ )
+ self.assertEqual(
+ expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}"
+ )
def test_landed_cost_voucher_against_purchase_invoice(self):
- pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(),
- posting_time=frappe.utils.nowtime(), cash_bank_account="Cash - TCP1",
- company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1",
- warehouse= "Stores - TCP1", cost_center = "Main - TCP1",
- expense_account ="_Test Account Cost for Goods Sold - TCP1")
+ pi = make_purchase_invoice(
+ update_stock=1,
+ posting_date=frappe.utils.nowdate(),
+ posting_time=frappe.utils.nowtime(),
+ cash_bank_account="Cash - TCP1",
+ company="_Test Company with perpetual inventory",
+ supplier_warehouse="Work In Progress - TCP1",
+ warehouse="Stores - TCP1",
+ cost_center="Main - TCP1",
+ expense_account="_Test Account Cost for Goods Sold - TCP1",
+ )
- last_sle = frappe.db.get_value("Stock Ledger Entry", {
+ last_sle = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
"voucher_type": pi.doctype,
"voucher_no": pi.name,
"item_code": "_Test Item",
- "warehouse": "Stores - TCP1"
+ "warehouse": "Stores - TCP1",
},
- fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
+ fieldname=["qty_after_transaction", "stock_value"],
+ as_dict=1,
+ )
create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company)
- pi_lc_value = frappe.db.get_value("Purchase Invoice Item", {"parent": pi.name},
- "landed_cost_voucher_amount")
+ pi_lc_value = frappe.db.get_value(
+ "Purchase Invoice Item", {"parent": pi.name}, "landed_cost_voucher_amount"
+ )
self.assertEqual(pi_lc_value, 50.0)
- last_sle_after_landed_cost = frappe.db.get_value("Stock Ledger Entry", {
+ last_sle_after_landed_cost = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
"voucher_type": pi.doctype,
"voucher_no": pi.name,
"item_code": "_Test Item",
- "warehouse": "Stores - TCP1"
+ "warehouse": "Stores - TCP1",
},
- fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
+ fieldname=["qty_after_transaction", "stock_value"],
+ as_dict=1,
+ )
- self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction)
+ self.assertEqual(
+ last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction
+ )
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
@@ -149,20 +180,26 @@ class TestLandedCostVoucher(ERPNextTestCase):
expected_values = {
stock_in_hand_account: [300.0, 0.0],
"Creditors - TCP1": [0.0, 250.0],
- "Expenses Included In Valuation - TCP1": [0.0, 50.0]
+ "Expenses Included In Valuation - TCP1": [0.0, 50.0],
}
for gle in gl_entries:
- if not gle.get('is_cancelled'):
+ if not gle.get("is_cancelled"):
self.assertEqual(expected_values[gle.account][0], gle.debit)
self.assertEqual(expected_values[gle.account][1], gle.credit)
-
def test_landed_cost_voucher_for_serialized_item(self):
- frappe.db.sql("delete from `tabSerial No` where name in ('SN001', 'SN002', 'SN003', 'SN004', 'SN005')")
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1",
- supplier_warehouse = "Work in Progress - TCP1", get_multiple_items = True,
- get_taxes_and_charges = True, do_not_submit = True)
+ frappe.db.sql(
+ "delete from `tabSerial No` where name in ('SN001', 'SN002', 'SN003', 'SN004', 'SN005')"
+ )
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ supplier_warehouse="Work in Progress - TCP1",
+ get_multiple_items=True,
+ get_taxes_and_charges=True,
+ do_not_submit=True,
+ )
pr.items[0].item_code = "_Test Serialized Item"
pr.items[0].serial_no = "SN001\nSN002\nSN003\nSN004\nSN005"
@@ -172,8 +209,7 @@ class TestLandedCostVoucher(ERPNextTestCase):
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
- serial_no = frappe.db.get_value("Serial No", "SN001",
- ["warehouse", "purchase_rate"], as_dict=1)
+ serial_no = frappe.db.get_value("Serial No", "SN001", ["warehouse", "purchase_rate"], as_dict=1)
self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0)
self.assertEqual(serial_no.warehouse, "Stores - TCP1")
@@ -183,60 +219,82 @@ class TestLandedCostVoucher(ERPNextTestCase):
landed costs, this should be allowed for serial nos too.
Case:
- - receipt a serial no @ X rate
- - delivery the serial no @ X rate
- - add LCV to receipt X + Y
- - LCV should be successful
- - delivery should reflect X+Y valuation.
+ - receipt a serial no @ X rate
+ - delivery the serial no @ X rate
+ - add LCV to receipt X + Y
+ - LCV should be successful
+ - delivery should reflect X+Y valuation.
"""
serial_no = "LCV_TEST_SR_NO"
item_code = "_Test Serialized Item"
warehouse = "Stores - TCP1"
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
- warehouse=warehouse, qty=1, rate=200,
- item_code=item_code, serial_no=serial_no)
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse=warehouse,
+ qty=1,
+ rate=200,
+ item_code=item_code,
+ serial_no=serial_no,
+ )
serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate")
# deliver it before creating LCV
- dn = create_delivery_note(item_code=item_code,
- company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
- serial_no=serial_no, qty=1, rate=500,
- cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1")
+ dn = create_delivery_note(
+ item_code=item_code,
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ serial_no=serial_no,
+ qty=1,
+ rate=500,
+ cost_center="Main - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ )
charges = 10
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges)
new_purchase_rate = serial_no_rate + charges
- serial_no = frappe.db.get_value("Serial No", serial_no,
- ["warehouse", "purchase_rate"], as_dict=1)
+ serial_no = frappe.db.get_value(
+ "Serial No", serial_no, ["warehouse", "purchase_rate"], as_dict=1
+ )
self.assertEqual(serial_no.purchase_rate, new_purchase_rate)
- stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
- filters={
- "voucher_no": dn.name,
- "voucher_type": dn.doctype,
- "is_cancelled": 0 # LCV cancels with same name.
- },
- fieldname="stock_value_difference")
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ filters={
+ "voucher_no": dn.name,
+ "voucher_type": dn.doctype,
+ "is_cancelled": 0, # LCV cancels with same name.
+ },
+ fieldname="stock_value_difference",
+ )
# reposting should update the purchase rate in future delivery
self.assertEqual(stock_value_difference, -new_purchase_rate)
- def test_landed_cost_voucher_for_odd_numbers (self):
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True)
+ def test_landed_cost_voucher_for_odd_numbers(self):
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ supplier_warehouse="Work in Progress - TCP1",
+ do_not_save=True,
+ )
pr.items[0].cost_center = "Main - TCP1"
for x in range(2):
- pr.append("items", {
- "item_code": "_Test Item",
- "warehouse": "Stores - TCP1",
- "cost_center": "Main - TCP1",
- "qty": 5,
- "rate": 50
- })
+ pr.append(
+ "items",
+ {
+ "item_code": "_Test Item",
+ "warehouse": "Stores - TCP1",
+ "cost_center": "Main - TCP1",
+ "qty": 5,
+ "rate": 50,
+ },
+ )
pr.submit()
lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22)
@@ -245,37 +303,50 @@ class TestLandedCostVoucher(ERPNextTestCase):
self.assertEqual(flt(lcv.items[2].applicable_charges, 2), 41.08)
def test_multiple_landed_cost_voucher_against_pr(self):
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1",
- supplier_warehouse = "Stores - TCP1", do_not_save=True)
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ supplier_warehouse="Stores - TCP1",
+ do_not_save=True,
+ )
- pr.append("items", {
- "item_code": "_Test Item",
- "warehouse": "Stores - TCP1",
- "cost_center": "Main - TCP1",
- "qty": 5,
- "rate": 100
- })
+ pr.append(
+ "items",
+ {
+ "item_code": "_Test Item",
+ "warehouse": "Stores - TCP1",
+ "cost_center": "Main - TCP1",
+ "qty": 5,
+ "rate": 100,
+ },
+ )
pr.submit()
- lcv1 = make_landed_cost_voucher(company = pr.company, receipt_document_type = 'Purchase Receipt',
- receipt_document=pr.name, charges=100, do_not_save=True)
+ lcv1 = make_landed_cost_voucher(
+ company=pr.company,
+ receipt_document_type="Purchase Receipt",
+ receipt_document=pr.name,
+ charges=100,
+ do_not_save=True,
+ )
lcv1.insert()
- lcv1.set('items', [
- lcv1.get('items')[0]
- ])
+ lcv1.set("items", [lcv1.get("items")[0]])
distribute_landed_cost_on_items(lcv1)
lcv1.submit()
- lcv2 = make_landed_cost_voucher(company = pr.company, receipt_document_type = 'Purchase Receipt',
- receipt_document=pr.name, charges=100, do_not_save=True)
+ lcv2 = make_landed_cost_voucher(
+ company=pr.company,
+ receipt_document_type="Purchase Receipt",
+ receipt_document=pr.name,
+ charges=100,
+ do_not_save=True,
+ )
lcv2.insert()
- lcv2.set('items', [
- lcv2.get('items')[1]
- ])
+ lcv2.set("items", [lcv2.get("items")[1]])
distribute_landed_cost_on_items(lcv2)
lcv2.submit()
@@ -294,22 +365,31 @@ class TestLandedCostVoucher(ERPNextTestCase):
save_new_records(test_records)
## Create USD Shipping charges_account
- usd_shipping = create_account(account_name="Shipping Charges USD",
- parent_account="Duties and Taxes - TCP1", company="_Test Company with perpetual inventory",
- account_currency="USD")
+ usd_shipping = create_account(
+ account_name="Shipping Charges USD",
+ parent_account="Duties and Taxes - TCP1",
+ company="_Test Company with perpetual inventory",
+ account_currency="USD",
+ )
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1",
- supplier_warehouse = "Stores - TCP1")
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ supplier_warehouse="Stores - TCP1",
+ )
pr.submit()
- lcv = make_landed_cost_voucher(company = pr.company, receipt_document_type = "Purchase Receipt",
- receipt_document=pr.name, charges=100, do_not_save=True)
+ lcv = make_landed_cost_voucher(
+ company=pr.company,
+ receipt_document_type="Purchase Receipt",
+ receipt_document=pr.name,
+ charges=100,
+ do_not_save=True,
+ )
- lcv.append("taxes", {
- "description": "Shipping Charges",
- "expense_account": usd_shipping,
- "amount": 10
- })
+ lcv.append(
+ "taxes", {"description": "Shipping Charges", "expense_account": usd_shipping, "amount": 10}
+ )
lcv.save()
lcv.submit()
@@ -319,12 +399,18 @@ class TestLandedCostVoucher(ERPNextTestCase):
self.assertEqual(lcv.total_taxes_and_charges, 729)
self.assertEqual(pr.items[0].landed_cost_voucher_amount, 729)
- gl_entries = frappe.get_all("GL Entry", fields=["account", "credit", "credit_in_account_currency"],
- filters={"voucher_no": pr.name, "account": ("in", ["Shipping Charges USD - TCP1", "Expenses Included In Valuation - TCP1"])})
+ gl_entries = frappe.get_all(
+ "GL Entry",
+ fields=["account", "credit", "credit_in_account_currency"],
+ filters={
+ "voucher_no": pr.name,
+ "account": ("in", ["Shipping Charges USD - TCP1", "Expenses Included In Valuation - TCP1"]),
+ },
+ )
expected_gl_entries = {
"Shipping Charges USD - TCP1": [629, 10],
- "Expenses Included In Valuation - TCP1": [100, 100]
+ "Expenses Included In Valuation - TCP1": [100, 100],
}
for entry in gl_entries:
@@ -334,7 +420,9 @@ class TestLandedCostVoucher(ERPNextTestCase):
def test_asset_lcv(self):
"Check if LCV for an Asset updates the Assets Gross Purchase Amount correctly."
- frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC")
+ frappe.db.set_value(
+ "Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC"
+ )
if not frappe.db.exists("Asset Category", "Computers"):
create_asset_category()
@@ -345,15 +433,16 @@ class TestLandedCostVoucher(ERPNextTestCase):
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=50000)
# check if draft asset was created
- assets = frappe.db.get_all('Asset', filters={'purchase_receipt': pr.name})
+ assets = frappe.db.get_all("Asset", filters={"purchase_receipt": pr.name})
self.assertEqual(len(assets), 1)
lcv = make_landed_cost_voucher(
- company = pr.company,
- receipt_document_type = "Purchase Receipt",
+ company=pr.company,
+ receipt_document_type="Purchase Receipt",
receipt_document=pr.name,
charges=80,
- expense_account="Expenses Included In Valuation - _TC")
+ expense_account="Expenses Included In Valuation - _TC",
+ )
lcv.save()
lcv.submit()
@@ -365,27 +454,38 @@ class TestLandedCostVoucher(ERPNextTestCase):
lcv.cancel()
pr.cancel()
-def make_landed_cost_voucher(** args):
+
+def make_landed_cost_voucher(**args):
args = frappe._dict(args)
ref_doc = frappe.get_doc(args.receipt_document_type, args.receipt_document)
- lcv = frappe.new_doc('Landed Cost Voucher')
- lcv.company = args.company or '_Test Company'
- lcv.distribute_charges_based_on = 'Amount'
+ lcv = frappe.new_doc("Landed Cost Voucher")
+ lcv.company = args.company or "_Test Company"
+ lcv.distribute_charges_based_on = "Amount"
- lcv.set('purchase_receipts', [{
- "receipt_document_type": args.receipt_document_type,
- "receipt_document": args.receipt_document,
- "supplier": ref_doc.supplier,
- "posting_date": ref_doc.posting_date,
- "grand_total": ref_doc.grand_total
- }])
+ lcv.set(
+ "purchase_receipts",
+ [
+ {
+ "receipt_document_type": args.receipt_document_type,
+ "receipt_document": args.receipt_document,
+ "supplier": ref_doc.supplier,
+ "posting_date": ref_doc.posting_date,
+ "grand_total": ref_doc.grand_total,
+ }
+ ],
+ )
- lcv.set("taxes", [{
- "description": "Shipping Charges",
- "expense_account": args.expense_account or "Expenses Included In Valuation - TCP1",
- "amount": args.charges
- }])
+ lcv.set(
+ "taxes",
+ [
+ {
+ "description": "Shipping Charges",
+ "expense_account": args.expense_account or "Expenses Included In Valuation - TCP1",
+ "amount": args.charges,
+ }
+ ],
+ )
if not args.do_not_save:
lcv.insert()
@@ -400,21 +500,31 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company,
lcv = frappe.new_doc("Landed Cost Voucher")
lcv.company = company
- lcv.distribute_charges_based_on = 'Amount'
+ lcv.distribute_charges_based_on = "Amount"
- lcv.set("purchase_receipts", [{
- "receipt_document_type": receipt_document_type,
- "receipt_document": receipt_document,
- "supplier": ref_doc.supplier,
- "posting_date": ref_doc.posting_date,
- "grand_total": ref_doc.base_grand_total
- }])
+ lcv.set(
+ "purchase_receipts",
+ [
+ {
+ "receipt_document_type": receipt_document_type,
+ "receipt_document": receipt_document,
+ "supplier": ref_doc.supplier,
+ "posting_date": ref_doc.posting_date,
+ "grand_total": ref_doc.base_grand_total,
+ }
+ ],
+ )
- lcv.set("taxes", [{
- "description": "Insurance Charges",
- "expense_account": "Expenses Included In Valuation - TCP1",
- "amount": charges
- }])
+ lcv.set(
+ "taxes",
+ [
+ {
+ "description": "Insurance Charges",
+ "expense_account": "Expenses Included In Valuation - TCP1",
+ "amount": charges,
+ }
+ ],
+ )
lcv.insert()
@@ -424,6 +534,7 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company,
return lcv
+
def distribute_landed_cost_on_items(lcv):
based_on = lcv.distribute_charges_based_on.lower()
total = sum(flt(d.get(based_on)) for d in lcv.get("items"))
@@ -432,4 +543,5 @@ def distribute_landed_cost_on_items(lcv):
item.applicable_charges = flt(item.get(based_on)) * flt(lcv.total_taxes_and_charges) / flt(total)
item.applicable_charges = flt(item.applicable_charges, lcv.precision("applicable_charges", item))
-test_records = frappe.get_test_records('Landed Cost Voucher')
+
+test_records = frappe.get_test_records("Landed Cost Voucher")
diff --git a/erpnext/stock/doctype/manufacturer/test_manufacturer.py b/erpnext/stock/doctype/manufacturer/test_manufacturer.py
index 66323478c83..e176b28b85a 100644
--- a/erpnext/stock/doctype/manufacturer/test_manufacturer.py
+++ b/erpnext/stock/doctype/manufacturer/test_manufacturer.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Manufacturer')
+
class TestManufacturer(unittest.TestCase):
pass
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 087a7883e09..cc64b5caa52 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -214,6 +214,7 @@ frappe.ui.form.on('Material Request', {
material_request_type: frm.doc.material_request_type,
plc_conversion_rate: 1,
rate: item.rate,
+ uom: item.uom,
conversion_factor: item.conversion_factor
},
overwrite_warehouse: overwrite_warehouse
@@ -392,6 +393,7 @@ frappe.ui.form.on("Material Request Item", {
item_code: function(frm, doctype, name) {
const item = locals[doctype][name];
item.rate = 0;
+ item.uom = '';
set_schedule_date(frm);
frm.events.get_item_data(frm, item, true);
},
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 50d43171f80..415c45cf1fe 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -19,9 +19,8 @@ from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.stock.stock_balance import get_indented_qty, update_bin_qty
-form_grid_templates = {
- "items": "templates/form_grid/material_request_grid.html"
-}
+form_grid_templates = {"items": "templates/form_grid/material_request_grid.html"}
+
class MaterialRequest(BuyingController):
def get_feed(self):
@@ -31,8 +30,8 @@ class MaterialRequest(BuyingController):
pass
def validate_qty_against_so(self):
- so_items = {} # Format --> {'SO/00001': {'Item/001': 120, 'Item/002': 24}}
- for d in self.get('items'):
+ so_items = {} # Format --> {'SO/00001': {'Item/001': 120, 'Item/002': 24}}
+ for d in self.get("items"):
if d.sales_order:
if not d.sales_order in so_items:
so_items[d.sales_order] = {d.item_code: flt(d.qty)}
@@ -44,24 +43,34 @@ class MaterialRequest(BuyingController):
for so_no in so_items.keys():
for item in so_items[so_no].keys():
- already_indented = frappe.db.sql("""select sum(qty)
+ already_indented = frappe.db.sql(
+ """select sum(qty)
from `tabMaterial Request Item`
where item_code = %s and sales_order = %s and
- docstatus = 1 and parent != %s""", (item, so_no, self.name))
+ docstatus = 1 and parent != %s""",
+ (item, so_no, self.name),
+ )
already_indented = already_indented and flt(already_indented[0][0]) or 0
- actual_so_qty = frappe.db.sql("""select sum(stock_qty) from `tabSales Order Item`
- where parent = %s and item_code = %s and docstatus = 1""", (so_no, item))
+ actual_so_qty = frappe.db.sql(
+ """select sum(stock_qty) from `tabSales Order Item`
+ where parent = %s and item_code = %s and docstatus = 1""",
+ (so_no, item),
+ )
actual_so_qty = actual_so_qty and flt(actual_so_qty[0][0]) or 0
if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty):
- frappe.throw(_("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format(actual_so_qty - already_indented, item, so_no))
+ frappe.throw(
+ _("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format(
+ actual_so_qty - already_indented, item, so_no
+ )
+ )
def validate(self):
super(MaterialRequest, self).validate()
self.validate_schedule_date()
- self.check_for_on_hold_or_closed_status('Sales Order', 'sales_order')
+ self.check_for_on_hold_or_closed_status("Sales Order", "sales_order")
self.validate_uom_is_integer("uom", "qty")
self.validate_material_request_type()
@@ -69,9 +78,22 @@ class MaterialRequest(BuyingController):
self.status = "Draft"
from erpnext.controllers.status_updater import validate_status
- validate_status(self.status,
- ["Draft", "Submitted", "Stopped", "Cancelled", "Pending",
- "Partially Ordered", "Ordered", "Issued", "Transferred", "Received"])
+
+ validate_status(
+ self.status,
+ [
+ "Draft",
+ "Submitted",
+ "Stopped",
+ "Cancelled",
+ "Pending",
+ "Partially Ordered",
+ "Ordered",
+ "Issued",
+ "Transferred",
+ "Received",
+ ],
+ )
validate_for_items(self)
@@ -83,23 +105,26 @@ class MaterialRequest(BuyingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
+ def before_update_after_submit(self):
+ self.validate_schedule_date()
+
def validate_material_request_type(self):
- """ Validate fields in accordance with selected type """
+ """Validate fields in accordance with selected type"""
if self.material_request_type != "Customer Provided":
self.customer = None
def set_title(self):
- '''Set title as comma separated list of items'''
+ """Set title as comma separated list of items"""
if not self.title:
- items = ', '.join([d.item_name for d in self.items][:3])
- self.title = _('{0} Request for {1}').format(self.material_request_type, items)[:100]
+ items = ", ".join([d.item_name for d in self.items][:3])
+ self.title = _("{0} Request for {1}").format(self.material_request_type, items)[:100]
def on_submit(self):
# frappe.db.set(self, 'status', 'Submitted')
self.update_requested_qty()
self.update_requested_qty_in_production_plan()
- if self.material_request_type == 'Purchase':
+ if self.material_request_type == "Purchase":
self.validate_budget()
def before_save(self):
@@ -112,13 +137,15 @@ class MaterialRequest(BuyingController):
# if MRQ is already closed, no point saving the document
check_on_hold_or_closed_status(self.doctype, self.name)
- self.set_status(update=True, status='Cancelled')
+ self.set_status(update=True, status="Cancelled")
def check_modified_date(self):
- mod_db = frappe.db.sql("""select modified from `tabMaterial Request` where name = %s""",
- self.name)
- date_diff = frappe.db.sql("""select TIMEDIFF('%s', '%s')"""
- % (mod_db[0][0], cstr(self.modified)))
+ mod_db = frappe.db.sql(
+ """select modified from `tabMaterial Request` where name = %s""", self.name
+ )
+ date_diff = frappe.db.sql(
+ """select TIMEDIFF('%s', '%s')""" % (mod_db[0][0], cstr(self.modified))
+ )
if date_diff and date_diff[0][0]:
frappe.throw(_("{0} {1} has been modified. Please refresh.").format(_(self.doctype), self.name))
@@ -134,22 +161,24 @@ class MaterialRequest(BuyingController):
validates that `status` is acceptable for the present controller status
and throws an Exception if otherwise.
"""
- if self.status and self.status == 'Cancelled':
+ if self.status and self.status == "Cancelled":
# cancelled documents cannot change
if status != self.status:
frappe.throw(
- _("{0} {1} is cancelled so the action cannot be completed").
- format(_(self.doctype), self.name),
- frappe.InvalidStatusError
+ _("{0} {1} is cancelled so the action cannot be completed").format(
+ _(self.doctype), self.name
+ ),
+ frappe.InvalidStatusError,
)
- elif self.status and self.status == 'Draft':
+ elif self.status and self.status == "Draft":
# draft document to pending only
- if status != 'Pending':
+ if status != "Pending":
frappe.throw(
- _("{0} {1} has not been submitted so the action cannot be completed").
- format(_(self.doctype), self.name),
- frappe.InvalidStatusError
+ _("{0} {1} has not been submitted so the action cannot be completed").format(
+ _(self.doctype), self.name
+ ),
+ frappe.InvalidStatusError,
)
def on_cancel(self):
@@ -166,67 +195,90 @@ class MaterialRequest(BuyingController):
for d in self.get("items"):
if d.name in mr_items:
if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
- d.ordered_qty = flt(frappe.db.sql("""select sum(transfer_qty)
+ d.ordered_qty = flt(
+ frappe.db.sql(
+ """select sum(transfer_qty)
from `tabStock Entry Detail` where material_request = %s
and material_request_item = %s and docstatus = 1""",
- (self.name, d.name))[0][0])
- mr_qty_allowance = frappe.db.get_single_value('Stock Settings', 'mr_qty_allowance')
+ (self.name, d.name),
+ )[0][0]
+ )
+ mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
if mr_qty_allowance:
- allowed_qty = d.qty + (d.qty * (mr_qty_allowance/100))
+ 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))
+ 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} \
- cannot be greater than requested quantity {2} for Item {3}").format(d.ordered_qty, d.parent, d.qty, d.item_code))
+ 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)
+ )
elif self.material_request_type == "Manufacture":
- d.ordered_qty = flt(frappe.db.sql("""select sum(qty)
+ d.ordered_qty = flt(
+ frappe.db.sql(
+ """select sum(qty)
from `tabWork Order` where material_request = %s
and material_request_item = %s and docstatus = 1""",
- (self.name, d.name))[0][0])
+ (self.name, d.name),
+ )[0][0]
+ )
frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty)
- self._update_percent_field({
- "target_dt": "Material Request Item",
- "target_parent_dt": self.doctype,
- "target_parent_field": "per_ordered",
- "target_ref_field": "stock_qty",
- "target_field": "ordered_qty",
- "name": self.name,
- }, update_modified)
+ self._update_percent_field(
+ {
+ "target_dt": "Material Request Item",
+ "target_parent_dt": self.doctype,
+ "target_parent_field": "per_ordered",
+ "target_ref_field": "stock_qty",
+ "target_field": "ordered_qty",
+ "name": self.name,
+ },
+ update_modified,
+ )
def update_requested_qty(self, mr_item_rows=None):
"""update requested qty (before ordered_qty is updated)"""
item_wh_list = []
for d in self.get("items"):
- if (not mr_item_rows or d.name in mr_item_rows) and [d.item_code, d.warehouse] not in item_wh_list \
- and d.warehouse and frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 :
+ if (
+ (not mr_item_rows or d.name in mr_item_rows)
+ and [d.item_code, d.warehouse] not in item_wh_list
+ and d.warehouse
+ and frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1
+ ):
item_wh_list.append([d.item_code, d.warehouse])
for item_code, warehouse in item_wh_list:
- update_bin_qty(item_code, warehouse, {
- "indented_qty": get_indented_qty(item_code, warehouse)
- })
+ update_bin_qty(item_code, warehouse, {"indented_qty": get_indented_qty(item_code, warehouse)})
def update_requested_qty_in_production_plan(self):
production_plans = []
- for d in self.get('items'):
+ for d in self.get("items"):
if d.production_plan and d.material_request_plan_item:
qty = d.qty if self.docstatus == 1 else 0
- frappe.db.set_value('Material Request Plan Item',
- d.material_request_plan_item, 'requested_qty', qty)
+ frappe.db.set_value(
+ "Material Request Plan Item", d.material_request_plan_item, "requested_qty", qty
+ )
if d.production_plan not in production_plans:
production_plans.append(d.production_plan)
for production_plan in production_plans:
- doc = frappe.get_doc('Production Plan', production_plan)
+ doc = frappe.get_doc("Production Plan", production_plan)
doc.set_status()
- doc.db_set('status', doc.status)
+ doc.db_set("status", doc.status)
+
def update_completed_and_requested_qty(stock_entry, method):
if stock_entry.doctype == "Stock Entry":
@@ -241,43 +293,55 @@ def update_completed_and_requested_qty(stock_entry, method):
mr_obj = frappe.get_doc("Material Request", mr)
if mr_obj.status in ["Stopped", "Cancelled"]:
- frappe.throw(_("{0} {1} is cancelled or stopped").format(_("Material Request"), mr),
- frappe.InvalidStatusError)
+ frappe.throw(
+ _("{0} {1} is cancelled or stopped").format(_("Material Request"), mr),
+ frappe.InvalidStatusError,
+ )
mr_obj.update_completed_qty(mr_item_rows)
mr_obj.update_requested_qty(mr_item_rows)
+
def set_missing_values(source, target_doc):
- if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate(nowdate()):
+ if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate(
+ nowdate()
+ ):
target_doc.schedule_date = None
target_doc.run_method("set_missing_values")
target_doc.run_method("calculate_taxes_and_totals")
+
def update_item(obj, target, source_parent):
target.conversion_factor = obj.conversion_factor
- target.qty = flt(flt(obj.stock_qty) - flt(obj.ordered_qty))/ target.conversion_factor
- target.stock_qty = (target.qty * target.conversion_factor)
+ target.qty = flt(flt(obj.stock_qty) - flt(obj.ordered_qty)) / target.conversion_factor
+ target.stock_qty = target.qty * target.conversion_factor
if getdate(target.schedule_date) < getdate(nowdate()):
target.schedule_date = None
+
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
+
list_context = get_list_context(context)
- list_context.update({
- 'show_sidebar': True,
- 'show_search': True,
- 'no_breadcrumbs': True,
- 'title': _('Material Request'),
- })
+ list_context.update(
+ {
+ "show_sidebar": True,
+ "show_search": True,
+ "no_breadcrumbs": True,
+ "title": _("Material Request"),
+ }
+ )
return list_context
+
@frappe.whitelist()
def update_status(name, status):
- material_request = frappe.get_doc('Material Request', name)
- material_request.check_permission('write')
+ material_request = frappe.get_doc("Material Request", name)
+ material_request.check_permission("write")
material_request.update_status(status)
+
@frappe.whitelist()
def make_purchase_order(source_name, target_doc=None, args=None):
if args is None:
@@ -290,7 +354,7 @@ def make_purchase_order(source_name, target_doc=None, args=None):
# items only for given default supplier
supplier_items = []
for d in target_doc.items:
- default_supplier = get_item_defaults(d.item_code, target_doc.company).get('default_supplier')
+ default_supplier = get_item_defaults(d.item_code, target_doc.company).get("default_supplier")
if frappe.flags.args.default_supplier == default_supplier:
supplier_items.append(d)
target_doc.items = supplier_items
@@ -298,58 +362,65 @@ def make_purchase_order(source_name, target_doc=None, args=None):
set_missing_values(source, target_doc)
def select_item(d):
- filtered_items = args.get('filtered_children', [])
+ filtered_items = args.get("filtered_children", [])
child_filter = d.name in filtered_items if filtered_items else True
return d.ordered_qty < d.stock_qty and child_filter
- doclist = get_mapped_doc("Material Request", source_name, {
- "Material Request": {
- "doctype": "Purchase Order",
- "validation": {
- "docstatus": ["=", 1],
- "material_request_type": ["=", "Purchase"]
- }
+ doclist = get_mapped_doc(
+ "Material Request",
+ source_name,
+ {
+ "Material Request": {
+ "doctype": "Purchase Order",
+ "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]},
+ },
+ "Material Request Item": {
+ "doctype": "Purchase Order Item",
+ "field_map": [
+ ["name", "material_request_item"],
+ ["parent", "material_request"],
+ ["uom", "stock_uom"],
+ ["uom", "uom"],
+ ["sales_order", "sales_order"],
+ ["sales_order_item", "sales_order_item"],
+ ],
+ "postprocess": update_item,
+ "condition": select_item,
+ },
},
- "Material Request Item": {
- "doctype": "Purchase Order Item",
- "field_map": [
- ["name", "material_request_item"],
- ["parent", "material_request"],
- ["uom", "stock_uom"],
- ["uom", "uom"],
- ["sales_order", "sales_order"],
- ["sales_order_item", "sales_order_item"]
- ],
- "postprocess": update_item,
- "condition": select_item
- }
- }, target_doc, postprocess)
+ target_doc,
+ postprocess,
+ )
return doclist
+
@frappe.whitelist()
def make_request_for_quotation(source_name, target_doc=None):
- doclist = get_mapped_doc("Material Request", source_name, {
- "Material Request": {
- "doctype": "Request for Quotation",
- "validation": {
- "docstatus": ["=", 1],
- "material_request_type": ["=", "Purchase"]
- }
+ doclist = get_mapped_doc(
+ "Material Request",
+ source_name,
+ {
+ "Material Request": {
+ "doctype": "Request for Quotation",
+ "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]},
+ },
+ "Material Request Item": {
+ "doctype": "Request for Quotation Item",
+ "field_map": [
+ ["name", "material_request_item"],
+ ["parent", "material_request"],
+ ["uom", "uom"],
+ ],
+ },
},
- "Material Request Item": {
- "doctype": "Request for Quotation Item",
- "field_map": [
- ["name", "material_request_item"],
- ["parent", "material_request"],
- ["uom", "uom"]
- ]
- }
- }, target_doc)
+ target_doc,
+ )
return doclist
+
@frappe.whitelist()
def make_purchase_order_based_on_supplier(source_name, target_doc=None, args=None):
mr = source_name
@@ -360,43 +431,59 @@ def make_purchase_order_based_on_supplier(source_name, target_doc=None, args=Non
target_doc.supplier = args.get("supplier")
if getdate(target_doc.schedule_date) < getdate(nowdate()):
target_doc.schedule_date = None
- target_doc.set("items", [d for d in target_doc.get("items")
- if d.get("item_code") in supplier_items and d.get("qty") > 0])
+ target_doc.set(
+ "items",
+ [
+ d for d in target_doc.get("items") if d.get("item_code") in supplier_items and d.get("qty") > 0
+ ],
+ )
set_missing_values(source, target_doc)
- target_doc = get_mapped_doc("Material Request", mr, {
- "Material Request": {
- "doctype": "Purchase Order",
+ target_doc = get_mapped_doc(
+ "Material Request",
+ mr,
+ {
+ "Material Request": {
+ "doctype": "Purchase Order",
+ },
+ "Material Request Item": {
+ "doctype": "Purchase Order Item",
+ "field_map": [
+ ["name", "material_request_item"],
+ ["parent", "material_request"],
+ ["uom", "stock_uom"],
+ ["uom", "uom"],
+ ],
+ "postprocess": update_item,
+ "condition": lambda doc: doc.ordered_qty < doc.qty,
+ },
},
- "Material Request Item": {
- "doctype": "Purchase Order Item",
- "field_map": [
- ["name", "material_request_item"],
- ["parent", "material_request"],
- ["uom", "stock_uom"],
- ["uom", "uom"]
- ],
- "postprocess": update_item,
- "condition": lambda doc: doc.ordered_qty < doc.qty
- }
- }, target_doc, postprocess)
+ target_doc,
+ postprocess,
+ )
return target_doc
+
@frappe.whitelist()
def get_items_based_on_default_supplier(supplier):
- supplier_items = [d.parent for d in frappe.db.get_all("Item Default",
- {"default_supplier": supplier, "parenttype": "Item"}, 'parent')]
+ supplier_items = [
+ d.parent
+ for d in frappe.db.get_all(
+ "Item Default", {"default_supplier": supplier, "parenttype": "Item"}, "parent"
+ )
+ ]
return supplier_items
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, page_len, filters):
conditions = ""
if txt:
- conditions += "and mr.name like '%%"+txt+"%%' "
+ conditions += "and mr.name like '%%" + txt + "%%' "
if filters.get("transaction_date"):
date = filters.get("transaction_date")[1]
@@ -408,7 +495,8 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa
if not supplier_items:
frappe.throw(_("{0} is not the default supplier for any items.").format(supplier))
- material_requests = frappe.db.sql("""select distinct mr.name, transaction_date,company
+ material_requests = frappe.db.sql(
+ """select distinct mr.name, transaction_date,company
from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
where mr.name = mr_item.parent
and mr_item.item_code in ({0})
@@ -419,12 +507,16 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa
and mr.company = '{1}'
{2}
order by mr_item.item_code ASC
- limit {3} offset {4} """ \
- .format(', '.join(['%s']*len(supplier_items)), filters.get("company"), conditions, page_len, start),
- tuple(supplier_items), as_dict=1)
+ limit {3} offset {4} """.format(
+ ", ".join(["%s"] * len(supplier_items)), filters.get("company"), conditions, page_len, start
+ ),
+ tuple(supplier_items),
+ as_dict=1,
+ )
return material_requests
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_default_supplier_query(doctype, txt, searchfield, start, page_len, filters):
@@ -433,47 +525,63 @@ def get_default_supplier_query(doctype, txt, searchfield, start, page_len, filte
for d in doc.items:
item_list.append(d.item_code)
- return frappe.db.sql("""select default_supplier
+ return frappe.db.sql(
+ """select default_supplier
from `tabItem Default`
where parent in ({0}) and
default_supplier IS NOT NULL
- """.format(', '.join(['%s']*len(item_list))),tuple(item_list))
+ """.format(
+ ", ".join(["%s"] * len(item_list))
+ ),
+ tuple(item_list),
+ )
+
@frappe.whitelist()
def make_supplier_quotation(source_name, target_doc=None):
def postprocess(source, target_doc):
set_missing_values(source, target_doc)
- doclist = get_mapped_doc("Material Request", source_name, {
- "Material Request": {
- "doctype": "Supplier Quotation",
- "validation": {
- "docstatus": ["=", 1],
- "material_request_type": ["=", "Purchase"]
- }
+ doclist = get_mapped_doc(
+ "Material Request",
+ source_name,
+ {
+ "Material Request": {
+ "doctype": "Supplier Quotation",
+ "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]},
+ },
+ "Material Request Item": {
+ "doctype": "Supplier Quotation Item",
+ "field_map": {
+ "name": "material_request_item",
+ "parent": "material_request",
+ "sales_order": "sales_order",
+ },
+ },
},
- "Material Request Item": {
- "doctype": "Supplier Quotation Item",
- "field_map": {
- "name": "material_request_item",
- "parent": "material_request",
- "sales_order": "sales_order"
- }
- }
- }, target_doc, postprocess)
+ target_doc,
+ postprocess,
+ )
return doclist
+
@frappe.whitelist()
def make_stock_entry(source_name, target_doc=None):
def update_item(obj, target, source_parent):
- qty = flt(flt(obj.stock_qty) - flt(obj.ordered_qty))/ target.conversion_factor \
- if flt(obj.stock_qty) > flt(obj.ordered_qty) else 0
+ qty = (
+ flt(flt(obj.stock_qty) - flt(obj.ordered_qty)) / target.conversion_factor
+ if flt(obj.stock_qty) > flt(obj.ordered_qty)
+ else 0
+ )
target.qty = qty
target.transfer_qty = qty * obj.conversion_factor
target.conversion_factor = obj.conversion_factor
- if source_parent.material_request_type == "Material Transfer" or source_parent.material_request_type == "Customer Provided":
+ if (
+ source_parent.material_request_type == "Material Transfer"
+ or source_parent.material_request_type == "Customer Provided"
+ ):
target.t_warehouse = obj.warehouse
else:
target.s_warehouse = obj.warehouse
@@ -487,7 +595,7 @@ def make_stock_entry(source_name, target_doc=None):
def set_missing_values(source, target):
target.purpose = source.material_request_type
if source.job_card:
- target.purpose = 'Material Transfer for Manufacture'
+ target.purpose = "Material Transfer for Manufacture"
if source.material_request_type == "Customer Provided":
target.purpose = "Material Receipt"
@@ -496,101 +604,119 @@ def make_stock_entry(source_name, target_doc=None):
target.set_stock_entry_type()
target.set_job_card_data()
- doclist = get_mapped_doc("Material Request", source_name, {
- "Material Request": {
- "doctype": "Stock Entry",
- "validation": {
- "docstatus": ["=", 1],
- "material_request_type": ["in", ["Material Transfer", "Material Issue", "Customer Provided"]]
- }
- },
- "Material Request Item": {
- "doctype": "Stock Entry Detail",
- "field_map": {
- "name": "material_request_item",
- "parent": "material_request",
- "uom": "stock_uom",
- "job_card_item": "job_card_item"
+ doclist = get_mapped_doc(
+ "Material Request",
+ source_name,
+ {
+ "Material Request": {
+ "doctype": "Stock Entry",
+ "validation": {
+ "docstatus": ["=", 1],
+ "material_request_type": ["in", ["Material Transfer", "Material Issue", "Customer Provided"]],
+ },
},
- "postprocess": update_item,
- "condition": lambda doc: doc.ordered_qty < doc.stock_qty
- }
- }, target_doc, set_missing_values)
+ "Material Request Item": {
+ "doctype": "Stock Entry Detail",
+ "field_map": {
+ "name": "material_request_item",
+ "parent": "material_request",
+ "uom": "stock_uom",
+ "job_card_item": "job_card_item",
+ },
+ "postprocess": update_item,
+ "condition": lambda doc: doc.ordered_qty < doc.stock_qty,
+ },
+ },
+ target_doc,
+ set_missing_values,
+ )
return doclist
+
@frappe.whitelist()
def raise_work_orders(material_request):
- mr= frappe.get_doc("Material Request", material_request)
- errors =[]
+ mr = frappe.get_doc("Material Request", material_request)
+ errors = []
work_orders = []
- default_wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse")
+ default_wip_warehouse = frappe.db.get_single_value(
+ "Manufacturing Settings", "default_wip_warehouse"
+ )
for d in mr.items:
if (d.stock_qty - d.ordered_qty) > 0:
if frappe.db.exists("BOM", {"item": d.item_code, "is_default": 1}):
wo_order = frappe.new_doc("Work Order")
- wo_order.update({
- "production_item": d.item_code,
- "qty": d.stock_qty - d.ordered_qty,
- "fg_warehouse": d.warehouse,
- "wip_warehouse": default_wip_warehouse,
- "description": d.description,
- "stock_uom": d.stock_uom,
- "expected_delivery_date": d.schedule_date,
- "sales_order": d.sales_order,
- "sales_order_item": d.get("sales_order_item"),
- "bom_no": get_item_details(d.item_code).bom_no,
- "material_request": mr.name,
- "material_request_item": d.name,
- "planned_start_date": mr.transaction_date,
- "company": mr.company
- })
+ wo_order.update(
+ {
+ "production_item": d.item_code,
+ "qty": d.stock_qty - d.ordered_qty,
+ "fg_warehouse": d.warehouse,
+ "wip_warehouse": default_wip_warehouse,
+ "description": d.description,
+ "stock_uom": d.stock_uom,
+ "expected_delivery_date": d.schedule_date,
+ "sales_order": d.sales_order,
+ "sales_order_item": d.get("sales_order_item"),
+ "bom_no": get_item_details(d.item_code).bom_no,
+ "material_request": mr.name,
+ "material_request_item": d.name,
+ "planned_start_date": mr.transaction_date,
+ "company": mr.company,
+ }
+ )
wo_order.set_work_order_operations()
wo_order.save()
work_orders.append(wo_order.name)
else:
- errors.append(_("Row {0}: Bill of Materials not found for the Item {1}")
- .format(d.idx, get_link_to_form("Item", d.item_code)))
+ errors.append(
+ _("Row {0}: Bill of Materials not found for the Item {1}").format(
+ d.idx, get_link_to_form("Item", d.item_code)
+ )
+ )
if work_orders:
work_orders_list = [get_link_to_form("Work Order", d) for d in work_orders]
if len(work_orders) > 1:
- msgprint(_("The following {0} were created: {1}")
- .format(frappe.bold(_("Work Orders")), ' ' + ', '.join(work_orders_list)))
+ msgprint(
+ _("The following {0} were created: {1}").format(
+ frappe.bold(_("Work Orders")), " " + ", ".join(work_orders_list)
+ )
+ )
else:
- msgprint(_("The {0} {1} created sucessfully")
- .format(frappe.bold(_("Work Order")), work_orders_list[0]))
+ msgprint(
+ _("The {0} {1} created sucessfully").format(frappe.bold(_("Work Order")), work_orders_list[0])
+ )
if errors:
- frappe.throw(_("Work Order cannot be created for following reason: {0}")
- .format(new_line_sep(errors)))
+ frappe.throw(
+ _("Work Order cannot be created for following reason: {0}").format(new_line_sep(errors))
+ )
return work_orders
+
@frappe.whitelist()
def create_pick_list(source_name, target_doc=None):
- doc = get_mapped_doc('Material Request', source_name, {
- 'Material Request': {
- 'doctype': 'Pick List',
- 'field_map': {
- 'material_request_type': 'purpose'
+ doc = get_mapped_doc(
+ "Material Request",
+ source_name,
+ {
+ "Material Request": {
+ "doctype": "Pick List",
+ "field_map": {"material_request_type": "purpose"},
+ "validation": {"docstatus": ["=", 1]},
},
- 'validation': {
- 'docstatus': ['=', 1]
- }
- },
- 'Material Request Item': {
- 'doctype': 'Pick List Item',
- 'field_map': {
- 'name': 'material_request_item',
- 'qty': 'stock_qty'
+ "Material Request Item": {
+ "doctype": "Pick List Item",
+ "field_map": {"name": "material_request_item", "qty": "stock_qty"},
},
},
- }, target_doc)
+ target_doc,
+ )
doc.set_item_locations()
diff --git a/erpnext/stock/doctype/material_request/material_request_dashboard.py b/erpnext/stock/doctype/material_request/material_request_dashboard.py
index 9133859b24f..b073e6a22ee 100644
--- a/erpnext/stock/doctype/material_request/material_request_dashboard.py
+++ b/erpnext/stock/doctype/material_request/material_request_dashboard.py
@@ -1,23 +1,15 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'material_request',
- 'transactions': [
+ "fieldname": "material_request",
+ "transactions": [
{
- 'label': _('Reference'),
- 'items': ['Request for Quotation', 'Supplier Quotation', 'Purchase Order']
+ "label": _("Reference"),
+ "items": ["Request for Quotation", "Supplier Quotation", "Purchase Order"],
},
- {
- 'label': _('Stock'),
- 'items': ['Stock Entry', 'Purchase Receipt', 'Pick List']
-
- },
- {
- 'label': _('Manufacturing'),
- 'items': ['Work Order']
- }
- ]
+ {"label": _("Stock"), "items": ["Stock Entry", "Purchase Receipt", "Pick List"]},
+ {"label": _("Manufacturing"), "items": ["Work Order"]},
+ ],
}
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index 383b0ae806e..78af1532ea8 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -6,6 +6,7 @@
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, today
from erpnext.stock.doctype.item.test_item import create_item
@@ -15,15 +16,13 @@ from erpnext.stock.doctype.material_request.material_request import (
make_supplier_quotation,
raise_work_orders,
)
-from erpnext.tests.utils import ERPNextTestCase
-class TestMaterialRequest(ERPNextTestCase):
+class TestMaterialRequest(FrappeTestCase):
def test_make_purchase_order(self):
mr = frappe.copy_doc(test_records[0]).insert()
- self.assertRaises(frappe.ValidationError, make_purchase_order,
- mr.name)
+ self.assertRaises(frappe.ValidationError, make_purchase_order, mr.name)
mr = frappe.get_doc("Material Request", mr.name)
mr.submit()
@@ -44,7 +43,6 @@ class TestMaterialRequest(ERPNextTestCase):
self.assertEqual(sq.doctype, "Supplier Quotation")
self.assertEqual(len(sq.get("items")), len(mr.get("items")))
-
def test_make_stock_entry(self):
mr = frappe.copy_doc(test_records[0]).insert()
@@ -58,42 +56,44 @@ class TestMaterialRequest(ERPNextTestCase):
self.assertEqual(se.doctype, "Stock Entry")
self.assertEqual(len(se.get("items")), len(mr.get("items")))
- def _insert_stock_entry(self, qty1, qty2, warehouse = None ):
- se = frappe.get_doc({
- "company": "_Test Company",
- "doctype": "Stock Entry",
- "posting_date": "2013-03-01",
- "posting_time": "00:00:00",
- "purpose": "Material Receipt",
- "items": [
- {
- "conversion_factor": 1.0,
- "doctype": "Stock Entry Detail",
- "item_code": "_Test Item Home Desktop 100",
- "parentfield": "items",
- "basic_rate": 100,
- "qty": qty1,
- "stock_uom": "_Test UOM 1",
- "transfer_qty": qty1,
- "uom": "_Test UOM 1",
- "t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
- "cost_center": "_Test Cost Center - _TC"
- },
- {
- "conversion_factor": 1.0,
- "doctype": "Stock Entry Detail",
- "item_code": "_Test Item Home Desktop 200",
- "parentfield": "items",
- "basic_rate": 100,
- "qty": qty2,
- "stock_uom": "_Test UOM 1",
- "transfer_qty": qty2,
- "uom": "_Test UOM 1",
- "t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
- "cost_center": "_Test Cost Center - _TC"
- }
- ]
- })
+ def _insert_stock_entry(self, qty1, qty2, warehouse=None):
+ se = frappe.get_doc(
+ {
+ "company": "_Test Company",
+ "doctype": "Stock Entry",
+ "posting_date": "2013-03-01",
+ "posting_time": "00:00:00",
+ "purpose": "Material Receipt",
+ "items": [
+ {
+ "conversion_factor": 1.0,
+ "doctype": "Stock Entry Detail",
+ "item_code": "_Test Item Home Desktop 100",
+ "parentfield": "items",
+ "basic_rate": 100,
+ "qty": qty1,
+ "stock_uom": "_Test UOM 1",
+ "transfer_qty": qty1,
+ "uom": "_Test UOM 1",
+ "t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ {
+ "conversion_factor": 1.0,
+ "doctype": "Stock Entry Detail",
+ "item_code": "_Test Item Home Desktop 200",
+ "parentfield": "items",
+ "basic_rate": 100,
+ "qty": qty2,
+ "stock_uom": "_Test UOM 1",
+ "transfer_qty": qty2,
+ "uom": "_Test UOM 1",
+ "t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ ],
+ }
+ )
se.set_stock_entry_type()
se.insert()
@@ -106,19 +106,19 @@ class TestMaterialRequest(ERPNextTestCase):
mr.load_from_db()
mr.cancel()
- self.assertRaises(frappe.ValidationError, mr.update_status, 'Stopped')
+ self.assertRaises(frappe.ValidationError, mr.update_status, "Stopped")
def test_mr_changes_from_stopped_to_pending_after_reopen(self):
mr = frappe.copy_doc(test_records[0])
mr.insert()
mr.submit()
- self.assertEqual('Pending', mr.status)
+ self.assertEqual("Pending", mr.status)
- mr.update_status('Stopped')
- self.assertEqual('Stopped', mr.status)
+ mr.update_status("Stopped")
+ self.assertEqual("Stopped", mr.status)
- mr.update_status('Submitted')
- self.assertEqual('Pending', mr.status)
+ mr.update_status("Submitted")
+ self.assertEqual("Pending", mr.status)
def test_cannot_submit_cancelled_mr(self):
mr = frappe.copy_doc(test_records[0])
@@ -133,7 +133,7 @@ class TestMaterialRequest(ERPNextTestCase):
mr.insert()
mr.submit()
mr.cancel()
- self.assertEqual('Cancelled', mr.status)
+ self.assertEqual("Cancelled", mr.status)
def test_cannot_change_cancelled_mr(self):
mr = frappe.copy_doc(test_records[0])
@@ -142,12 +142,12 @@ class TestMaterialRequest(ERPNextTestCase):
mr.load_from_db()
mr.cancel()
- self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Draft')
- self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Stopped')
- self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Ordered')
- self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Issued')
- self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Transferred')
- self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Pending')
+ self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Draft")
+ self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Stopped")
+ self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Ordered")
+ self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Issued")
+ self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Transferred")
+ self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Pending")
def test_cannot_submit_deleted_material_request(self):
mr = frappe.copy_doc(test_records[0])
@@ -169,9 +169,9 @@ class TestMaterialRequest(ERPNextTestCase):
mr.submit()
mr.load_from_db()
- mr.update_status('Stopped')
- mr.update_status('Submitted')
- self.assertEqual(mr.status, 'Pending')
+ mr.update_status("Stopped")
+ mr.update_status("Submitted")
+ self.assertEqual(mr.status, "Pending")
def test_pending_mr_changes_to_stopped_after_stop(self):
mr = frappe.copy_doc(test_records[0])
@@ -179,17 +179,21 @@ class TestMaterialRequest(ERPNextTestCase):
mr.submit()
mr.load_from_db()
- mr.update_status('Stopped')
- self.assertEqual(mr.status, 'Stopped')
+ mr.update_status("Stopped")
+ self.assertEqual(mr.status, "Stopped")
def test_cannot_stop_unsubmitted_mr(self):
mr = frappe.copy_doc(test_records[0])
mr.insert()
- self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Stopped')
+ self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Stopped")
def test_completed_qty_for_purchase(self):
- 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_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"
+ )
# submit material request of type Purchase
mr = frappe.copy_doc(test_records[0])
@@ -206,19 +210,18 @@ class TestMaterialRequest(ERPNextTestCase):
po_doc.get("items")[0].schedule_date = "2013-07-09"
po_doc.get("items")[1].schedule_date = "2013-07-09"
-
# check for stopped status of Material Request
po = frappe.copy_doc(po_doc)
po.insert()
po.load_from_db()
- mr.update_status('Stopped')
+ mr.update_status("Stopped")
self.assertRaises(frappe.InvalidStatusError, po.submit)
frappe.db.set(po, "docstatus", 1)
self.assertRaises(frappe.InvalidStatusError, po.cancel)
# resubmit and check for per complete
mr.load_from_db()
- mr.update_status('Submitted')
+ mr.update_status("Submitted")
po = frappe.copy_doc(po_doc)
po.insert()
po.submit()
@@ -229,8 +232,12 @@ class TestMaterialRequest(ERPNextTestCase):
self.assertEqual(mr.get("items")[0].ordered_qty, 27.0)
self.assertEqual(mr.get("items")[1].ordered_qty, 1.5)
- current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
- current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
+ current_requested_qty_item1 = self._get_requested_qty(
+ "_Test Item Home Desktop 100", "_Test Warehouse - _TC"
+ )
+ current_requested_qty_item2 = self._get_requested_qty(
+ "_Test Item Home Desktop 200", "_Test Warehouse - _TC"
+ )
self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 27.0)
self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 1.5)
@@ -242,15 +249,23 @@ class TestMaterialRequest(ERPNextTestCase):
self.assertEqual(mr.get("items")[0].ordered_qty, 0)
self.assertEqual(mr.get("items")[1].ordered_qty, 0)
- current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
- current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
+ current_requested_qty_item1 = self._get_requested_qty(
+ "_Test Item Home Desktop 100", "_Test Warehouse - _TC"
+ )
+ current_requested_qty_item2 = self._get_requested_qty(
+ "_Test Item Home Desktop 200", "_Test Warehouse - _TC"
+ )
self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
def test_completed_qty_for_transfer(self):
- 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_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"
+ )
# submit material request of type Purchase
mr = frappe.copy_doc(test_records[0])
@@ -264,31 +279,31 @@ class TestMaterialRequest(ERPNextTestCase):
self.assertEqual(mr.get("items")[0].ordered_qty, 0)
self.assertEqual(mr.get("items")[1].ordered_qty, 0)
- current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
- current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
+ current_requested_qty_item1 = self._get_requested_qty(
+ "_Test Item Home Desktop 100", "_Test Warehouse - _TC"
+ )
+ current_requested_qty_item2 = self._get_requested_qty(
+ "_Test Item Home Desktop 200", "_Test Warehouse - _TC"
+ )
self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
# map a stock entry
se_doc = make_stock_entry(mr.name)
- se_doc.update({
- "posting_date": "2013-03-01",
- "posting_time": "01:00",
- "fiscal_year": "_Test Fiscal Year 2013",
- })
- se_doc.get("items")[0].update({
- "qty": 27.0,
- "transfer_qty": 27.0,
- "s_warehouse": "_Test Warehouse 1 - _TC",
- "basic_rate": 1.0
- })
- se_doc.get("items")[1].update({
- "qty": 1.5,
- "transfer_qty": 1.5,
- "s_warehouse": "_Test Warehouse 1 - _TC",
- "basic_rate": 1.0
- })
+ se_doc.update(
+ {
+ "posting_date": "2013-03-01",
+ "posting_time": "01:00",
+ "fiscal_year": "_Test Fiscal Year 2013",
+ }
+ )
+ se_doc.get("items")[0].update(
+ {"qty": 27.0, "transfer_qty": 27.0, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0}
+ )
+ se_doc.get("items")[1].update(
+ {"qty": 1.5, "transfer_qty": 1.5, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0}
+ )
# make available the qty in _Test Warehouse 1 before transfer
self._insert_stock_entry(27.0, 1.5)
@@ -296,17 +311,17 @@ class TestMaterialRequest(ERPNextTestCase):
# check for stopped status of Material Request
se = frappe.copy_doc(se_doc)
se.insert()
- mr.update_status('Stopped')
+ mr.update_status("Stopped")
self.assertRaises(frappe.InvalidStatusError, se.submit)
- mr.update_status('Submitted')
+ mr.update_status("Submitted")
se.flags.ignore_validate_update_after_submit = True
se.submit()
- mr.update_status('Stopped')
+ mr.update_status("Stopped")
self.assertRaises(frappe.InvalidStatusError, se.cancel)
- mr.update_status('Submitted')
+ mr.update_status("Submitted")
se = frappe.copy_doc(se_doc)
se.insert()
se.submit()
@@ -317,8 +332,12 @@ class TestMaterialRequest(ERPNextTestCase):
self.assertEqual(mr.get("items")[0].ordered_qty, 27.0)
self.assertEqual(mr.get("items")[1].ordered_qty, 1.5)
- current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
- current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
+ current_requested_qty_item1 = self._get_requested_qty(
+ "_Test Item Home Desktop 100", "_Test Warehouse - _TC"
+ )
+ current_requested_qty_item2 = self._get_requested_qty(
+ "_Test Item Home Desktop 200", "_Test Warehouse - _TC"
+ )
self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 27.0)
self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 1.5)
@@ -330,56 +349,70 @@ class TestMaterialRequest(ERPNextTestCase):
self.assertEqual(mr.get("items")[0].ordered_qty, 0)
self.assertEqual(mr.get("items")[1].ordered_qty, 0)
- current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
- current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
+ current_requested_qty_item1 = self._get_requested_qty(
+ "_Test Item Home Desktop 100", "_Test Warehouse - _TC"
+ )
+ current_requested_qty_item2 = self._get_requested_qty(
+ "_Test Item Home Desktop 200", "_Test Warehouse - _TC"
+ )
self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.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 = 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.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)
+ 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
- })
+ 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.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)
@@ -389,8 +422,12 @@ class TestMaterialRequest(ERPNextTestCase):
se.submit()
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_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_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"
+ )
# submit material request of type Purchase
mr = frappe.copy_doc(test_records[0])
@@ -401,23 +438,19 @@ class TestMaterialRequest(ERPNextTestCase):
# map a stock entry
se_doc = make_stock_entry(mr.name)
- se_doc.update({
- "posting_date": "2013-03-01",
- "posting_time": "00:00",
- "fiscal_year": "_Test Fiscal Year 2013",
- })
- se_doc.get("items")[0].update({
- "qty": 54.0,
- "transfer_qty": 54.0,
- "s_warehouse": "_Test Warehouse 1 - _TC",
- "basic_rate": 1.0
- })
- se_doc.get("items")[1].update({
- "qty": 3.0,
- "transfer_qty": 3.0,
- "s_warehouse": "_Test Warehouse 1 - _TC",
- "basic_rate": 1.0
- })
+ se_doc.update(
+ {
+ "posting_date": "2013-03-01",
+ "posting_time": "00:00",
+ "fiscal_year": "_Test Fiscal Year 2013",
+ }
+ )
+ se_doc.get("items")[0].update(
+ {"qty": 54.0, "transfer_qty": 54.0, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0}
+ )
+ se_doc.get("items")[1].update(
+ {"qty": 3.0, "transfer_qty": 3.0, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0}
+ )
# make available the qty in _Test Warehouse 1 before transfer
self._insert_stock_entry(60.0, 3.0)
@@ -426,11 +459,11 @@ class TestMaterialRequest(ERPNextTestCase):
se = frappe.copy_doc(se_doc)
se.set_stock_entry_type()
se.insert()
- mr.update_status('Stopped')
+ mr.update_status("Stopped")
self.assertRaises(frappe.InvalidStatusError, se.submit)
self.assertRaises(frappe.InvalidStatusError, se.cancel)
- mr.update_status('Submitted')
+ mr.update_status("Submitted")
se = frappe.copy_doc(se_doc)
se.set_stock_entry_type()
se.insert()
@@ -443,8 +476,12 @@ class TestMaterialRequest(ERPNextTestCase):
self.assertEqual(mr.get("items")[0].ordered_qty, 54.0)
self.assertEqual(mr.get("items")[1].ordered_qty, 3.0)
- current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
- current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
+ current_requested_qty_item1 = self._get_requested_qty(
+ "_Test Item Home Desktop 100", "_Test Warehouse - _TC"
+ )
+ current_requested_qty_item2 = self._get_requested_qty(
+ "_Test Item Home Desktop 200", "_Test Warehouse - _TC"
+ )
self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1)
self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2)
@@ -456,8 +493,12 @@ class TestMaterialRequest(ERPNextTestCase):
self.assertEqual(mr.get("items")[0].ordered_qty, 0)
self.assertEqual(mr.get("items")[1].ordered_qty, 0)
- current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
- current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
+ current_requested_qty_item1 = self._get_requested_qty(
+ "_Test Item Home Desktop 100", "_Test Warehouse - _TC"
+ )
+ current_requested_qty_item2 = self._get_requested_qty(
+ "_Test Item Home Desktop 200", "_Test Warehouse - _TC"
+ )
self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
@@ -470,25 +511,31 @@ class TestMaterialRequest(ERPNextTestCase):
mr.submit()
se_doc = make_stock_entry(mr.name)
- se_doc.update({
- "posting_date": "2013-03-01",
- "posting_time": "00:00",
- "fiscal_year": "_Test Fiscal Year 2013",
- })
- se_doc.get("items")[0].update({
- "qty": 60.0,
- "transfer_qty": 60.0,
- "s_warehouse": "_Test Warehouse - _TC",
- "t_warehouse": "_Test Warehouse 1 - _TC",
- "basic_rate": 1.0
- })
- se_doc.get("items")[1].update({
- "item_code": "_Test Item Home Desktop 100",
- "qty": 3.0,
- "transfer_qty": 3.0,
- "s_warehouse": "_Test Warehouse 1 - _TC",
- "basic_rate": 1.0
- })
+ se_doc.update(
+ {
+ "posting_date": "2013-03-01",
+ "posting_time": "00:00",
+ "fiscal_year": "_Test Fiscal Year 2013",
+ }
+ )
+ se_doc.get("items")[0].update(
+ {
+ "qty": 60.0,
+ "transfer_qty": 60.0,
+ "s_warehouse": "_Test Warehouse - _TC",
+ "t_warehouse": "_Test Warehouse 1 - _TC",
+ "basic_rate": 1.0,
+ }
+ )
+ se_doc.get("items")[1].update(
+ {
+ "item_code": "_Test Item Home Desktop 100",
+ "qty": 3.0,
+ "transfer_qty": 3.0,
+ "s_warehouse": "_Test Warehouse 1 - _TC",
+ "basic_rate": 1.0,
+ }
+ )
# check for stopped status of Material Request
se = frappe.copy_doc(se_doc)
@@ -505,18 +552,20 @@ class TestMaterialRequest(ERPNextTestCase):
def test_warehouse_company_validation(self):
from erpnext.stock.utils import InvalidWarehouseCompany
+
mr = frappe.copy_doc(test_records[0])
mr.company = "_Test Company 1"
self.assertRaises(InvalidWarehouseCompany, mr.insert)
def _get_requested_qty(self, item_code, warehouse):
- return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty"))
+ return flt(
+ frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty")
+ )
def test_make_stock_entry_for_material_issue(self):
mr = frappe.copy_doc(test_records[0]).insert()
- self.assertRaises(frappe.ValidationError, make_stock_entry,
- mr.name)
+ self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name)
mr = frappe.get_doc("Material Request", mr.name)
mr.material_request_type = "Material Issue"
@@ -528,8 +577,13 @@ class TestMaterialRequest(ERPNextTestCase):
def test_completed_qty_for_issue(self):
def _get_requested_qty():
- return flt(frappe.db.get_value("Bin", {"item_code": "_Test Item Home Desktop 100",
- "warehouse": "_Test Warehouse - _TC"}, "indented_qty"))
+ return flt(
+ frappe.db.get_value(
+ "Bin",
+ {"item_code": "_Test Item Home Desktop 100", "warehouse": "_Test Warehouse - _TC"},
+ "indented_qty",
+ )
+ )
existing_requested_qty = _get_requested_qty()
@@ -537,7 +591,7 @@ class TestMaterialRequest(ERPNextTestCase):
mr.material_request_type = "Material Issue"
mr.submit()
- #testing bin value after material request is submitted
+ # testing bin value after material request is submitted
self.assertEqual(_get_requested_qty(), existing_requested_qty - 54.0)
# receive items to allow issue
@@ -556,7 +610,7 @@ class TestMaterialRequest(ERPNextTestCase):
self.assertEqual(mr.get("items")[0].ordered_qty, 54.0)
self.assertEqual(mr.get("items")[1].ordered_qty, 3.0)
- #testing bin requested qty after issuing stock against material request
+ # testing bin requested qty after issuing stock against material request
self.assertEqual(_get_requested_qty(), existing_requested_qty)
def test_material_request_type_manufacture(self):
@@ -564,8 +618,11 @@ class TestMaterialRequest(ERPNextTestCase):
mr = frappe.get_doc("Material Request", mr.name)
mr.submit()
completed_qty = mr.items[0].ordered_qty
- requested_qty = frappe.db.sql("""select indented_qty from `tabBin` where \
- item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0]
+ requested_qty = frappe.db.sql(
+ """select indented_qty from `tabBin` where \
+ item_code= %s and warehouse= %s """,
+ (mr.items[0].item_code, mr.items[0].warehouse),
+ )[0][0]
prod_order = raise_work_orders(mr.name)
po = frappe.get_doc("Work Order", prod_order[0])
@@ -575,8 +632,11 @@ class TestMaterialRequest(ERPNextTestCase):
mr = frappe.get_doc("Material Request", mr.name)
self.assertEqual(completed_qty + po.qty, mr.items[0].ordered_qty)
- new_requested_qty = frappe.db.sql("""select indented_qty from `tabBin` where \
- item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0]
+ new_requested_qty = frappe.db.sql(
+ """select indented_qty from `tabBin` where \
+ item_code= %s and warehouse= %s """,
+ (mr.items[0].item_code, mr.items[0].warehouse),
+ )[0][0]
self.assertEqual(requested_qty - po.qty, new_requested_qty)
@@ -585,17 +645,24 @@ class TestMaterialRequest(ERPNextTestCase):
mr = frappe.get_doc("Material Request", mr.name)
self.assertEqual(completed_qty, mr.items[0].ordered_qty)
- new_requested_qty = frappe.db.sql("""select indented_qty from `tabBin` where \
- item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0]
+ new_requested_qty = frappe.db.sql(
+ """select indented_qty from `tabBin` where \
+ item_code= %s and warehouse= %s """,
+ (mr.items[0].item_code, mr.items[0].warehouse),
+ )[0][0]
self.assertEqual(requested_qty, new_requested_qty)
def test_requested_qty_multi_uom(self):
- existing_requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
+ existing_requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC")
- mr = make_material_request(item_code='_Test FG Item', material_request_type='Manufacture',
- uom="_Test UOM 1", conversion_factor=12)
+ mr = make_material_request(
+ item_code="_Test FG Item",
+ material_request_type="Manufacture",
+ uom="_Test UOM 1",
+ conversion_factor=12,
+ )
- requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
+ requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC")
self.assertEqual(requested_qty, existing_requested_qty + 120)
@@ -605,42 +672,36 @@ class TestMaterialRequest(ERPNextTestCase):
wo.wip_warehouse = "_Test Warehouse 1 - _TC"
wo.submit()
- requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
+ requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC")
self.assertEqual(requested_qty, existing_requested_qty + 70)
wo.cancel()
- requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
+ requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC")
self.assertEqual(requested_qty, existing_requested_qty + 120)
mr.reload()
mr.cancel()
- requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
+ requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC")
self.assertEqual(requested_qty, existing_requested_qty)
-
def test_multi_uom_for_purchase(self):
mr = frappe.copy_doc(test_records[0])
- mr.material_request_type = 'Purchase'
+ mr.material_request_type = "Purchase"
item = mr.items[0]
mr.schedule_date = today()
- if not frappe.db.get_value('UOM Conversion Detail',
- {'parent': item.item_code, 'uom': 'Kg'}):
- item_doc = frappe.get_doc('Item', item.item_code)
- item_doc.append('uoms', {
- 'uom': 'Kg',
- 'conversion_factor': 5
- })
- item_doc.save(ignore_permissions=True)
+ if not frappe.db.get_value("UOM Conversion Detail", {"parent": item.item_code, "uom": "Kg"}):
+ item_doc = frappe.get_doc("Item", item.item_code)
+ item_doc.append("uoms", {"uom": "Kg", "conversion_factor": 5})
+ item_doc.save(ignore_permissions=True)
- item.uom = 'Kg'
+ item.uom = "Kg"
for item in mr.items:
item.schedule_date = mr.schedule_date
mr.insert()
- self.assertRaises(frappe.ValidationError, make_purchase_order,
- mr.name)
+ self.assertRaises(frappe.ValidationError, make_purchase_order, mr.name)
mr = frappe.get_doc("Material Request", mr.name)
mr.submit()
@@ -654,17 +715,19 @@ class TestMaterialRequest(ERPNextTestCase):
self.assertEqual(po.doctype, "Purchase Order")
self.assertEqual(len(po.get("items")), len(mr.get("items")))
- po.supplier = '_Test Supplier'
+ po.supplier = "_Test Supplier"
po.insert()
po.submit()
mr = frappe.get_doc("Material Request", mr.name)
self.assertEqual(mr.per_ordered, 100)
def test_customer_provided_parts_mr(self):
- create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
+ create_item(
+ "CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0
+ )
existing_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC")
- mr = make_material_request(item_code='CUST-0987', material_request_type='Customer Provided')
+ mr = make_material_request(item_code="CUST-0987", material_request_type="Customer Provided")
se = make_stock_entry(mr.name)
se.insert()
se.submit()
@@ -677,25 +740,30 @@ class TestMaterialRequest(ERPNextTestCase):
self.assertEqual(mr.per_ordered, 100)
self.assertEqual(existing_requested_qty, current_requested_qty)
+
def make_material_request(**args):
args = frappe._dict(args)
mr = frappe.new_doc("Material Request")
mr.material_request_type = args.material_request_type or "Purchase"
mr.company = args.company or "_Test Company"
- mr.customer = args.customer or '_Test Customer'
- mr.append("items", {
- "item_code": args.item_code or "_Test Item",
- "qty": args.qty or 10,
- "uom": args.uom or "_Test UOM",
- "conversion_factor": args.conversion_factor or 1,
- "schedule_date": args.schedule_date or today(),
- "warehouse": args.warehouse or "_Test Warehouse - _TC",
- "cost_center": args.cost_center or "_Test Cost Center - _TC"
- })
+ mr.customer = args.customer or "_Test Customer"
+ mr.append(
+ "items",
+ {
+ "item_code": args.item_code or "_Test Item",
+ "qty": args.qty or 10,
+ "uom": args.uom or "_Test UOM",
+ "conversion_factor": args.conversion_factor or 1,
+ "schedule_date": args.schedule_date or today(),
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "cost_center": args.cost_center or "_Test Cost Center - _TC",
+ },
+ )
mr.insert()
if not args.do_not_submit:
mr.submit()
return mr
+
test_dependencies = ["Currency Exchange", "BOM"]
-test_records = frappe.get_test_records('Material Request')
+test_records = frappe.get_test_records("Material Request")
diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json
index 2bad42a0ebb..dd66cfff8be 100644
--- a/erpnext/stock/doctype/material_request_item/material_request_item.json
+++ b/erpnext/stock/doctype/material_request_item/material_request_item.json
@@ -177,6 +177,7 @@
"fieldtype": "Column Break"
},
{
+ "allow_on_submit": 1,
"bold": 1,
"columns": 2,
"fieldname": "schedule_date",
@@ -459,7 +460,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-11-03 14:40:24.409826",
+ "modified": "2022-03-10 18:42:42.705190",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request Item",
diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.py b/erpnext/stock/doctype/material_request_item/material_request_item.py
index 32407d0fb09..08c9ed27427 100644
--- a/erpnext/stock/doctype/material_request_item/material_request_item.py
+++ b/erpnext/stock/doctype/material_request_item/material_request_item.py
@@ -11,5 +11,6 @@ from frappe.model.document import Document
class MaterialRequestItem(Document):
pass
+
def on_doctype_update():
frappe.db.add_index("Material Request Item", ["item_code", "warehouse"])
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index d2d47897658..cb8eb30cb30 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -26,8 +26,10 @@
"section_break_13",
"actual_qty",
"projected_qty",
+ "ordered_qty",
"column_break_16",
"incoming_rate",
+ "picked_qty",
"page_break",
"prevdoc_doctype",
"parent_detail_docname"
@@ -222,15 +224,31 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate",
+ "options": "currency",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "ordered_qty",
+ "fieldtype": "Float",
+ "label": "Ordered Qty",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "picked_qty",
+ "fieldtype": "Float",
+ "label": "Picked Qty",
+ "no_copy": 1,
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-01-28 16:03:30.780111",
+ "modified": "2022-04-27 05:23:08.683245",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index 07c2f1f0dd3..4d05d7a345c 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -23,19 +23,23 @@ def make_packing_list(doc):
return
parent_items_price, reset = {}, False
- set_price_from_children = frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates")
+ set_price_from_children = frappe.db.get_single_value(
+ "Selling Settings", "editable_bundle_item_rates"
+ )
stale_packed_items_table = get_indexed_packed_items_table(doc)
reset = reset_packing_list(doc)
for item_row in doc.get("items"):
- if frappe.db.exists("Product Bundle", {"new_item_code": item_row.item_code}):
+ if is_product_bundle(item_row.item_code):
for bundle_item in get_product_bundle_items(item_row.item_code):
pi_row = add_packed_item_row(
- doc=doc, packing_item=bundle_item,
- main_item_row=item_row, packed_items_table=stale_packed_items_table,
- reset=reset
+ doc=doc,
+ packing_item=bundle_item,
+ main_item_row=item_row,
+ packed_items_table=stale_packed_items_table,
+ reset=reset,
)
item_data = get_packed_item_details(bundle_item.item_code, doc.company)
update_packed_item_basic_data(item_row, pi_row, bundle_item, item_data)
@@ -43,18 +47,23 @@ def make_packing_list(doc):
update_packed_item_price_data(pi_row, item_data, doc)
update_packed_item_from_cancelled_doc(item_row, bundle_item, pi_row, doc)
- if set_price_from_children: # create/update bundle item wise price dict
+ if set_price_from_children: # create/update bundle item wise price dict
update_product_bundle_rate(parent_items_price, pi_row)
if parent_items_price:
- set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item
+ set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item
+
+
+def is_product_bundle(item_code: str) -> bool:
+ return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code}))
+
def get_indexed_packed_items_table(doc):
"""
- Create dict from stale packed items table like:
- {(Parent Item 1, Bundle Item 1, ae4b5678): {...}, (key): {value}}
+ Create dict from stale packed items table like:
+ {(Parent Item 1, Bundle Item 1, ae4b5678): {...}, (key): {value}}
- Use: to quickly retrieve/check if row existed in table instead of looping n times
+ Use: to quickly retrieve/check if row existed in table instead of looping n times
"""
indexed_table = {}
for packed_item in doc.get("packed_items"):
@@ -63,6 +72,7 @@ def get_indexed_packed_items_table(doc):
return indexed_table
+
def reset_packing_list(doc):
"Conditionally reset the table and return if it was reset or not."
reset_table = False
@@ -86,33 +96,34 @@ def reset_packing_list(doc):
doc.set("packed_items", [])
return reset_table
+
def get_product_bundle_items(item_code):
product_bundle = frappe.qb.DocType("Product Bundle")
product_bundle_item = frappe.qb.DocType("Product Bundle Item")
query = (
frappe.qb.from_(product_bundle_item)
- .join(product_bundle).on(product_bundle_item.parent == product_bundle.name)
+ .join(product_bundle)
+ .on(product_bundle_item.parent == product_bundle.name)
.select(
product_bundle_item.item_code,
product_bundle_item.qty,
product_bundle_item.uom,
- product_bundle_item.description
- ).where(
- product_bundle.new_item_code == item_code
- ).orderby(
- product_bundle_item.idx
+ product_bundle_item.description,
)
+ .where(product_bundle.new_item_code == item_code)
+ .orderby(product_bundle_item.idx)
)
return query.run(as_dict=True)
+
def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, reset):
"""Add and return packed item row.
- doc: Transaction document
- packing_item (dict): Packed Item details
- main_item_row (dict): Items table row corresponding to packed item
- packed_items_table (dict): Packed Items table before save (indexed)
- reset (bool): State if table is reset or preserved as is
+ doc: Transaction document
+ packing_item (dict): Packed Item details
+ main_item_row (dict): Items table row corresponding to packed item
+ packed_items_table (dict): Packed Items table before save (indexed)
+ reset (bool): State if table is reset or preserved as is
"""
exists, pi_row = False, {}
@@ -122,33 +133,34 @@ def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, re
pi_row, exists = packed_items_table.get(key), True
if not exists:
- pi_row = doc.append('packed_items', {})
- elif reset: # add row if row exists but table is reset
+ pi_row = doc.append("packed_items", {})
+ elif reset: # add row if row exists but table is reset
pi_row.idx, pi_row.name = None, None
- pi_row = doc.append('packed_items', pi_row)
+ pi_row = doc.append("packed_items", pi_row)
return pi_row
+
def get_packed_item_details(item_code, company):
item = frappe.qb.DocType("Item")
item_default = frappe.qb.DocType("Item Default")
query = (
frappe.qb.from_(item)
.left_join(item_default)
- .on(
- (item_default.parent == item.name)
- & (item_default.company == company)
- ).select(
- item.item_name, item.is_stock_item,
- item.description, item.stock_uom,
+ .on((item_default.parent == item.name) & (item_default.company == company))
+ .select(
+ item.item_name,
+ item.is_stock_item,
+ item.description,
+ item.stock_uom,
item.valuation_rate,
- item_default.default_warehouse
- ).where(
- item.name == item_code
+ item_default.default_warehouse,
)
+ .where(item.name == item_code)
)
return query.run(as_dict=True)[0]
+
def update_packed_item_basic_data(main_item_row, pi_row, packing_item, item_data):
pi_row.parent_item = main_item_row.item_code
pi_row.parent_detail_docname = main_item_row.name
@@ -161,12 +173,16 @@ def update_packed_item_basic_data(main_item_row, pi_row, packing_item, item_data
if not pi_row.description:
pi_row.description = packing_item.get("description")
+
def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data, doc):
# TODO batch_no, actual_batch_qty, incoming_rate
if not pi_row.warehouse and not doc.amended_from:
- fetch_warehouse = (doc.get('is_pos') or item_data.is_stock_item or not item_data.default_warehouse)
- pi_row.warehouse = (main_item_row.warehouse if (fetch_warehouse and main_item_row.warehouse)
- else item_data.default_warehouse)
+ fetch_warehouse = doc.get("is_pos") or item_data.is_stock_item or not item_data.default_warehouse
+ pi_row.warehouse = (
+ main_item_row.warehouse
+ if (fetch_warehouse and main_item_row.warehouse)
+ else item_data.default_warehouse
+ )
if not pi_row.target_warehouse:
pi_row.target_warehouse = main_item_row.get("target_warehouse")
@@ -175,6 +191,7 @@ def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data
pi_row.actual_qty = flt(bin.get("actual_qty"))
pi_row.projected_qty = flt(bin.get("projected_qty"))
+
def update_packed_item_price_data(pi_row, item_data, doc):
"Set price as per price list or from the Item master."
if pi_row.rate:
@@ -182,49 +199,60 @@ def update_packed_item_price_data(pi_row, item_data, doc):
item_doc = frappe.get_cached_doc("Item", pi_row.item_code)
row_data = pi_row.as_dict().copy()
- row_data.update({
- "company": doc.get("company"),
- "price_list": doc.get("selling_price_list"),
- "currency": doc.get("currency")
- })
+ row_data.update(
+ {
+ "company": doc.get("company"),
+ "price_list": doc.get("selling_price_list"),
+ "currency": doc.get("currency"),
+ "conversion_rate": doc.get("conversion_rate"),
+ }
+ )
rate = get_price_list_rate(row_data, item_doc).get("price_list_rate")
pi_row.rate = rate or item_data.get("valuation_rate") or 0.0
+
def update_packed_item_from_cancelled_doc(main_item_row, packing_item, pi_row, doc):
"Update packed item row details from cancelled doc into amended doc."
prev_doc_packed_items_map = None
if doc.amended_from:
prev_doc_packed_items_map = get_cancelled_doc_packed_item_details(doc.packed_items)
- if prev_doc_packed_items_map and prev_doc_packed_items_map.get((packing_item.item_code, main_item_row.item_code)):
+ if prev_doc_packed_items_map and prev_doc_packed_items_map.get(
+ (packing_item.item_code, main_item_row.item_code)
+ ):
prev_doc_row = prev_doc_packed_items_map.get((packing_item.item_code, main_item_row.item_code))
pi_row.batch_no = prev_doc_row[0].batch_no
pi_row.serial_no = prev_doc_row[0].serial_no
pi_row.warehouse = prev_doc_row[0].warehouse
+
def get_packed_item_bin_qty(item, warehouse):
bin_data = frappe.db.get_values(
"Bin",
fieldname=["actual_qty", "projected_qty"],
filters={"item_code": item, "warehouse": warehouse},
- as_dict=True
+ as_dict=True,
)
return bin_data[0] if bin_data else {}
+
def get_cancelled_doc_packed_item_details(old_packed_items):
prev_doc_packed_items_map = {}
for items in old_packed_items:
- prev_doc_packed_items_map.setdefault((items.item_code ,items.parent_item), []).append(items.as_dict())
+ prev_doc_packed_items_map.setdefault((items.item_code, items.parent_item), []).append(
+ items.as_dict()
+ )
return prev_doc_packed_items_map
+
def update_product_bundle_rate(parent_items_price, pi_row):
"""
- Update the price dict of Product Bundles based on the rates of the Items in the bundle.
+ Update the price dict of Product Bundles based on the rates of the Items in the bundle.
- Stucture:
- {(Bundle Item 1, ae56fgji): 150.0, (Bundle Item 2, bc78fkjo): 200.0}
+ Stucture:
+ {(Bundle Item 1, ae56fgji): 150.0, (Bundle Item 2, bc78fkjo): 200.0}
"""
key = (pi_row.parent_item, pi_row.parent_detail_docname)
rate = parent_items_price.get(key)
@@ -233,6 +261,7 @@ def update_product_bundle_rate(parent_items_price, pi_row):
parent_items_price[key] += flt(pi_row.rate)
+
def set_product_bundle_rate_amount(doc, parent_items_price):
"Set cumulative rate and amount in bundle item."
for item in doc.get("items"):
@@ -241,6 +270,7 @@ def set_product_bundle_rate_amount(doc, parent_items_price):
item.rate = bundle_rate
item.amount = flt(bundle_rate * item.qty)
+
def on_doctype_update():
frappe.db.add_index("Packed Item", ["item_code", "warehouse"])
@@ -251,10 +281,7 @@ def get_items_from_product_bundle(row):
bundled_items = get_product_bundle_items(row["item_code"])
for item in bundled_items:
- row.update({
- "item_code": item.item_code,
- "qty": flt(row["quantity"]) * flt(item.qty)
- })
+ row.update({"item_code": item.item_code, "qty": flt(row["quantity"]) * flt(item.qty)})
items.append(get_item_details(row))
return items
diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py
index 2521ac9fe72..ad7fd9a6976 100644
--- a/erpnext/stock/doctype/packed_item/test_packed_item.py
+++ b/erpnext/stock/doctype/packed_item/test_packed_item.py
@@ -1,36 +1,62 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
+from typing import List, Optional, Tuple
+
+import frappe
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_to_date, nowdate
-from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-from erpnext.tests.utils import ERPNextTestCase, change_settings
-class TestPackedItem(ERPNextTestCase):
+def create_product_bundle(
+ quantities: Optional[List[int]] = None, warehouse: Optional[str] = None
+) -> Tuple[str, List[str]]:
+ """Get a new product_bundle for use in tests.
+
+ Create 10x required stock if warehouse is specified.
+ """
+ if not quantities:
+ quantities = [2, 2]
+
+ bundle = make_item(properties={"is_stock_item": 0}).name
+
+ bundle_doc = frappe.get_doc({"doctype": "Product Bundle", "new_item_code": bundle})
+
+ components = []
+ for qty in quantities:
+ compoenent = make_item().name
+ components.append(compoenent)
+ bundle_doc.append("items", {"item_code": compoenent, "qty": qty})
+ if warehouse:
+ make_stock_entry(item=compoenent, to_warehouse=warehouse, qty=10 * qty, rate=100)
+
+ bundle_doc.insert()
+
+ return bundle, components
+
+
+class TestPackedItem(FrappeTestCase):
"Test impact on Packed Items table in various scenarios."
+
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
- cls.bundle = "_Test Product Bundle X"
- cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"]
- make_item(cls.bundle, {"is_stock_item": 0})
- for item in cls.bundle_items:
- make_item(item, {"is_stock_item": 1})
+ cls.warehouse = "_Test Warehouse - _TC"
- make_item("_Test Normal Stock Item", {"is_stock_item": 1})
+ cls.bundle, cls.bundle_items = create_product_bundle(warehouse=cls.warehouse)
+ cls.bundle2, cls.bundle2_items = create_product_bundle(warehouse=cls.warehouse)
- make_product_bundle(cls.bundle, cls.bundle_items, qty=2)
+ cls.normal_item = make_item().name
def test_adding_bundle_item(self):
"Test impact on packed items if bundle item row is added."
- so = make_sales_order(item_code = self.bundle, qty=1,
- do_not_submit=True)
+ so = make_sales_order(item_code=self.bundle, qty=1, do_not_submit=True)
self.assertEqual(so.items[0].qty, 1)
self.assertEqual(len(so.packed_items), 2)
@@ -41,14 +67,14 @@ class TestPackedItem(ERPNextTestCase):
"Test impact on packed items if bundle item row is updated."
so = make_sales_order(item_code=self.bundle, qty=1, do_not_submit=True)
- so.items[0].qty = 2 # change qty
+ so.items[0].qty = 2 # change qty
so.save()
self.assertEqual(so.packed_items[0].qty, 4)
self.assertEqual(so.packed_items[1].qty, 4)
# change item code to non bundle item
- so.items[0].item_code = "_Test Normal Stock Item"
+ so.items[0].item_code = self.normal_item
so.save()
self.assertEqual(len(so.packed_items), 0)
@@ -57,12 +83,9 @@ class TestPackedItem(ERPNextTestCase):
"Test impact on packed items if same bundle item is added and removed."
so_items = []
for qty in [2, 4, 6, 8]:
- so_items.append({
- "item_code": self.bundle,
- "qty": qty,
- "rate": 400,
- "warehouse": "_Test Warehouse - _TC"
- })
+ so_items.append(
+ {"item_code": self.bundle, "qty": qty, "rate": 400, "warehouse": "_Test Warehouse - _TC"}
+ )
# create SO with recurring bundle item
so = make_sales_order(item_list=so_items, do_not_submit=True)
@@ -110,18 +133,15 @@ class TestPackedItem(ERPNextTestCase):
"Test impact on packed items in newly mapped DN from SO."
so_items = []
for qty in [2, 4]:
- so_items.append({
- "item_code": self.bundle,
- "qty": qty,
- "rate": 400,
- "warehouse": "_Test Warehouse - _TC"
- })
+ so_items.append(
+ {"item_code": self.bundle, "qty": qty, "rate": 400, "warehouse": "_Test Warehouse - _TC"}
+ )
# create SO with recurring bundle item
so = make_sales_order(item_list=so_items)
dn = make_delivery_note(so.name)
- dn.items[1].qty = 3 # change second row qty for inserting doc
+ dn.items[1].qty = 3 # change second row qty for inserting doc
dn.save()
self.assertEqual(len(dn.packed_items), 4)
@@ -138,7 +158,7 @@ class TestPackedItem(ERPNextTestCase):
for item in self.bundle_items:
make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date=today)
- so = make_sales_order(item_code = self.bundle, qty=1, company=company, warehouse=warehouse)
+ so = make_sales_order(item_code=self.bundle, qty=1, company=company, warehouse=warehouse)
dn = make_delivery_note(so.name)
dn.save()
@@ -149,10 +169,111 @@ class TestPackedItem(ERPNextTestCase):
# backdated stock entry
for item in self.bundle_items:
- make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday)
+ make_stock_entry(
+ item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday
+ )
# assert correct reposting
gles = get_gl_entries(dn.doctype, dn.name)
credit_after_reposting = sum(gle.credit for gle in gles)
self.assertNotEqual(credit_before_repost, credit_after_reposting)
self.assertAlmostEqual(credit_after_reposting, 2 * credit_before_repost)
+
+ def assertReturns(self, original, returned):
+ self.assertEqual(len(original), len(returned))
+
+ sort_function = lambda p: (p.parent_item, p.item_code, p.qty)
+
+ for sent, returned in zip(
+ sorted(original, key=sort_function), sorted(returned, key=sort_function)
+ ):
+ self.assertEqual(sent.item_code, returned.item_code)
+ self.assertEqual(sent.parent_item, returned.parent_item)
+ self.assertEqual(sent.qty, -1 * returned.qty)
+
+ def test_returning_full_bundles(self):
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+
+ item_list = [
+ {
+ "item_code": self.bundle,
+ "warehouse": self.warehouse,
+ "qty": 1,
+ "rate": 100,
+ },
+ {
+ "item_code": self.bundle2,
+ "warehouse": self.warehouse,
+ "qty": 1,
+ "rate": 100,
+ },
+ ]
+ so = make_sales_order(item_list=item_list, warehouse=self.warehouse)
+
+ dn = make_delivery_note(so.name)
+ dn.save()
+ dn.submit()
+
+ # create return
+ dn_ret = make_sales_return(dn.name)
+ dn_ret.save()
+ dn_ret.submit()
+ self.assertReturns(dn.packed_items, dn_ret.packed_items)
+
+ def test_returning_partial_bundles(self):
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+
+ item_list = [
+ {
+ "item_code": self.bundle,
+ "warehouse": self.warehouse,
+ "qty": 1,
+ "rate": 100,
+ },
+ {
+ "item_code": self.bundle2,
+ "warehouse": self.warehouse,
+ "qty": 1,
+ "rate": 100,
+ },
+ ]
+ so = make_sales_order(item_list=item_list, warehouse=self.warehouse)
+
+ dn = make_delivery_note(so.name)
+ dn.save()
+ dn.submit()
+
+ # create return
+ dn_ret = make_sales_return(dn.name)
+ # remove bundle 2
+ dn_ret.items.pop()
+
+ dn_ret.save()
+ dn_ret.submit()
+ dn_ret.reload()
+
+ self.assertTrue(all(d.parent_item == self.bundle for d in dn_ret.packed_items))
+
+ expected_returns = [d for d in dn.packed_items if d.parent_item == self.bundle]
+ self.assertReturns(expected_returns, dn_ret.packed_items)
+
+ def test_returning_partial_bundle_qty(self):
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+
+ so = make_sales_order(item_code=self.bundle, warehouse=self.warehouse, qty=2)
+
+ dn = make_delivery_note(so.name)
+ dn.save()
+ dn.submit()
+
+ # create return
+ dn_ret = make_sales_return(dn.name)
+ # halve the qty
+ dn_ret.items[0].qty = -1
+ dn_ret.save()
+ dn_ret.submit()
+
+ expected_returns = dn.packed_items
+ for d in expected_returns:
+ d.qty /= 2
+ self.assertReturns(expected_returns, dn_ret.packed_items)
diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py
index b092862415a..e9ccf5fc779 100644
--- a/erpnext/stock/doctype/packing_slip/packing_slip.py
+++ b/erpnext/stock/doctype/packing_slip/packing_slip.py
@@ -10,14 +10,13 @@ from frappe.utils import cint, flt
class PackingSlip(Document):
-
def validate(self):
"""
- * Validate existence of submitted Delivery Note
- * Case nos do not overlap
- * Check if packed qty doesn't exceed actual qty of delivery note
+ * Validate existence of submitted Delivery Note
+ * Case nos do not overlap
+ * Check if packed qty doesn't exceed actual qty of delivery note
- It is necessary to validate case nos before checking quantity
+ It is necessary to validate case nos before checking quantity
"""
self.validate_delivery_note()
self.validate_items_mandatory()
@@ -25,12 +24,13 @@ class PackingSlip(Document):
self.validate_qty()
from erpnext.utilities.transaction_base import validate_uom_is_integer
+
validate_uom_is_integer(self, "stock_uom", "qty")
validate_uom_is_integer(self, "weight_uom", "net_weight")
def validate_delivery_note(self):
"""
- Validates if delivery note has status as draft
+ Validates if delivery note has status as draft
"""
if cint(frappe.db.get_value("Delivery Note", self.delivery_note, "docstatus")) != 0:
frappe.throw(_("Delivery Note {0} must not be submitted").format(self.delivery_note))
@@ -42,27 +42,33 @@ class PackingSlip(Document):
def validate_case_nos(self):
"""
- Validate if case nos overlap. If they do, recommend next case no.
+ Validate if case nos overlap. If they do, recommend next case no.
"""
if not cint(self.from_case_no):
frappe.msgprint(_("Please specify a valid 'From Case No.'"), raise_exception=1)
elif not self.to_case_no:
self.to_case_no = self.from_case_no
elif cint(self.from_case_no) > cint(self.to_case_no):
- frappe.msgprint(_("'To Case No.' cannot be less than 'From Case No.'"),
- raise_exception=1)
+ frappe.msgprint(_("'To Case No.' cannot be less than 'From Case No.'"), raise_exception=1)
- res = frappe.db.sql("""SELECT name FROM `tabPacking Slip`
+ res = frappe.db.sql(
+ """SELECT name FROM `tabPacking Slip`
WHERE delivery_note = %(delivery_note)s AND docstatus = 1 AND
((from_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s)
OR (to_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s)
OR (%(from_case_no)s BETWEEN from_case_no AND to_case_no))
- """, {"delivery_note":self.delivery_note,
- "from_case_no":self.from_case_no,
- "to_case_no":self.to_case_no})
+ """,
+ {
+ "delivery_note": self.delivery_note,
+ "from_case_no": self.from_case_no,
+ "to_case_no": self.to_case_no,
+ },
+ )
if res:
- frappe.throw(_("""Case No(s) already in use. Try from Case No {0}""").format(self.get_recommended_case_no()))
+ frappe.throw(
+ _("""Case No(s) already in use. Try from Case No {0}""").format(self.get_recommended_case_no())
+ )
def validate_qty(self):
"""Check packed qty across packing slips and delivery note"""
@@ -70,36 +76,37 @@ class PackingSlip(Document):
dn_details, ps_item_qty, no_of_cases = self.get_details_for_packing()
for item in dn_details:
- new_packed_qty = (flt(ps_item_qty[item['item_code']]) * no_of_cases) + \
- flt(item['packed_qty'])
- if new_packed_qty > flt(item['qty']) and no_of_cases:
+ new_packed_qty = (flt(ps_item_qty[item["item_code"]]) * no_of_cases) + flt(item["packed_qty"])
+ if new_packed_qty > flt(item["qty"]) and no_of_cases:
self.recommend_new_qty(item, ps_item_qty, no_of_cases)
-
def get_details_for_packing(self):
"""
- Returns
- * 'Delivery Note Items' query result as a list of dict
- * Item Quantity dict of current packing slip doc
- * No. of Cases of this packing slip
+ Returns
+ * 'Delivery Note Items' query result as a list of dict
+ * Item Quantity dict of current packing slip doc
+ * No. of Cases of this packing slip
"""
rows = [d.item_code for d in self.get("items")]
# also pick custom fields from delivery note
- custom_fields = ', '.join('dni.`{0}`'.format(d.fieldname)
+ custom_fields = ", ".join(
+ "dni.`{0}`".format(d.fieldname)
for d in frappe.get_meta("Delivery Note Item").get_custom_fields()
- if d.fieldtype not in no_value_fields)
+ if d.fieldtype not in no_value_fields
+ )
if custom_fields:
- custom_fields = ', ' + custom_fields
+ custom_fields = ", " + custom_fields
condition = ""
if rows:
- condition = " and item_code in (%s)" % (", ".join(["%s"]*len(rows)))
+ condition = " and item_code in (%s)" % (", ".join(["%s"] * len(rows)))
# gets item code, qty per item code, latest packed qty per item code and stock uom
- res = frappe.db.sql("""select item_code, sum(qty) as qty,
+ res = frappe.db.sql(
+ """select item_code, sum(qty) as qty,
(select sum(psi.qty * (abs(ps.to_case_no - ps.from_case_no) + 1))
from `tabPacking Slip` ps, `tabPacking Slip Item` psi
where ps.name = psi.parent and ps.docstatus = 1
@@ -107,47 +114,57 @@ class PackingSlip(Document):
stock_uom, item_name, description, dni.batch_no {custom_fields}
from `tabDelivery Note Item` dni
where parent=%s {condition}
- group by item_code""".format(condition=condition, custom_fields=custom_fields),
- tuple([self.delivery_note] + rows), as_dict=1)
+ group by item_code""".format(
+ condition=condition, custom_fields=custom_fields
+ ),
+ tuple([self.delivery_note] + rows),
+ as_dict=1,
+ )
ps_item_qty = dict([[d.item_code, d.qty] for d in self.get("items")])
no_of_cases = cint(self.to_case_no) - cint(self.from_case_no) + 1
return res, ps_item_qty, no_of_cases
-
def recommend_new_qty(self, item, ps_item_qty, no_of_cases):
"""
- Recommend a new quantity and raise a validation exception
+ Recommend a new quantity and raise a validation exception
"""
- item['recommended_qty'] = (flt(item['qty']) - flt(item['packed_qty'])) / no_of_cases
- item['specified_qty'] = flt(ps_item_qty[item['item_code']])
- if not item['packed_qty']: item['packed_qty'] = 0
+ item["recommended_qty"] = (flt(item["qty"]) - flt(item["packed_qty"])) / no_of_cases
+ item["specified_qty"] = flt(ps_item_qty[item["item_code"]])
+ if not item["packed_qty"]:
+ item["packed_qty"] = 0
- frappe.throw(_("Quantity for Item {0} must be less than {1}").format(item.get("item_code"), item.get("recommended_qty")))
+ frappe.throw(
+ _("Quantity for Item {0} must be less than {1}").format(
+ item.get("item_code"), item.get("recommended_qty")
+ )
+ )
def update_item_details(self):
"""
- Fill empty columns in Packing Slip Item
+ Fill empty columns in Packing Slip Item
"""
if not self.from_case_no:
self.from_case_no = self.get_recommended_case_no()
for d in self.get("items"):
- res = frappe.db.get_value("Item", d.item_code,
- ["weight_per_unit", "weight_uom"], as_dict=True)
+ res = frappe.db.get_value("Item", d.item_code, ["weight_per_unit", "weight_uom"], as_dict=True)
- if res and len(res)>0:
+ if res and len(res) > 0:
d.net_weight = res["weight_per_unit"]
d.weight_uom = res["weight_uom"]
def get_recommended_case_no(self):
"""
- Returns the next case no. for a new packing slip for a delivery
- note
+ Returns the next case no. for a new packing slip for a delivery
+ note
"""
- recommended_case_no = frappe.db.sql("""SELECT MAX(to_case_no) FROM `tabPacking Slip`
- WHERE delivery_note = %s AND docstatus=1""", self.delivery_note)
+ recommended_case_no = frappe.db.sql(
+ """SELECT MAX(to_case_no) FROM `tabPacking Slip`
+ WHERE delivery_note = %s AND docstatus=1""",
+ self.delivery_note,
+ )
return cint(recommended_case_no[0][0]) + 1
@@ -160,7 +177,7 @@ class PackingSlip(Document):
dn_details = self.get_details_for_packing()[0]
for item in dn_details:
if flt(item.qty) > flt(item.packed_qty):
- ch = self.append('items', {})
+ ch = self.append("items", {})
ch.item_code = item.item_code
ch.item_name = item.item_name
ch.stock_uom = item.stock_uom
@@ -175,14 +192,18 @@ class PackingSlip(Document):
self.update_item_details()
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def item_details(doctype, txt, searchfield, start, page_len, filters):
from erpnext.controllers.queries import get_match_cond
- return frappe.db.sql("""select name, item_name, description from `tabItem`
+
+ return frappe.db.sql(
+ """select name, item_name, description from `tabItem`
where name in ( select item_code FROM `tabDelivery Note Item`
where parent= %s)
and %s like "%s" %s
- limit %s, %s """ % ("%s", searchfield, "%s",
- get_match_cond(doctype), "%s", "%s"),
- ((filters or {}).get("delivery_note"), "%%%s%%" % txt, start, page_len))
+ limit %s, %s """
+ % ("%s", searchfield, "%s", get_match_cond(doctype), "%s", "%s"),
+ ((filters or {}).get("delivery_note"), "%%%s%%" % txt, start, page_len),
+ )
diff --git a/erpnext/stock/doctype/packing_slip/test_packing_slip.py b/erpnext/stock/doctype/packing_slip/test_packing_slip.py
index 5eb6b7399ae..bc405b20995 100644
--- a/erpnext/stock/doctype/packing_slip/test_packing_slip.py
+++ b/erpnext/stock/doctype/packing_slip/test_packing_slip.py
@@ -4,7 +4,7 @@
import unittest
# test_records = frappe.get_test_records('Packing Slip')
-from erpnext.tests.utils import ERPNextTestCase
+from frappe.tests.utils import FrappeTestCase
class TestPackingSlip(unittest.TestCase):
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
index 730fd7a829c..13b74b5eb16 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -146,10 +146,6 @@ frappe.ui.form.on('Pick List', {
customer: frm.doc.customer
};
frm.get_items_btn = frm.add_custom_button(__('Get Items'), () => {
- if (!frm.doc.customer) {
- frappe.msgprint(__('Please select Customer first'));
- return;
- }
erpnext.utils.map_current_doc({
method: 'erpnext.selling.doctype.sales_order.sales_order.create_pick_list',
source_doctype: 'Sales Order',
diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json
index c604c711ef5..e984c082d48 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.json
+++ b/erpnext/stock/doctype/pick_list/pick_list.json
@@ -114,6 +114,7 @@
"set_only_once": 1
},
{
+ "collapsible": 1,
"fieldname": "print_settings_section",
"fieldtype": "Section Break",
"label": "Print Settings"
@@ -129,7 +130,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-10-05 15:08:40.369957",
+ "modified": "2022-04-21 07:56:40.646473",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",
@@ -199,5 +200,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index b7987543f2b..858481aa7b9 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -3,13 +3,15 @@
import json
from collections import OrderedDict, defaultdict
+from itertools import groupby
+from typing import Dict, List, Set
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc
from frappe.utils import cint, floor, flt, today
-from six import iteritems
+from frappe.utils.nestedset import get_descendants_of
from erpnext.selling.doctype.sales_order.sales_order import (
make_delivery_note as create_delivery_note_from_sales_order,
@@ -18,6 +20,7 @@ from erpnext.stock.get_item_details import get_conversion_factor
# TODO: Prioritize SO or WO group warehouse
+
class PickList(Document):
def validate(self):
self.validate_for_qty()
@@ -25,18 +28,86 @@ class PickList(Document):
def before_save(self):
self.set_item_locations()
+ # set percentage picked in SO
+ for location in self.get("locations"):
+ if (
+ location.sales_order
+ and frappe.db.get_value("Sales Order", location.sales_order, "per_picked") == 100
+ ):
+ frappe.throw("Row " + str(location.idx) + " has been picked already!")
+
def before_submit(self):
+ update_sales_orders = set()
for item in self.locations:
- if not frappe.get_cached_value('Item', item.item_code, 'has_serial_no'):
+ # if the user has not entered any picked qty, set it to stock_qty, before submit
+ if item.picked_qty == 0:
+ item.picked_qty = item.stock_qty
+
+ if item.sales_order_item:
+ # update the picked_qty in SO Item
+ self.update_sales_order_item(item, item.picked_qty, item.item_code)
+ update_sales_orders.add(item.sales_order)
+
+ if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
continue
if not item.serial_no:
- frappe.throw(_("Row #{0}: {1} does not have any available serial numbers in {2}").format(
- frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse)),
- title=_("Serial Nos Required"))
- if len(item.serial_no.split('\n')) == item.picked_qty:
+ frappe.throw(
+ _("Row #{0}: {1} does not have any available serial numbers in {2}").format(
+ frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse)
+ ),
+ title=_("Serial Nos Required"),
+ )
+ if len(item.serial_no.split("\n")) == item.picked_qty:
continue
- frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity')
- .format(frappe.bold(item.item_code), frappe.bold(item.idx)), title=_("Quantity Mismatch"))
+ frappe.throw(
+ _(
+ "For item {0} at row {1}, count of serial numbers does not match with the picked quantity"
+ ).format(frappe.bold(item.item_code), frappe.bold(item.idx)),
+ title=_("Quantity Mismatch"),
+ )
+
+ self.update_bundle_picked_qty()
+ self.update_sales_order_picking_status(update_sales_orders)
+
+ def before_cancel(self):
+ """Deduct picked qty on cancelling pick list"""
+ updated_sales_orders = set()
+
+ for item in self.get("locations"):
+ if item.sales_order_item:
+ self.update_sales_order_item(item, -1 * item.picked_qty, item.item_code)
+ updated_sales_orders.add(item.sales_order)
+
+ self.update_bundle_picked_qty()
+ self.update_sales_order_picking_status(updated_sales_orders)
+
+ def update_sales_order_item(self, item, picked_qty, item_code):
+ item_table = "Sales Order Item" if not item.product_bundle_item else "Packed Item"
+ stock_qty_field = "stock_qty" if not item.product_bundle_item else "qty"
+
+ already_picked, actual_qty = frappe.db.get_value(
+ item_table,
+ item.sales_order_item,
+ ["picked_qty", stock_qty_field],
+ )
+
+ if self.docstatus == 1:
+ if (((already_picked + picked_qty) / actual_qty) * 100) > (
+ 100 + flt(frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance"))
+ ):
+ frappe.throw(
+ _(
+ "You are picking more than required quantity for {}. Check if there is any other pick list created for {}"
+ ).format(item_code, item.sales_order)
+ )
+
+ frappe.db.set_value(item_table, item.sales_order_item, "picked_qty", already_picked + picked_qty)
+
+ @staticmethod
+ def update_sales_order_picking_status(sales_orders: Set[str]) -> None:
+ for sales_order in sales_orders:
+ if sales_order:
+ frappe.get_doc("Sales Order", sales_order).update_picking_status()
@frappe.whitelist()
def set_item_locations(self, save=False):
@@ -46,48 +117,55 @@ class PickList(Document):
from_warehouses = None
if self.parent_warehouse:
- from_warehouses = frappe.db.get_descendants('Warehouse', self.parent_warehouse)
+ from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse)
# Create replica before resetting, to handle empty table on update after submit.
- locations_replica = self.get('locations')
+ locations_replica = self.get("locations")
# reset
- self.delete_key('locations')
+ self.delete_key("locations")
for item_doc in items:
item_code = item_doc.item_code
- self.item_location_map.setdefault(item_code,
- get_available_item_locations(item_code, from_warehouses, self.item_count_map.get(item_code), self.company))
+ self.item_location_map.setdefault(
+ item_code,
+ get_available_item_locations(
+ item_code, from_warehouses, self.item_count_map.get(item_code), self.company
+ ),
+ )
- locations = get_items_with_location_and_quantity(item_doc, self.item_location_map, self.docstatus)
+ locations = get_items_with_location_and_quantity(
+ item_doc, self.item_location_map, self.docstatus
+ )
item_doc.idx = None
item_doc.name = None
for row in locations:
- row.update({
- 'picked_qty': row.stock_qty
- })
-
location = item_doc.as_dict()
location.update(row)
- self.append('locations', location)
+ self.append("locations", location)
# If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red
# and give feedback to the user. This is to avoid empty Pick Lists.
- if not self.get('locations') and self.docstatus == 1:
+ if not self.get("locations") and self.docstatus == 1:
for location in locations_replica:
location.stock_qty = 0
location.picked_qty = 0
- self.append('locations', location)
- frappe.msgprint(_("Please Restock Items and Update the Pick List to continue. To discontinue, cancel the Pick List."),
- title=_("Out of Stock"), indicator="red")
+ self.append("locations", location)
+ frappe.msgprint(
+ _(
+ "Please Restock Items and Update the Pick List to continue. To discontinue, cancel the Pick List."
+ ),
+ title=_("Out of Stock"),
+ indicator="red",
+ )
if save:
self.save()
def aggregate_item_qty(self):
- locations = self.get('locations')
+ locations = self.get("locations")
self.item_count_map = {}
# aggregate qty for same item
item_map = OrderedDict()
@@ -114,20 +192,20 @@ class PickList(Document):
return item_map.values()
def validate_for_qty(self):
- if self.purpose == "Material Transfer for Manufacture" \
- and (self.for_qty is None or self.for_qty == 0):
+ if self.purpose == "Material Transfer for Manufacture" and (
+ self.for_qty is None or self.for_qty == 0
+ ):
frappe.throw(_("Qty of Finished Goods Item should be greater than 0."))
def before_print(self, settings=None):
- if self.get("group_same_items"):
- self.group_similar_items()
+ self.group_similar_items()
def group_similar_items(self):
group_item_qty = defaultdict(float)
group_picked_qty = defaultdict(float)
for item in self.locations:
- group_item_qty[(item.item_code, item.warehouse)] += item.qty
+ group_item_qty[(item.item_code, item.warehouse)] += item.qty
group_picked_qty[(item.item_code, item.warehouse)] += item.picked_qty
duplicate_list = []
@@ -146,42 +224,104 @@ class PickList(Document):
for idx, item in enumerate(self.locations, start=1):
item.idx = idx
+ def update_bundle_picked_qty(self):
+ product_bundles = self._get_product_bundles()
+ product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values())
+
+ for so_row, item_code in product_bundles.items():
+ picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code])
+ item_table = "Sales Order Item"
+ already_picked = frappe.db.get_value(item_table, so_row, "picked_qty")
+ frappe.db.set_value(
+ item_table,
+ so_row,
+ "picked_qty",
+ already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)),
+ )
+
+ def _get_product_bundles(self) -> Dict[str, str]:
+ # Dict[so_item_row: item_code]
+ product_bundles = {}
+ for item in self.locations:
+ if not item.product_bundle_item:
+ continue
+ product_bundles[item.product_bundle_item] = frappe.db.get_value(
+ "Sales Order Item",
+ item.product_bundle_item,
+ "item_code",
+ )
+ return product_bundles
+
+ def _get_product_bundle_qty_map(self, bundles: List[str]) -> Dict[str, Dict[str, float]]:
+ # bundle_item_code: Dict[component, qty]
+ product_bundle_qty_map = {}
+ for bundle_item_code in bundles:
+ bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code})
+ product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items}
+ return product_bundle_qty_map
+
+ def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int:
+ """Compute how many full bundles can be created from picked items."""
+ precision = frappe.get_precision("Stock Ledger Entry", "qty_after_transaction")
+
+ possible_bundles = []
+ for item in self.locations:
+ if item.product_bundle_item != bundle_row:
+ continue
+
+ qty_in_bundle = bundle_items.get(item.item_code)
+ if qty_in_bundle:
+ possible_bundles.append(item.picked_qty / qty_in_bundle)
+ else:
+ possible_bundles.append(0)
+ return int(flt(min(possible_bundles), precision or 6))
+
def validate_item_locations(pick_list):
if not pick_list.locations:
frappe.throw(_("Add items in the Item Locations table"))
+
def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus):
available_locations = item_location_map.get(item_doc.item_code)
locations = []
# if stock qty is zero on submitted entry, show positive remaining qty to recalculate in case of restock.
- remaining_stock_qty = item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty
+ remaining_stock_qty = (
+ item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty
+ )
while remaining_stock_qty > 0 and available_locations:
item_location = available_locations.pop(0)
item_location = frappe._dict(item_location)
- stock_qty = remaining_stock_qty if item_location.qty >= remaining_stock_qty else item_location.qty
+ stock_qty = (
+ remaining_stock_qty if item_location.qty >= remaining_stock_qty else item_location.qty
+ )
qty = stock_qty / (item_doc.conversion_factor or 1)
- uom_must_be_whole_number = frappe.db.get_value('UOM', item_doc.uom, 'must_be_whole_number')
+ uom_must_be_whole_number = frappe.db.get_value("UOM", item_doc.uom, "must_be_whole_number")
if uom_must_be_whole_number:
qty = floor(qty)
stock_qty = qty * item_doc.conversion_factor
- if not stock_qty: break
+ if not stock_qty:
+ break
serial_nos = None
if item_location.serial_no:
- serial_nos = '\n'.join(item_location.serial_no[0: cint(stock_qty)])
+ serial_nos = "\n".join(item_location.serial_no[0 : cint(stock_qty)])
- locations.append(frappe._dict({
- 'qty': qty,
- 'stock_qty': stock_qty,
- 'warehouse': item_location.warehouse,
- 'serial_no': serial_nos,
- 'batch_no': item_location.batch_no
- }))
+ locations.append(
+ frappe._dict(
+ {
+ "qty": qty,
+ "stock_qty": stock_qty,
+ "warehouse": item_location.warehouse,
+ "serial_no": serial_nos,
+ "batch_no": item_location.batch_no,
+ }
+ )
+ )
remaining_stock_qty -= stock_qty
@@ -191,73 +331,87 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
item_location.qty = qty_diff
if item_location.serial_no:
# set remaining serial numbers
- item_location.serial_no = item_location.serial_no[-int(qty_diff):]
+ item_location.serial_no = item_location.serial_no[-int(qty_diff) :]
available_locations = [item_location] + available_locations
# update available locations for the item
item_location_map[item_doc.item_code] = available_locations
return locations
-def get_available_item_locations(item_code, from_warehouses, required_qty, company, ignore_validation=False):
+
+def get_available_item_locations(
+ item_code, from_warehouses, required_qty, company, ignore_validation=False
+):
locations = []
- has_serial_no = frappe.get_cached_value('Item', item_code, 'has_serial_no')
- has_batch_no = frappe.get_cached_value('Item', item_code, 'has_batch_no')
+ has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no")
+ has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no")
if has_batch_no and has_serial_no:
- locations = get_available_item_locations_for_serial_and_batched_item(item_code, from_warehouses, required_qty, company)
+ locations = get_available_item_locations_for_serial_and_batched_item(
+ item_code, from_warehouses, required_qty, company
+ )
elif has_serial_no:
- locations = get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty, company)
+ locations = get_available_item_locations_for_serialized_item(
+ item_code, from_warehouses, required_qty, company
+ )
elif has_batch_no:
- locations = get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company)
+ locations = get_available_item_locations_for_batched_item(
+ item_code, from_warehouses, required_qty, company
+ )
else:
- locations = get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company)
+ locations = get_available_item_locations_for_other_item(
+ item_code, from_warehouses, required_qty, company
+ )
- total_qty_available = sum(location.get('qty') for location in locations)
+ total_qty_available = sum(location.get("qty") for location in locations)
remaining_qty = required_qty - total_qty_available
if remaining_qty > 0 and not ignore_validation:
- frappe.msgprint(_('{0} units of Item {1} is not available.')
- .format(remaining_qty, frappe.get_desk_link('Item', item_code)),
- title=_("Insufficient Stock"))
+ frappe.msgprint(
+ _("{0} units of Item {1} is not available.").format(
+ remaining_qty, frappe.get_desk_link("Item", item_code)
+ ),
+ title=_("Insufficient Stock"),
+ )
return locations
-def get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty, company):
- filters = frappe._dict({
- 'item_code': item_code,
- 'company': company,
- 'warehouse': ['!=', '']
- })
+def get_available_item_locations_for_serialized_item(
+ item_code, from_warehouses, required_qty, company
+):
+ filters = frappe._dict({"item_code": item_code, "company": company, "warehouse": ["!=", ""]})
if from_warehouses:
- filters.warehouse = ['in', from_warehouses]
+ filters.warehouse = ["in", from_warehouses]
- serial_nos = frappe.get_all('Serial No',
- fields=['name', 'warehouse'],
+ serial_nos = frappe.get_all(
+ "Serial No",
+ fields=["name", "warehouse"],
filters=filters,
limit=required_qty,
- order_by='purchase_date',
- as_list=1)
+ order_by="purchase_date",
+ as_list=1,
+ )
warehouse_serial_nos_map = frappe._dict()
for serial_no, warehouse in serial_nos:
warehouse_serial_nos_map.setdefault(warehouse, []).append(serial_no)
locations = []
- for warehouse, serial_nos in iteritems(warehouse_serial_nos_map):
- locations.append({
- 'qty': len(serial_nos),
- 'warehouse': warehouse,
- 'serial_no': serial_nos
- })
+ for warehouse, serial_nos in warehouse_serial_nos_map.items():
+ locations.append({"qty": len(serial_nos), "warehouse": warehouse, "serial_no": serial_nos})
return locations
-def get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company):
- warehouse_condition = 'and warehouse in %(warehouses)s' if from_warehouses else ''
- batch_locations = frappe.db.sql("""
+
+def get_available_item_locations_for_batched_item(
+ item_code, from_warehouses, required_qty, company
+):
+ warehouse_condition = "and warehouse in %(warehouses)s" if from_warehouses else ""
+ batch_locations = frappe.db.sql(
+ """
SELECT
sle.`warehouse`,
sle.`batch_no`,
@@ -273,118 +427,171 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re
and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s
{warehouse_condition}
GROUP BY
- `warehouse`,
- `batch_no`,
- `item_code`
+ sle.`warehouse`,
+ sle.`batch_no`,
+ sle.`item_code`
HAVING `qty` > 0
ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation`
- """.format(warehouse_condition=warehouse_condition), { #nosec
- 'item_code': item_code,
- 'company': company,
- 'today': today(),
- 'warehouses': from_warehouses
- }, as_dict=1)
+ """.format(
+ warehouse_condition=warehouse_condition
+ ),
+ { # nosec
+ "item_code": item_code,
+ "company": company,
+ "today": today(),
+ "warehouses": from_warehouses,
+ },
+ as_dict=1,
+ )
return batch_locations
-def get_available_item_locations_for_serial_and_batched_item(item_code, from_warehouses, required_qty, company):
- # Get batch nos by FIFO
- locations = get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company)
- filters = frappe._dict({
- 'item_code': item_code,
- 'company': company,
- 'warehouse': ['!=', ''],
- 'batch_no': ''
- })
+def get_available_item_locations_for_serial_and_batched_item(
+ item_code, from_warehouses, required_qty, company
+):
+ # Get batch nos by FIFO
+ locations = get_available_item_locations_for_batched_item(
+ item_code, from_warehouses, required_qty, company
+ )
+
+ filters = frappe._dict(
+ {"item_code": item_code, "company": company, "warehouse": ["!=", ""], "batch_no": ""}
+ )
# Get Serial Nos by FIFO for Batch No
for location in locations:
filters.batch_no = location.batch_no
filters.warehouse = location.warehouse
- location.qty = required_qty if location.qty > required_qty else location.qty # if extra qty in batch
+ location.qty = (
+ required_qty if location.qty > required_qty else location.qty
+ ) # if extra qty in batch
- serial_nos = frappe.get_list('Serial No',
- fields=['name'],
- filters=filters,
- limit=location.qty,
- order_by='purchase_date')
+ serial_nos = frappe.get_list(
+ "Serial No", fields=["name"], filters=filters, limit=location.qty, order_by="purchase_date"
+ )
serial_nos = [sn.name for sn in serial_nos]
location.serial_no = serial_nos
return locations
+
def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company):
# gets all items available in different warehouses
- warehouses = [x.get('name') for x in frappe.get_list("Warehouse", {'company': company}, "name")]
+ warehouses = [x.get("name") for x in frappe.get_list("Warehouse", {"company": company}, "name")]
- filters = frappe._dict({
- 'item_code': item_code,
- 'warehouse': ['in', warehouses],
- 'actual_qty': ['>', 0]
- })
+ filters = frappe._dict(
+ {"item_code": item_code, "warehouse": ["in", warehouses], "actual_qty": [">", 0]}
+ )
if from_warehouses:
- filters.warehouse = ['in', from_warehouses]
+ filters.warehouse = ["in", from_warehouses]
- item_locations = frappe.get_all('Bin',
- fields=['warehouse', 'actual_qty as qty'],
+ item_locations = frappe.get_all(
+ "Bin",
+ fields=["warehouse", "actual_qty as qty"],
filters=filters,
limit=required_qty,
- order_by='creation')
+ order_by="creation",
+ )
return item_locations
@frappe.whitelist()
def create_delivery_note(source_name, target_doc=None):
- pick_list = frappe.get_doc('Pick List', source_name)
+ pick_list = frappe.get_doc("Pick List", source_name)
validate_item_locations(pick_list)
-
- sales_orders = [d.sales_order for d in pick_list.locations if d.sales_order]
- sales_orders = set(sales_orders)
-
+ sales_dict = dict()
+ sales_orders = []
delivery_note = None
- for sales_order in sales_orders:
- delivery_note = create_delivery_note_from_sales_order(sales_order,
- delivery_note, skip_item_mapping=True)
+ for location in pick_list.locations:
+ if location.sales_order:
+ sales_orders.append(
+ frappe.db.get_value(
+ "Sales Order", location.sales_order, ["customer", "name as sales_order"], as_dict=True
+ )
+ )
- # map rows without sales orders as well
- if not delivery_note:
- delivery_note = frappe.new_doc("Delivery Note")
+ for customer, rows in groupby(sales_orders, key=lambda so: so["customer"]):
+ sales_dict[customer] = {row.sales_order for row in rows}
- item_table_mapper = {
- 'doctype': 'Delivery Note Item',
- 'field_map': {
- 'rate': 'rate',
- 'name': 'so_detail',
- 'parent': 'against_sales_order',
- },
- 'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
- }
+ if sales_dict:
+ delivery_note = create_dn_with_so(sales_dict, pick_list)
+
+ if not all(item.sales_order for item in pick_list.locations):
+ delivery_note = create_dn_wo_so(pick_list)
+
+ frappe.msgprint(_("Delivery Note(s) created for the Pick List"))
+ return delivery_note
+
+
+def create_dn_wo_so(pick_list):
+ delivery_note = frappe.new_doc("Delivery Note")
item_table_mapper_without_so = {
- 'doctype': 'Delivery Note Item',
- 'field_map': {
- 'rate': 'rate',
- 'name': 'name',
- 'parent': '',
- }
+ "doctype": "Delivery Note Item",
+ "field_map": {
+ "rate": "rate",
+ "name": "name",
+ "parent": "",
+ },
+ }
+ map_pl_locations(pick_list, item_table_mapper_without_so, delivery_note)
+ delivery_note.insert(ignore_mandatory=True)
+
+ return delivery_note
+
+
+def create_dn_with_so(sales_dict, pick_list):
+ delivery_note = None
+
+ item_table_mapper = {
+ "doctype": "Delivery Note Item",
+ "field_map": {
+ "rate": "rate",
+ "name": "so_detail",
+ "parent": "against_sales_order",
+ },
+ "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
+ and doc.delivered_by_supplier != 1,
}
+ for customer in sales_dict:
+ for so in sales_dict[customer]:
+ delivery_note = None
+ delivery_note = create_delivery_note_from_sales_order(so, delivery_note, skip_item_mapping=True)
+ break
+ if delivery_note:
+ # map all items of all sales orders of that customer
+ for so in sales_dict[customer]:
+ map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
+ delivery_note.flags.ignore_mandatory = True
+ delivery_note.insert()
+ update_packed_item_details(pick_list, delivery_note)
+ delivery_note.save()
+
+ return delivery_note
+
+
+def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
+
for location in pick_list.locations:
+ if location.sales_order != sales_order or location.product_bundle_item:
+ continue
+
if location.sales_order_item:
- sales_order_item = frappe.get_cached_doc('Sales Order Item', {'name':location.sales_order_item})
+ sales_order_item = frappe.get_doc("Sales Order Item", location.sales_order_item)
else:
sales_order_item = None
- source_doc, table_mapper = [sales_order_item, item_table_mapper] if sales_order_item \
- else [location, item_table_mapper_without_so]
+ source_doc = sales_order_item or location
- dn_item = map_child_doc(source_doc, delivery_note, table_mapper)
+ dn_item = map_child_doc(source_doc, delivery_note, item_mapper)
if dn_item:
+ dn_item.pick_list_item = location.name
dn_item.warehouse = location.warehouse
dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
dn_item.batch_no = location.batch_no
@@ -392,29 +599,74 @@ def create_delivery_note(source_name, target_doc=None):
update_delivery_note_item(source_doc, dn_item, delivery_note)
+ add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper)
set_delivery_note_missing_values(delivery_note)
delivery_note.pick_list = pick_list.name
- delivery_note.customer = pick_list.customer if pick_list.customer else None
+ delivery_note.company = pick_list.company
+ delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
+
+
+def add_product_bundles_to_delivery_note(
+ pick_list: "PickList", delivery_note, item_mapper
+) -> None:
+ """Add product bundles found in pick list to delivery note.
+
+ When mapping pick list items, the bundle item itself isn't part of the
+ locations. Dynamically fetch and add parent bundle item into DN."""
+ product_bundles = pick_list._get_product_bundles()
+ product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values())
+
+ for so_row, item_code in product_bundles.items():
+ sales_order_item = frappe.get_doc("Sales Order Item", so_row)
+ dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper)
+ dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle(
+ so_row, product_bundle_qty_map[item_code]
+ )
+ update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note)
+
+
+def update_packed_item_details(pick_list: "PickList", delivery_note) -> None:
+ """Update stock details on packed items table of delivery note."""
+
+ def _find_so_row(packed_item):
+ for item in delivery_note.items:
+ if packed_item.parent_detail_docname == item.name:
+ return item.so_detail
+
+ def _find_pick_list_location(bundle_row, packed_item):
+ if not bundle_row:
+ return
+ for loc in pick_list.locations:
+ if loc.product_bundle_item == bundle_row and loc.item_code == packed_item.item_code:
+ return loc
+
+ for packed_item in delivery_note.packed_items:
+ so_row = _find_so_row(packed_item)
+ location = _find_pick_list_location(so_row, packed_item)
+ if not location:
+ continue
+ packed_item.warehouse = location.warehouse
+ packed_item.batch_no = location.batch_no
+ packed_item.serial_no = location.serial_no
- return delivery_note
@frappe.whitelist()
def create_stock_entry(pick_list):
pick_list = frappe.get_doc(json.loads(pick_list))
validate_item_locations(pick_list)
- if stock_entry_exists(pick_list.get('name')):
- return frappe.msgprint(_('Stock Entry has been already created against this Pick List'))
+ if stock_entry_exists(pick_list.get("name")):
+ return frappe.msgprint(_("Stock Entry has been already created against this Pick List"))
- stock_entry = frappe.new_doc('Stock Entry')
- stock_entry.pick_list = pick_list.get('name')
- stock_entry.purpose = pick_list.get('purpose')
+ stock_entry = frappe.new_doc("Stock Entry")
+ stock_entry.pick_list = pick_list.get("name")
+ stock_entry.purpose = pick_list.get("purpose")
stock_entry.set_stock_entry_type()
- if pick_list.get('work_order'):
+ if pick_list.get("work_order"):
stock_entry = update_stock_entry_based_on_work_order(pick_list, stock_entry)
- elif pick_list.get('material_request'):
+ elif pick_list.get("material_request"):
stock_entry = update_stock_entry_based_on_material_request(pick_list, stock_entry)
else:
stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry)
@@ -424,9 +676,11 @@ def create_stock_entry(pick_list):
return stock_entry.as_dict()
+
@frappe.whitelist()
def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filters, as_dict):
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
SELECT
`name`, `company`, `planned_start_date`
FROM
@@ -442,25 +696,27 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte
LIMIT
%(start)s, %(page_length)s""",
{
- 'txt': "%%%s%%" % txt,
- '_txt': txt.replace('%', ''),
- 'start': start,
- 'page_length': frappe.utils.cint(page_length),
- 'company': filters.get('company')
- }, as_dict=as_dict)
+ "txt": "%%%s%%" % txt,
+ "_txt": txt.replace("%", ""),
+ "start": start,
+ "page_length": frappe.utils.cint(page_length),
+ "company": filters.get("company"),
+ },
+ as_dict=as_dict,
+ )
+
@frappe.whitelist()
def target_document_exists(pick_list_name, purpose):
- if purpose == 'Delivery':
- return frappe.db.exists('Delivery Note', {
- 'pick_list': pick_list_name
- })
+ if purpose == "Delivery":
+ return frappe.db.exists("Delivery Note", {"pick_list": pick_list_name})
return stock_entry_exists(pick_list_name)
+
@frappe.whitelist()
def get_item_details(item_code, uom=None):
- details = frappe.db.get_value('Item', item_code, ['stock_uom', 'name'], as_dict=1)
+ details = frappe.db.get_value("Item", item_code, ["stock_uom", "name"], as_dict=1)
details.uom = uom or details.stock_uom
if uom:
details.update(get_conversion_factor(item_code, uom))
@@ -469,37 +725,37 @@ def get_item_details(item_code, uom=None):
def update_delivery_note_item(source, target, delivery_note):
- cost_center = frappe.db.get_value('Project', delivery_note.project, 'cost_center')
+ cost_center = frappe.db.get_value("Project", delivery_note.project, "cost_center")
if not cost_center:
- cost_center = get_cost_center(source.item_code, 'Item', delivery_note.company)
+ cost_center = get_cost_center(source.item_code, "Item", delivery_note.company)
if not cost_center:
- cost_center = get_cost_center(source.item_group, 'Item Group', delivery_note.company)
+ cost_center = get_cost_center(source.item_group, "Item Group", delivery_note.company)
target.cost_center = cost_center
+
def get_cost_center(for_item, from_doctype, company):
- '''Returns Cost Center for Item or Item Group'''
- return frappe.db.get_value('Item Default',
- fieldname=['buying_cost_center'],
- filters={
- 'parent': for_item,
- 'parenttype': from_doctype,
- 'company': company
- })
+ """Returns Cost Center for Item or Item Group"""
+ return frappe.db.get_value(
+ "Item Default",
+ fieldname=["buying_cost_center"],
+ filters={"parent": for_item, "parenttype": from_doctype, "company": company},
+ )
+
def set_delivery_note_missing_values(target):
- target.run_method('set_missing_values')
- target.run_method('set_po_nos')
- target.run_method('calculate_taxes_and_totals')
+ target.run_method("set_missing_values")
+ target.run_method("set_po_nos")
+ target.run_method("calculate_taxes_and_totals")
+
def stock_entry_exists(pick_list_name):
- return frappe.db.exists('Stock Entry', {
- 'pick_list': pick_list_name
- })
+ return frappe.db.exists("Stock Entry", {"pick_list": pick_list_name})
+
def update_stock_entry_based_on_work_order(pick_list, stock_entry):
- work_order = frappe.get_doc("Work Order", pick_list.get('work_order'))
+ work_order = frappe.get_doc("Work Order", pick_list.get("work_order"))
stock_entry.work_order = work_order.name
stock_entry.company = work_order.company
@@ -508,10 +764,11 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry):
stock_entry.use_multi_level_bom = work_order.use_multi_level_bom
stock_entry.fg_completed_qty = pick_list.for_qty
if work_order.bom_no:
- stock_entry.inspection_required = frappe.db.get_value('BOM',
- work_order.bom_no, 'inspection_required')
+ stock_entry.inspection_required = frappe.db.get_value(
+ "BOM", work_order.bom_no, "inspection_required"
+ )
- is_wip_warehouse_group = frappe.db.get_value('Warehouse', work_order.wip_warehouse, 'is_group')
+ is_wip_warehouse_group = frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group")
if not (is_wip_warehouse_group and work_order.skip_transfer):
wip_warehouse = work_order.wip_warehouse
else:
@@ -525,32 +782,36 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry):
update_common_item_properties(item, location)
item.t_warehouse = wip_warehouse
- stock_entry.append('items', item)
+ stock_entry.append("items", item)
return stock_entry
+
def update_stock_entry_based_on_material_request(pick_list, stock_entry):
for location in pick_list.locations:
target_warehouse = None
if location.material_request_item:
- target_warehouse = frappe.get_value('Material Request Item',
- location.material_request_item, 'warehouse')
+ target_warehouse = frappe.get_value(
+ "Material Request Item", location.material_request_item, "warehouse"
+ )
item = frappe._dict()
update_common_item_properties(item, location)
item.t_warehouse = target_warehouse
- stock_entry.append('items', item)
+ stock_entry.append("items", item)
return stock_entry
+
def update_stock_entry_items_with_no_reference(pick_list, stock_entry):
for location in pick_list.locations:
item = frappe._dict()
update_common_item_properties(item, location)
- stock_entry.append('items', item)
+ stock_entry.append("items", item)
return stock_entry
+
def update_common_item_properties(item, location):
item.item_code = location.item_code
item.s_warehouse = location.warehouse
diff --git a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py
index d19bedeeafe..92e57bed220 100644
--- a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py
+++ b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py
@@ -1,11 +1,7 @@
-
-
def get_data():
return {
- 'fieldname': 'pick_list',
- 'transactions': [
- {
- 'items': ['Stock Entry', 'Delivery Note']
- },
- ]
+ "fieldname": "pick_list",
+ "transactions": [
+ {"items": ["Stock Entry", "Delivery Note"]},
+ ],
}
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index 41e3150f0d7..e8cebc8e622 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -3,162 +3,193 @@
import frappe
from frappe import _dict
+from frappe.tests.utils import FrappeTestCase
-test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch']
-
-from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.selling.doctype.sales_order.sales_order import create_pick_list
+from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+from erpnext.stock.doctype.item.test_item import create_item, make_item
+from erpnext.stock.doctype.packed_item.test_packed_item import create_product_bundle
from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
EmptyStockReconciliationItemsError,
)
-from erpnext.tests.utils import ERPNextTestCase
+
+test_dependencies = ["Item", "Sales Invoice", "Stock Entry", "Batch"]
-class TestPickList(ERPNextTestCase):
-
+class TestPickList(FrappeTestCase):
def test_pick_list_picks_warehouse_for_each_item(self):
try:
- frappe.get_doc({
- 'doctype': 'Stock Reconciliation',
- 'company': '_Test Company',
- 'purpose': 'Opening Stock',
- 'expense_account': 'Temporary Opening - _TC',
- 'items': [{
- 'item_code': '_Test Item',
- 'warehouse': '_Test Warehouse - _TC',
- 'valuation_rate': 100,
- 'qty': 5
- }]
- }).submit()
+ frappe.get_doc(
+ {
+ "doctype": "Stock Reconciliation",
+ "company": "_Test Company",
+ "purpose": "Opening Stock",
+ "expense_account": "Temporary Opening - _TC",
+ "items": [
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "valuation_rate": 100,
+ "qty": 5,
+ }
+ ],
+ }
+ ).submit()
except EmptyStockReconciliationItemsError:
pass
- pick_list = frappe.get_doc({
- 'doctype': 'Pick List',
- 'company': '_Test Company',
- 'customer': '_Test Customer',
- 'items_based_on': 'Sales Order',
- 'purpose': 'Delivery',
- 'locations': [{
- 'item_code': '_Test Item',
- 'qty': 5,
- 'stock_qty': 5,
- 'conversion_factor': 1,
- 'sales_order': '_T-Sales Order-1',
- 'sales_order_item': '_T-Sales Order-1_item',
- }]
- })
+ pick_list = frappe.get_doc(
+ {
+ "doctype": "Pick List",
+ "company": "_Test Company",
+ "customer": "_Test Customer",
+ "items_based_on": "Sales Order",
+ "purpose": "Delivery",
+ "locations": [
+ {
+ "item_code": "_Test Item",
+ "qty": 5,
+ "stock_qty": 5,
+ "conversion_factor": 1,
+ "sales_order": "_T-Sales Order-1",
+ "sales_order_item": "_T-Sales Order-1_item",
+ }
+ ],
+ }
+ )
pick_list.set_item_locations()
- self.assertEqual(pick_list.locations[0].item_code, '_Test Item')
- self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC')
+ self.assertEqual(pick_list.locations[0].item_code, "_Test Item")
+ self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC")
self.assertEqual(pick_list.locations[0].qty, 5)
def test_pick_list_splits_row_according_to_warehouse_availability(self):
try:
- frappe.get_doc({
- 'doctype': 'Stock Reconciliation',
- 'company': '_Test Company',
- 'purpose': 'Opening Stock',
- 'expense_account': 'Temporary Opening - _TC',
- 'items': [{
- 'item_code': '_Test Item Warehouse Group Wise Reorder',
- 'warehouse': '_Test Warehouse Group-C1 - _TC',
- 'valuation_rate': 100,
- 'qty': 5
- }]
- }).submit()
+ frappe.get_doc(
+ {
+ "doctype": "Stock Reconciliation",
+ "company": "_Test Company",
+ "purpose": "Opening Stock",
+ "expense_account": "Temporary Opening - _TC",
+ "items": [
+ {
+ "item_code": "_Test Item Warehouse Group Wise Reorder",
+ "warehouse": "_Test Warehouse Group-C1 - _TC",
+ "valuation_rate": 100,
+ "qty": 5,
+ }
+ ],
+ }
+ ).submit()
except EmptyStockReconciliationItemsError:
pass
try:
- frappe.get_doc({
- 'doctype': 'Stock Reconciliation',
- 'company': '_Test Company',
- 'purpose': 'Opening Stock',
- 'expense_account': 'Temporary Opening - _TC',
- 'items': [{
- 'item_code': '_Test Item Warehouse Group Wise Reorder',
- 'warehouse': '_Test Warehouse 2 - _TC',
- 'valuation_rate': 400,
- 'qty': 10
- }]
- }).submit()
+ frappe.get_doc(
+ {
+ "doctype": "Stock Reconciliation",
+ "company": "_Test Company",
+ "purpose": "Opening Stock",
+ "expense_account": "Temporary Opening - _TC",
+ "items": [
+ {
+ "item_code": "_Test Item Warehouse Group Wise Reorder",
+ "warehouse": "_Test Warehouse 2 - _TC",
+ "valuation_rate": 400,
+ "qty": 10,
+ }
+ ],
+ }
+ ).submit()
except EmptyStockReconciliationItemsError:
pass
- pick_list = frappe.get_doc({
- 'doctype': 'Pick List',
- 'company': '_Test Company',
- 'customer': '_Test Customer',
- 'items_based_on': 'Sales Order',
- 'purpose': 'Delivery',
- 'locations': [{
- 'item_code': '_Test Item Warehouse Group Wise Reorder',
- 'qty': 1000,
- 'stock_qty': 1000,
- 'conversion_factor': 1,
- 'sales_order': '_T-Sales Order-1',
- 'sales_order_item': '_T-Sales Order-1_item',
- }]
- })
+ pick_list = frappe.get_doc(
+ {
+ "doctype": "Pick List",
+ "company": "_Test Company",
+ "customer": "_Test Customer",
+ "items_based_on": "Sales Order",
+ "purpose": "Delivery",
+ "locations": [
+ {
+ "item_code": "_Test Item Warehouse Group Wise Reorder",
+ "qty": 1000,
+ "stock_qty": 1000,
+ "conversion_factor": 1,
+ "sales_order": "_T-Sales Order-1",
+ "sales_order_item": "_T-Sales Order-1_item",
+ }
+ ],
+ }
+ )
pick_list.set_item_locations()
- self.assertEqual(pick_list.locations[0].item_code, '_Test Item Warehouse Group Wise Reorder')
- self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse Group-C1 - _TC')
+ self.assertEqual(pick_list.locations[0].item_code, "_Test Item Warehouse Group Wise Reorder")
+ self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse Group-C1 - _TC")
self.assertEqual(pick_list.locations[0].qty, 5)
- self.assertEqual(pick_list.locations[1].item_code, '_Test Item Warehouse Group Wise Reorder')
- self.assertEqual(pick_list.locations[1].warehouse, '_Test Warehouse 2 - _TC')
+ self.assertEqual(pick_list.locations[1].item_code, "_Test Item Warehouse Group Wise Reorder")
+ self.assertEqual(pick_list.locations[1].warehouse, "_Test Warehouse 2 - _TC")
self.assertEqual(pick_list.locations[1].qty, 10)
def test_pick_list_shows_serial_no_for_serialized_item(self):
- stock_reconciliation = frappe.get_doc({
- 'doctype': 'Stock Reconciliation',
- 'purpose': 'Stock Reconciliation',
- 'company': '_Test Company',
- 'items': [{
- 'item_code': '_Test Serialized Item',
- 'warehouse': '_Test Warehouse - _TC',
- 'valuation_rate': 100,
- 'qty': 5,
- 'serial_no': '123450\n123451\n123452\n123453\n123454'
- }]
- })
+ stock_reconciliation = frappe.get_doc(
+ {
+ "doctype": "Stock Reconciliation",
+ "purpose": "Stock Reconciliation",
+ "company": "_Test Company",
+ "items": [
+ {
+ "item_code": "_Test Serialized Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "valuation_rate": 100,
+ "qty": 5,
+ "serial_no": "123450\n123451\n123452\n123453\n123454",
+ }
+ ],
+ }
+ )
try:
stock_reconciliation.submit()
except EmptyStockReconciliationItemsError:
pass
- pick_list = frappe.get_doc({
- 'doctype': 'Pick List',
- 'company': '_Test Company',
- 'customer': '_Test Customer',
- 'items_based_on': 'Sales Order',
- 'purpose': 'Delivery',
- 'locations': [{
- 'item_code': '_Test Serialized Item',
- 'qty': 1000,
- 'stock_qty': 1000,
- 'conversion_factor': 1,
- 'sales_order': '_T-Sales Order-1',
- 'sales_order_item': '_T-Sales Order-1_item',
- }]
- })
+ pick_list = frappe.get_doc(
+ {
+ "doctype": "Pick List",
+ "company": "_Test Company",
+ "customer": "_Test Customer",
+ "items_based_on": "Sales Order",
+ "purpose": "Delivery",
+ "locations": [
+ {
+ "item_code": "_Test Serialized Item",
+ "qty": 1000,
+ "stock_qty": 1000,
+ "conversion_factor": 1,
+ "sales_order": "_T-Sales Order-1",
+ "sales_order_item": "_T-Sales Order-1_item",
+ }
+ ],
+ }
+ )
pick_list.set_item_locations()
- self.assertEqual(pick_list.locations[0].item_code, '_Test Serialized Item')
- self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC')
+ self.assertEqual(pick_list.locations[0].item_code, "_Test Serialized Item")
+ self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC")
self.assertEqual(pick_list.locations[0].qty, 5)
- self.assertEqual(pick_list.locations[0].serial_no, '123450\n123451\n123452\n123453\n123454')
+ self.assertEqual(pick_list.locations[0].serial_no, "123450\n123451\n123452\n123453\n123454")
def test_pick_list_shows_batch_no_for_batched_item(self):
# check if oldest batch no is picked
- item = frappe.db.exists("Item", {'item_name': 'Batched Item'})
+ item = frappe.db.exists("Item", {"item_name": "Batched Item"})
if not item:
item = create_item("Batched Item")
item.has_batch_no = 1
@@ -166,7 +197,7 @@ class TestPickList(ERPNextTestCase):
item.batch_number_series = "B-BATCH-.##"
item.save()
else:
- item = frappe.get_doc("Item", {'item_name': 'Batched Item'})
+ item = frappe.get_doc("Item", {"item_name": "Batched Item"})
pr1 = make_purchase_receipt(item_code="Batched Item", qty=1, rate=100.0)
@@ -175,28 +206,30 @@ class TestPickList(ERPNextTestCase):
pr2 = make_purchase_receipt(item_code="Batched Item", qty=2, rate=100.0)
- pick_list = frappe.get_doc({
- 'doctype': 'Pick List',
- 'company': '_Test Company',
- 'purpose': 'Material Transfer',
- 'locations': [{
- 'item_code': 'Batched Item',
- 'qty': 1,
- 'stock_qty': 1,
- 'conversion_factor': 1,
- }]
- })
+ pick_list = frappe.get_doc(
+ {
+ "doctype": "Pick List",
+ "company": "_Test Company",
+ "purpose": "Material Transfer",
+ "locations": [
+ {
+ "item_code": "Batched Item",
+ "qty": 1,
+ "stock_qty": 1,
+ "conversion_factor": 1,
+ }
+ ],
+ }
+ )
pick_list.set_item_locations()
-
self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no)
pr1.cancel()
pr2.cancel()
-
def test_pick_list_for_batched_and_serialised_item(self):
# check if oldest batch no and serial nos are picked
- item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'})
+ item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})
if not item:
item = create_item("Batched and Serialised Item")
item.has_batch_no = 1
@@ -206,7 +239,7 @@ class TestPickList(ERPNextTestCase):
item.serial_no_series = "S-.####"
item.save()
else:
- item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'})
+ item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"})
pr1 = make_purchase_receipt(item_code="Batched and Serialised Item", qty=2, rate=100.0)
@@ -216,17 +249,21 @@ class TestPickList(ERPNextTestCase):
pr2 = make_purchase_receipt(item_code="Batched and Serialised Item", qty=2, rate=100.0)
- pick_list = frappe.get_doc({
- 'doctype': 'Pick List',
- 'company': '_Test Company',
- 'purpose': 'Material Transfer',
- 'locations': [{
- 'item_code': 'Batched and Serialised Item',
- 'qty': 2,
- 'stock_qty': 2,
- 'conversion_factor': 1,
- }]
- })
+ pick_list = frappe.get_doc(
+ {
+ "doctype": "Pick List",
+ "company": "_Test Company",
+ "purpose": "Material Transfer",
+ "locations": [
+ {
+ "item_code": "Batched and Serialised Item",
+ "qty": 2,
+ "stock_qty": 2,
+ "conversion_factor": 1,
+ }
+ ],
+ }
+ )
pick_list.set_item_locations()
self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no)
@@ -237,64 +274,71 @@ class TestPickList(ERPNextTestCase):
def test_pick_list_for_items_from_multiple_sales_orders(self):
try:
- frappe.get_doc({
- 'doctype': 'Stock Reconciliation',
- 'company': '_Test Company',
- 'purpose': 'Opening Stock',
- 'expense_account': 'Temporary Opening - _TC',
- 'items': [{
- 'item_code': '_Test Item',
- 'warehouse': '_Test Warehouse - _TC',
- 'valuation_rate': 100,
- 'qty': 10
- }]
- }).submit()
+ frappe.get_doc(
+ {
+ "doctype": "Stock Reconciliation",
+ "company": "_Test Company",
+ "purpose": "Opening Stock",
+ "expense_account": "Temporary Opening - _TC",
+ "items": [
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "valuation_rate": 100,
+ "qty": 10,
+ }
+ ],
+ }
+ ).submit()
except EmptyStockReconciliationItemsError:
pass
- sales_order = frappe.get_doc({
- 'doctype': "Sales Order",
- 'customer': '_Test Customer',
- 'company': '_Test Company',
- 'items': [{
- 'item_code': '_Test Item',
- 'qty': 10,
- 'delivery_date': frappe.utils.today()
- }],
- })
+ sales_order = frappe.get_doc(
+ {
+ "doctype": "Sales Order",
+ "customer": "_Test Customer",
+ "company": "_Test Company",
+ "items": [{"item_code": "_Test Item", "qty": 10, "delivery_date": frappe.utils.today()}],
+ }
+ )
sales_order.submit()
- pick_list = frappe.get_doc({
- 'doctype': 'Pick List',
- 'company': '_Test Company',
- 'customer': '_Test Customer',
- 'items_based_on': 'Sales Order',
- 'purpose': 'Delivery',
- 'locations': [{
- 'item_code': '_Test Item',
- 'qty': 5,
- 'stock_qty': 5,
- 'conversion_factor': 1,
- 'sales_order': '_T-Sales Order-1',
- 'sales_order_item': '_T-Sales Order-1_item',
- }, {
- 'item_code': '_Test Item',
- 'qty': 5,
- 'stock_qty': 5,
- 'conversion_factor': 1,
- 'sales_order': sales_order.name,
- 'sales_order_item': sales_order.items[0].name,
- }]
- })
+ pick_list = frappe.get_doc(
+ {
+ "doctype": "Pick List",
+ "company": "_Test Company",
+ "customer": "_Test Customer",
+ "items_based_on": "Sales Order",
+ "purpose": "Delivery",
+ "locations": [
+ {
+ "item_code": "_Test Item",
+ "qty": 5,
+ "stock_qty": 5,
+ "conversion_factor": 1,
+ "sales_order": "_T-Sales Order-1",
+ "sales_order_item": "_T-Sales Order-1_item",
+ },
+ {
+ "item_code": "_Test Item",
+ "qty": 5,
+ "stock_qty": 5,
+ "conversion_factor": 1,
+ "sales_order": sales_order.name,
+ "sales_order_item": sales_order.items[0].name,
+ },
+ ],
+ }
+ )
pick_list.set_item_locations()
- self.assertEqual(pick_list.locations[0].item_code, '_Test Item')
- self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC')
+ self.assertEqual(pick_list.locations[0].item_code, "_Test Item")
+ self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC")
self.assertEqual(pick_list.locations[0].qty, 5)
- self.assertEqual(pick_list.locations[0].sales_order_item, '_T-Sales Order-1_item')
+ self.assertEqual(pick_list.locations[0].sales_order_item, "_T-Sales Order-1_item")
- self.assertEqual(pick_list.locations[1].item_code, '_Test Item')
- self.assertEqual(pick_list.locations[1].warehouse, '_Test Warehouse - _TC')
+ self.assertEqual(pick_list.locations[1].item_code, "_Test Item")
+ self.assertEqual(pick_list.locations[1].warehouse, "_Test Warehouse - _TC")
self.assertEqual(pick_list.locations[1].qty, 5)
self.assertEqual(pick_list.locations[1].sales_order_item, sales_order.items[0].name)
@@ -302,46 +346,57 @@ class TestPickList(ERPNextTestCase):
purchase_receipt = make_purchase_receipt(item_code="_Test Item", qty=10)
purchase_receipt.submit()
- sales_order = frappe.get_doc({
- 'doctype': 'Sales Order',
- 'customer': '_Test Customer',
- 'company': '_Test Company',
- 'items': [{
- 'item_code': '_Test Item',
- 'qty': 1,
- 'conversion_factor': 5,
- 'delivery_date': frappe.utils.today()
- }, {
- 'item_code': '_Test Item',
- 'qty': 1,
- 'conversion_factor': 1,
- 'delivery_date': frappe.utils.today()
- }],
- }).insert()
+ sales_order = frappe.get_doc(
+ {
+ "doctype": "Sales Order",
+ "customer": "_Test Customer",
+ "company": "_Test Company",
+ "items": [
+ {
+ "item_code": "_Test Item",
+ "qty": 1,
+ "conversion_factor": 5,
+ "stock_qty": 5,
+ "delivery_date": frappe.utils.today(),
+ },
+ {
+ "item_code": "_Test Item",
+ "qty": 1,
+ "conversion_factor": 1,
+ "delivery_date": frappe.utils.today(),
+ },
+ ],
+ }
+ ).insert()
sales_order.submit()
- pick_list = frappe.get_doc({
- 'doctype': 'Pick List',
- 'company': '_Test Company',
- 'customer': '_Test Customer',
- 'items_based_on': 'Sales Order',
- 'purpose': 'Delivery',
- 'locations': [{
- 'item_code': '_Test Item',
- 'qty': 1,
- 'stock_qty': 5,
- 'conversion_factor': 5,
- 'sales_order': sales_order.name,
- 'sales_order_item': sales_order.items[0].name ,
- }, {
- 'item_code': '_Test Item',
- 'qty': 1,
- 'stock_qty': 1,
- 'conversion_factor': 1,
- 'sales_order': sales_order.name,
- 'sales_order_item': sales_order.items[1].name ,
- }]
- })
+ pick_list = frappe.get_doc(
+ {
+ "doctype": "Pick List",
+ "company": "_Test Company",
+ "customer": "_Test Customer",
+ "items_based_on": "Sales Order",
+ "purpose": "Delivery",
+ "locations": [
+ {
+ "item_code": "_Test Item",
+ "qty": 2,
+ "stock_qty": 1,
+ "conversion_factor": 0.5,
+ "sales_order": sales_order.name,
+ "sales_order_item": sales_order.items[0].name,
+ },
+ {
+ "item_code": "_Test Item",
+ "qty": 1,
+ "stock_qty": 1,
+ "conversion_factor": 1,
+ "sales_order": sales_order.name,
+ "sales_order_item": sales_order.items[1].name,
+ },
+ ],
+ }
+ )
pick_list.set_item_locations()
pick_list.submit()
@@ -349,7 +404,9 @@ class TestPickList(ERPNextTestCase):
self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty)
self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty)
- self.assertEqual(sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor)
+ self.assertEqual(
+ sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor
+ )
pick_list.cancel()
sales_order.cancel()
@@ -362,22 +419,30 @@ class TestPickList(ERPNextTestCase):
self.assertEqual(b.get(key), value, msg=f"{key} doesn't match")
# nothing should be grouped
- pl = frappe.get_doc(doctype="Pick List", group_same_items=True, locations=[
- _dict(item_code="A", warehouse="X", qty=1, picked_qty=2),
- _dict(item_code="B", warehouse="X", qty=1, picked_qty=2),
- _dict(item_code="A", warehouse="Y", qty=1, picked_qty=2),
- _dict(item_code="B", warehouse="Y", qty=1, picked_qty=2),
- ])
+ pl = frappe.get_doc(
+ doctype="Pick List",
+ group_same_items=True,
+ locations=[
+ _dict(item_code="A", warehouse="X", qty=1, picked_qty=2),
+ _dict(item_code="B", warehouse="X", qty=1, picked_qty=2),
+ _dict(item_code="A", warehouse="Y", qty=1, picked_qty=2),
+ _dict(item_code="B", warehouse="Y", qty=1, picked_qty=2),
+ ],
+ )
pl.before_print()
self.assertEqual(len(pl.locations), 4)
# grouping should halve the number of items
- pl = frappe.get_doc(doctype="Pick List", group_same_items=True, locations=[
- _dict(item_code="A", warehouse="X", qty=5, picked_qty=1),
- _dict(item_code="B", warehouse="Y", qty=4, picked_qty=2),
- _dict(item_code="A", warehouse="X", qty=3, picked_qty=2),
- _dict(item_code="B", warehouse="Y", qty=2, picked_qty=2),
- ])
+ pl = frappe.get_doc(
+ doctype="Pick List",
+ group_same_items=True,
+ locations=[
+ _dict(item_code="A", warehouse="X", qty=5, picked_qty=1),
+ _dict(item_code="B", warehouse="Y", qty=4, picked_qty=2),
+ _dict(item_code="A", warehouse="X", qty=3, picked_qty=2),
+ _dict(item_code="B", warehouse="Y", qty=2, picked_qty=2),
+ ],
+ )
pl.before_print()
self.assertEqual(len(pl.locations), 2)
@@ -388,14 +453,195 @@ class TestPickList(ERPNextTestCase):
for expected_item, created_item in zip(expected_items, pl.locations):
_compare_dicts(expected_item, created_item)
- # def test_pick_list_skips_items_in_expired_batch(self):
- # pass
+ def test_multiple_dn_creation(self):
+ sales_order_1 = frappe.get_doc(
+ {
+ "doctype": "Sales Order",
+ "customer": "_Test Customer",
+ "company": "_Test Company",
+ "items": [
+ {
+ "item_code": "_Test Item",
+ "qty": 1,
+ "conversion_factor": 1,
+ "delivery_date": frappe.utils.today(),
+ }
+ ],
+ }
+ ).insert()
+ sales_order_1.submit()
+ sales_order_2 = frappe.get_doc(
+ {
+ "doctype": "Sales Order",
+ "customer": "_Test Customer 1",
+ "company": "_Test Company",
+ "items": [
+ {
+ "item_code": "_Test Item 2",
+ "qty": 1,
+ "conversion_factor": 1,
+ "delivery_date": frappe.utils.today(),
+ },
+ ],
+ }
+ ).insert()
+ sales_order_2.submit()
+ pick_list = frappe.get_doc(
+ {
+ "doctype": "Pick List",
+ "company": "_Test Company",
+ "items_based_on": "Sales Order",
+ "purpose": "Delivery",
+ "picker": "P001",
+ "locations": [
+ {
+ "item_code": "_Test Item ",
+ "qty": 1,
+ "stock_qty": 1,
+ "conversion_factor": 1,
+ "sales_order": sales_order_1.name,
+ "sales_order_item": sales_order_1.items[0].name,
+ },
+ {
+ "item_code": "_Test Item 2",
+ "qty": 1,
+ "stock_qty": 1,
+ "conversion_factor": 1,
+ "sales_order": sales_order_2.name,
+ "sales_order_item": sales_order_2.items[0].name,
+ },
+ ],
+ }
+ )
+ pick_list.set_item_locations()
+ pick_list.submit()
+ create_delivery_note(pick_list.name)
+ for dn in frappe.get_all(
+ "Delivery Note",
+ filters={"pick_list": pick_list.name, "customer": "_Test Customer"},
+ fields={"name"},
+ ):
+ for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
+ self.assertEqual(dn_item.item_code, "_Test Item")
+ self.assertEqual(dn_item.against_sales_order, sales_order_1.name)
+ self.assertEqual(dn_item.pick_list_item, pick_list.locations[dn_item.idx - 1].name)
- # def test_pick_list_from_sales_order(self):
- # pass
+ for dn in frappe.get_all(
+ "Delivery Note",
+ filters={"pick_list": pick_list.name, "customer": "_Test Customer 1"},
+ fields={"name"},
+ ):
+ for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
+ self.assertEqual(dn_item.item_code, "_Test Item 2")
+ self.assertEqual(dn_item.against_sales_order, sales_order_2.name)
+ # test DN creation without so
+ pick_list_1 = frappe.get_doc(
+ {
+ "doctype": "Pick List",
+ "company": "_Test Company",
+ "purpose": "Delivery",
+ "picker": "P001",
+ "locations": [
+ {
+ "item_code": "_Test Item ",
+ "qty": 1,
+ "stock_qty": 1,
+ "conversion_factor": 1,
+ },
+ {
+ "item_code": "_Test Item 2",
+ "qty": 2,
+ "stock_qty": 2,
+ "conversion_factor": 1,
+ },
+ ],
+ }
+ )
+ pick_list_1.set_item_locations()
+ pick_list_1.submit()
+ create_delivery_note(pick_list_1.name)
+ for dn in frappe.get_all(
+ "Delivery Note", filters={"pick_list": pick_list_1.name}, fields={"name"}
+ ):
+ for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
+ if dn_item.item_code == "_Test Item":
+ self.assertEqual(dn_item.qty, 1)
+ if dn_item.item_code == "_Test Item 2":
+ self.assertEqual(dn_item.qty, 2)
- # def test_pick_list_from_work_order(self):
- # pass
+ def test_picklist_with_multi_uom(self):
+ warehouse = "_Test Warehouse - _TC"
+ item = make_item(properties={"uoms": [dict(uom="Box", conversion_factor=24)]}).name
+ make_stock_entry(item=item, to_warehouse=warehouse, qty=1000)
- # def test_pick_list_from_material_request(self):
- # pass
+ so = make_sales_order(item_code=item, qty=10, rate=42, uom="Box")
+ pl = create_pick_list(so.name)
+ # pick half the qty
+ for loc in pl.locations:
+ loc.picked_qty = loc.stock_qty / 2
+ pl.save()
+ pl.submit()
+
+ so.reload()
+ self.assertEqual(so.per_picked, 50)
+
+ def test_picklist_with_bundles(self):
+ warehouse = "_Test Warehouse - _TC"
+
+ quantities = [5, 2]
+ bundle, components = create_product_bundle(quantities, warehouse=warehouse)
+ bundle_items = dict(zip(components, quantities))
+
+ so = make_sales_order(item_code=bundle, qty=3, rate=42)
+
+ pl = create_pick_list(so.name)
+ pl.save()
+ self.assertEqual(len(pl.locations), 2)
+ for item in pl.locations:
+ self.assertEqual(item.stock_qty, bundle_items[item.item_code] * 3)
+
+ # check picking status on sales order
+ pl.submit()
+ so.reload()
+ self.assertEqual(so.per_picked, 100)
+
+ # deliver
+ dn = create_delivery_note(pl.name).submit()
+ self.assertEqual(dn.items[0].rate, 42)
+ self.assertEqual(dn.packed_items[0].warehouse, warehouse)
+ so.reload()
+ self.assertEqual(so.per_delivered, 100)
+
+ def test_picklist_with_partial_bundles(self):
+ # from test_records.json
+ warehouse = "_Test Warehouse - _TC"
+
+ quantities = [5, 2]
+ bundle, components = create_product_bundle(quantities, warehouse=warehouse)
+
+ so = make_sales_order(item_code=bundle, qty=4, rate=42)
+
+ pl = create_pick_list(so.name)
+ for loc in pl.locations:
+ loc.picked_qty = loc.qty / 2
+
+ pl.save().submit()
+ so.reload()
+ self.assertEqual(so.per_picked, 50)
+
+ # deliver half qty
+ dn = create_delivery_note(pl.name).submit()
+ self.assertEqual(dn.items[0].rate, 42)
+ so.reload()
+ self.assertEqual(so.per_delivered, 50)
+
+ pl = create_pick_list(so.name)
+ pl.save().submit()
+ so.reload()
+ self.assertEqual(so.per_picked, 100)
+
+ # deliver remaining
+ dn = create_delivery_note(pl.name).submit()
+ self.assertEqual(dn.items[0].rate, 42)
+ so.reload()
+ self.assertEqual(so.per_delivered, 100)
diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
index 805286ddcc0..a96ebfcdee6 100644
--- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json
+++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
@@ -27,6 +27,7 @@
"column_break_15",
"sales_order",
"sales_order_item",
+ "product_bundle_item",
"material_request",
"material_request_item"
],
@@ -146,6 +147,7 @@
{
"fieldname": "sales_order_item",
"fieldtype": "Data",
+ "hidden": 1,
"label": "Sales Order Item",
"read_only": 1
},
@@ -177,11 +179,19 @@
"fieldtype": "Data",
"label": "Item Group",
"read_only": 1
+ },
+ {
+ "description": "product bundle item row's name in sales order. Also indicates that picked item is to be used for a product bundle",
+ "fieldname": "product_bundle_item",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Product Bundle Item",
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2021-09-28 12:02:16.923056",
+ "modified": "2022-04-22 05:27:38.497997",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",
@@ -190,5 +200,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/price_list/price_list.py b/erpnext/stock/doctype/price_list/price_list.py
index 8a3172e9e22..554055fd839 100644
--- a/erpnext/stock/doctype/price_list/price_list.py
+++ b/erpnext/stock/doctype/price_list/price_list.py
@@ -31,9 +31,11 @@ class PriceList(Document):
frappe.set_value("Buying Settings", "Buying Settings", "buying_price_list", self.name)
def update_item_price(self):
- frappe.db.sql("""update `tabItem Price` set currency=%s,
+ frappe.db.sql(
+ """update `tabItem Price` set currency=%s,
buying=%s, selling=%s, modified=NOW() where price_list=%s""",
- (self.currency, cint(self.buying), cint(self.selling), self.name))
+ (self.currency, cint(self.buying), cint(self.selling), self.name),
+ )
def check_impact_on_shopping_cart(self):
"Check if Price List currency change impacts E Commerce Cart."
@@ -66,12 +68,14 @@ class PriceList(Document):
def delete_price_list_details_key(self):
frappe.cache().hdel("price_list_details", self.name)
+
def get_price_list_details(price_list):
price_list_details = frappe.cache().hget("price_list_details", price_list)
if not price_list_details:
- price_list_details = frappe.get_cached_value("Price List", price_list,
- ["currency", "price_not_uom_dependent", "enabled"], as_dict=1)
+ price_list_details = frappe.get_cached_value(
+ "Price List", price_list, ["currency", "price_not_uom_dependent", "enabled"], as_dict=1
+ )
if not price_list_details or not price_list_details.get("enabled"):
throw(_("Price List {0} is disabled or does not exist").format(price_list))
diff --git a/erpnext/stock/doctype/price_list/test_price_list.py b/erpnext/stock/doctype/price_list/test_price_list.py
index b8218b942e7..93660930c79 100644
--- a/erpnext/stock/doctype/price_list/test_price_list.py
+++ b/erpnext/stock/doctype/price_list/test_price_list.py
@@ -6,4 +6,4 @@ import frappe
# test_ignore = ["Item"]
-test_records = frappe.get_test_records('Price List')
+test_records = frappe.get_test_records("Price List")
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index b54a90eed35..355f0e593ef 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -24,6 +24,10 @@
"apply_putaway_rule",
"is_return",
"return_against",
+ "accounting_dimensions_section",
+ "cost_center",
+ "dimension_col_break",
+ "project",
"section_addresses",
"supplier_address",
"contact_person",
@@ -106,10 +110,7 @@
"terms",
"bill_no",
"bill_date",
- "accounting_details_section",
- "provisional_expense_account",
"more_info",
- "project",
"status",
"amended_from",
"range",
@@ -1149,23 +1150,26 @@
},
{
"collapsible": 1,
- "fieldname": "accounting_details_section",
+ "fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
- "label": "Accounting Details"
+ "label": "Accounting Dimensions"
},
{
- "fieldname": "provisional_expense_account",
+ "fieldname": "cost_center",
"fieldtype": "Link",
- "hidden": 1,
- "label": "Provisional Expense Account",
- "options": "Account"
+ "label": "Cost Center",
+ "options": "Cost Center"
+ },
+ {
+ "fieldname": "dimension_col_break",
+ "fieldtype": "Column Break"
}
],
"icon": "fa fa-truck",
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2022-02-01 11:40:52.690984",
+ "modified": "2022-04-26 13:41:32.625197",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index afaa8b02a89..db94beccbcf 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -17,82 +17,85 @@ from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction
-form_grid_templates = {
- "items": "templates/form_grid/item_grid.html"
-}
+form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
+
class PurchaseReceipt(BuyingController):
def __init__(self, *args, **kwargs):
super(PurchaseReceipt, self).__init__(*args, **kwargs)
- self.status_updater = [{
- 'target_dt': 'Purchase Order Item',
- 'join_field': 'purchase_order_item',
- 'target_field': 'received_qty',
- 'target_parent_dt': 'Purchase Order',
- 'target_parent_field': 'per_received',
- 'target_ref_field': 'qty',
- 'source_dt': 'Purchase Receipt Item',
- 'source_field': 'received_qty',
- 'second_source_dt': 'Purchase Invoice Item',
- 'second_source_field': 'received_qty',
- 'second_join_field': 'po_detail',
- 'percent_join_field': 'purchase_order',
- 'overflow_type': 'receipt',
- 'second_source_extra_cond': """ and exists(select name from `tabPurchase Invoice`
- where name=`tabPurchase Invoice Item`.parent and update_stock = 1)"""
- },
- {
- 'source_dt': 'Purchase Receipt Item',
- 'target_dt': 'Material Request Item',
- 'join_field': 'material_request_item',
- 'target_field': 'received_qty',
- 'target_parent_dt': 'Material Request',
- 'target_parent_field': 'per_received',
- 'target_ref_field': 'stock_qty',
- 'source_field': 'stock_qty',
- 'percent_join_field': 'material_request'
- },
- {
- 'source_dt': 'Purchase Receipt Item',
- 'target_dt': 'Purchase Invoice Item',
- 'join_field': 'purchase_invoice_item',
- 'target_field': 'received_qty',
- 'target_parent_dt': 'Purchase Invoice',
- 'target_parent_field': 'per_received',
- 'target_ref_field': 'qty',
- 'source_field': 'received_qty',
- 'percent_join_field': 'purchase_invoice',
- 'overflow_type': 'receipt'
- }]
+ self.status_updater = [
+ {
+ "target_dt": "Purchase Order Item",
+ "join_field": "purchase_order_item",
+ "target_field": "received_qty",
+ "target_parent_dt": "Purchase Order",
+ "target_parent_field": "per_received",
+ "target_ref_field": "qty",
+ "source_dt": "Purchase Receipt Item",
+ "source_field": "received_qty",
+ "second_source_dt": "Purchase Invoice Item",
+ "second_source_field": "received_qty",
+ "second_join_field": "po_detail",
+ "percent_join_field": "purchase_order",
+ "overflow_type": "receipt",
+ "second_source_extra_cond": """ and exists(select name from `tabPurchase Invoice`
+ where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""",
+ },
+ {
+ "source_dt": "Purchase Receipt Item",
+ "target_dt": "Material Request Item",
+ "join_field": "material_request_item",
+ "target_field": "received_qty",
+ "target_parent_dt": "Material Request",
+ "target_parent_field": "per_received",
+ "target_ref_field": "stock_qty",
+ "source_field": "stock_qty",
+ "percent_join_field": "material_request",
+ },
+ {
+ "source_dt": "Purchase Receipt Item",
+ "target_dt": "Purchase Invoice Item",
+ "join_field": "purchase_invoice_item",
+ "target_field": "received_qty",
+ "target_parent_dt": "Purchase Invoice",
+ "target_parent_field": "per_received",
+ "target_ref_field": "qty",
+ "source_field": "received_qty",
+ "percent_join_field": "purchase_invoice",
+ "overflow_type": "receipt",
+ },
+ ]
if cint(self.is_return):
- self.status_updater.extend([
- {
- 'source_dt': 'Purchase Receipt Item',
- 'target_dt': 'Purchase Order Item',
- 'join_field': 'purchase_order_item',
- 'target_field': 'returned_qty',
- 'source_field': '-1 * qty',
- 'second_source_dt': 'Purchase Invoice Item',
- 'second_source_field': '-1 * qty',
- 'second_join_field': 'po_detail',
- 'extra_cond': """ and exists (select name from `tabPurchase Receipt`
+ self.status_updater.extend(
+ [
+ {
+ "source_dt": "Purchase Receipt Item",
+ "target_dt": "Purchase Order Item",
+ "join_field": "purchase_order_item",
+ "target_field": "returned_qty",
+ "source_field": "-1 * qty",
+ "second_source_dt": "Purchase Invoice Item",
+ "second_source_field": "-1 * qty",
+ "second_join_field": "po_detail",
+ "extra_cond": """ and exists (select name from `tabPurchase Receipt`
where name=`tabPurchase Receipt Item`.parent and is_return=1)""",
- 'second_source_extra_cond': """ and exists (select name from `tabPurchase Invoice`
- where name=`tabPurchase Invoice Item`.parent and is_return=1 and update_stock=1)"""
- },
- {
- 'source_dt': 'Purchase Receipt Item',
- 'target_dt': 'Purchase Receipt Item',
- 'join_field': 'purchase_receipt_item',
- 'target_field': 'returned_qty',
- 'target_parent_dt': 'Purchase Receipt',
- 'target_parent_field': 'per_returned',
- 'target_ref_field': 'received_stock_qty',
- 'source_field': '-1 * received_stock_qty',
- 'percent_join_field_parent': 'return_against'
- }
- ])
+ "second_source_extra_cond": """ and exists (select name from `tabPurchase Invoice`
+ where name=`tabPurchase Invoice Item`.parent and is_return=1 and update_stock=1)""",
+ },
+ {
+ "source_dt": "Purchase Receipt Item",
+ "target_dt": "Purchase Receipt Item",
+ "join_field": "purchase_receipt_item",
+ "target_field": "returned_qty",
+ "target_parent_dt": "Purchase Receipt",
+ "target_parent_field": "per_returned",
+ "target_ref_field": "received_stock_qty",
+ "source_field": "-1 * received_stock_qty",
+ "percent_join_field_parent": "return_against",
+ },
+ ]
+ )
def before_validate(self):
from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule
@@ -104,8 +107,8 @@ class PurchaseReceipt(BuyingController):
self.validate_posting_time()
super(PurchaseReceipt, self).validate()
- if self._action=="submit":
- self.make_batches('warehouse')
+ if self._action == "submit":
+ self.make_batches("warehouse")
else:
self.set_status()
@@ -125,77 +128,95 @@ class PurchaseReceipt(BuyingController):
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
-
def validate_cwip_accounts(self):
- for item in self.get('items'):
+ for item in self.get("items"):
if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category):
# check cwip accounts before making auto assets
# Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account
arbnb_account = self.get_company_default("asset_received_but_not_billed")
- cwip_account = get_asset_account("capital_work_in_progress_account", asset_category = item.asset_category, \
- company = self.company)
+ cwip_account = get_asset_account(
+ "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company
+ )
break
def validate_provisional_expense_account(self):
- provisional_accounting_for_non_stock_items = \
- cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items'))
+ provisional_accounting_for_non_stock_items = cint(
+ frappe.db.get_value(
+ "Company", self.company, "enable_provisional_accounting_for_non_stock_items"
+ )
+ )
- if provisional_accounting_for_non_stock_items:
- default_provisional_account = self.get_company_default("default_provisional_account")
- if not self.provisional_expense_account:
- self.provisional_expense_account = default_provisional_account
+ if not provisional_accounting_for_non_stock_items:
+ return
+
+ default_provisional_account = self.get_company_default("default_provisional_account")
+ for item in self.get("items"):
+ if not item.get("provisional_expense_account"):
+ item.provisional_expense_account = default_provisional_account
def validate_with_previous_doc(self):
- super(PurchaseReceipt, self).validate_with_previous_doc({
- "Purchase Order": {
- "ref_dn_field": "purchase_order",
- "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]],
- },
- "Purchase Order Item": {
- "ref_dn_field": "purchase_order_item",
- "compare_fields": [["project", "="], ["uom", "="], ["item_code", "="]],
- "is_child_table": True,
- "allow_duplicate_prev_row_id": True
+ super(PurchaseReceipt, self).validate_with_previous_doc(
+ {
+ "Purchase Order": {
+ "ref_dn_field": "purchase_order",
+ "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]],
+ },
+ "Purchase Order Item": {
+ "ref_dn_field": "purchase_order_item",
+ "compare_fields": [["project", "="], ["uom", "="], ["item_code", "="]],
+ "is_child_table": True,
+ "allow_duplicate_prev_row_id": True,
+ },
}
- })
+ )
- if cint(frappe.db.get_single_value('Buying Settings', 'maintain_same_rate')) and not self.is_return:
- self.validate_rate_with_reference_doc([["Purchase Order", "purchase_order", "purchase_order_item"]])
+ if (
+ cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) and not self.is_return
+ ):
+ self.validate_rate_with_reference_doc(
+ [["Purchase Order", "purchase_order", "purchase_order_item"]]
+ )
def po_required(self):
- if frappe.db.get_value("Buying Settings", None, "po_required") == 'Yes':
- for d in self.get('items'):
+ if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes":
+ for d in self.get("items"):
if not d.purchase_order:
frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code))
def get_already_received_qty(self, po, po_detail):
- qty = frappe.db.sql("""select sum(qty) from `tabPurchase Receipt Item`
+ qty = frappe.db.sql(
+ """select sum(qty) from `tabPurchase Receipt Item`
where purchase_order_item = %s and docstatus = 1
and purchase_order=%s
- and parent != %s""", (po_detail, po, self.name))
+ and parent != %s""",
+ (po_detail, po, self.name),
+ )
return qty and flt(qty[0][0]) or 0.0
def get_po_qty_and_warehouse(self, po_detail):
- po_qty, po_warehouse = frappe.db.get_value("Purchase Order Item", po_detail,
- ["qty", "warehouse"])
+ po_qty, po_warehouse = frappe.db.get_value(
+ "Purchase Order Item", po_detail, ["qty", "warehouse"]
+ )
return po_qty, po_warehouse
# Check for Closed status
def check_on_hold_or_closed_status(self):
- check_list =[]
- for d in self.get('items'):
- if (d.meta.get_field('purchase_order') and d.purchase_order
- and d.purchase_order not in check_list):
+ check_list = []
+ for d in self.get("items"):
+ if (
+ d.meta.get_field("purchase_order") and d.purchase_order and d.purchase_order not in check_list
+ ):
check_list.append(d.purchase_order)
- check_on_hold_or_closed_status('Purchase Order', d.purchase_order)
+ check_on_hold_or_closed_status("Purchase Order", d.purchase_order)
# on submit
def on_submit(self):
super(PurchaseReceipt, self).on_submit()
# Check for Approving Authority
- frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype,
- self.company, self.base_grand_total)
+ frappe.get_doc("Authorization Control").validate_approving_authority(
+ self.doctype, self.company, self.base_grand_total
+ )
self.update_prevdoc_status()
if flt(self.per_billed) < 100:
@@ -203,13 +224,13 @@ class PurchaseReceipt(BuyingController):
else:
self.db_set("status", "Completed")
-
# Updating stock ledger should always be called after updating prevdoc status,
# because updating ordered qty, reserved_qty_for_subcontract in bin
# depends upon updated ordered qty in PO
self.update_stock_ledger()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
+
update_serial_nos_after_submit(self, "items")
self.make_gl_entries()
@@ -217,10 +238,12 @@ class PurchaseReceipt(BuyingController):
self.set_consumed_qty_in_po()
def check_next_docstatus(self):
- submit_rv = frappe.db.sql("""select t1.name
+ submit_rv = frappe.db.sql(
+ """select t1.name
from `tabPurchase Invoice` t1,`tabPurchase Invoice Item` t2
where t1.name = t2.parent and t2.purchase_receipt = %s and t1.docstatus = 1""",
- (self.name))
+ (self.name),
+ )
if submit_rv:
frappe.throw(_("Purchase Invoice {0} is already submitted").format(self.submit_rv[0][0]))
@@ -229,10 +252,12 @@ class PurchaseReceipt(BuyingController):
self.check_on_hold_or_closed_status()
# Check if Purchase Invoice has been submitted against current Purchase Order
- submitted = frappe.db.sql("""select t1.name
+ submitted = frappe.db.sql(
+ """select t1.name
from `tabPurchase Invoice` t1,`tabPurchase Invoice Item` t2
where t1.name = t2.parent and t2.purchase_receipt = %s and t1.docstatus = 1""",
- self.name)
+ self.name,
+ )
if submitted:
frappe.throw(_("Purchase Invoice {0} is already submitted").format(submitted[0][0]))
@@ -244,19 +269,24 @@ class PurchaseReceipt(BuyingController):
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.delete_auto_created_batches()
self.set_consumed_qty_in_po()
@frappe.whitelist()
def get_current_stock(self):
- for d in self.get('supplied_items'):
+ for d in self.get("supplied_items"):
if self.supplier_warehouse:
- bin = frappe.db.sql("select actual_qty from `tabBin` where item_code = %s and warehouse = %s", (d.rm_item_code, self.supplier_warehouse), as_dict = 1)
- d.current_stock = bin and flt(bin[0]['actual_qty']) or 0
+ bin = frappe.db.sql(
+ "select actual_qty from `tabBin` where item_code = %s and warehouse = %s",
+ (d.rm_item_code, self.supplier_warehouse),
+ as_dict=1,
+ )
+ d.current_stock = bin and flt(bin[0]["actual_qty"]) or 0
def get_gl_entries(self, warehouse_account=None):
from erpnext.accounts.general_ledger import process_gl_map
+
gl_entries = []
self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account)
@@ -273,29 +303,44 @@ class PurchaseReceipt(BuyingController):
warehouse_with_no_account = []
stock_items = self.get_stock_items()
- provisional_accounting_for_non_stock_items = \
- cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items'))
+ provisional_accounting_for_non_stock_items = cint(
+ frappe.db.get_value(
+ "Company", self.company, "enable_provisional_accounting_for_non_stock_items"
+ )
+ )
for d in self.get("items"):
if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty):
if warehouse_account.get(d.warehouse):
- stock_value_diff = frappe.db.get_value("Stock Ledger Entry",
- {"voucher_type": "Purchase Receipt", "voucher_no": self.name,
- "voucher_detail_no": d.name, "warehouse": d.warehouse, "is_cancelled": 0}, "stock_value_difference")
+ stock_value_diff = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": self.name,
+ "voucher_detail_no": d.name,
+ "warehouse": d.warehouse,
+ "is_cancelled": 0,
+ },
+ "stock_value_difference",
+ )
warehouse_account_name = warehouse_account[d.warehouse]["account"]
warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"]
supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account")
- supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get("account_currency")
+ supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get(
+ "account_currency"
+ )
remarks = self.get("remarks") or _("Accounting Entry for Stock")
# If PR is sub-contracted and fg item rate is zero
# in that case if account for source and target warehouse are same,
# then GL entries should not be posted
- if flt(stock_value_diff) == flt(d.rm_supp_cost) \
- and warehouse_account.get(self.supplier_warehouse) \
- and warehouse_account_name == supplier_warehouse_account:
- continue
+ if (
+ flt(stock_value_diff) == flt(d.rm_supp_cost)
+ and warehouse_account.get(self.supplier_warehouse)
+ and warehouse_account_name == supplier_warehouse_account
+ ):
+ continue
self.add_gl_entry(
gl_entries=gl_entries,
@@ -306,18 +351,24 @@ class PurchaseReceipt(BuyingController):
remarks=remarks,
against_account=stock_rbnb,
account_currency=warehouse_account_currency,
- item=d)
+ item=d,
+ )
# GL Entry for from warehouse or Stock Received but not billed
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
- credit_currency = get_account_currency(warehouse_account[d.from_warehouse]['account']) \
- if d.from_warehouse else get_account_currency(stock_rbnb)
+ credit_currency = (
+ get_account_currency(warehouse_account[d.from_warehouse]["account"])
+ if d.from_warehouse
+ else get_account_currency(stock_rbnb)
+ )
- credit_amount = flt(d.base_net_amount, d.precision("base_net_amount")) \
- if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount"))
+ credit_amount = (
+ flt(d.base_net_amount, d.precision("base_net_amount"))
+ if credit_currency == self.company_currency
+ else flt(d.net_amount, d.precision("net_amount"))
+ )
if credit_amount:
- account = warehouse_account[d.from_warehouse]['account'] \
- if d.from_warehouse else stock_rbnb
+ account = warehouse_account[d.from_warehouse]["account"] if d.from_warehouse else stock_rbnb
self.add_gl_entry(
gl_entries=gl_entries,
@@ -329,14 +380,18 @@ class PurchaseReceipt(BuyingController):
against_account=warehouse_account_name,
debit_in_account_currency=-1 * credit_amount,
account_currency=credit_currency,
- item=d)
+ item=d,
+ )
# Amount added through landed-cos-voucher
if d.landed_cost_voucher_amount and landed_cost_entries:
for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]):
account_currency = get_account_currency(account)
- credit_amount = (flt(amount["base_amount"]) if (amount["base_amount"] or
- account_currency!=self.company_currency) else flt(amount["amount"]))
+ credit_amount = (
+ flt(amount["base_amount"])
+ if (amount["base_amount"] or account_currency != self.company_currency)
+ else flt(amount["amount"])
+ )
self.add_gl_entry(
gl_entries=gl_entries,
@@ -349,7 +404,8 @@ class PurchaseReceipt(BuyingController):
credit_in_account_currency=flt(amount["amount"]),
account_currency=account_currency,
project=d.project,
- item=d)
+ item=d,
+ )
# sub-contracting warehouse
if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
@@ -362,22 +418,32 @@ class PurchaseReceipt(BuyingController):
remarks=remarks,
against_account=warehouse_account_name,
account_currency=supplier_warehouse_account_currency,
- item=d)
+ item=d,
+ )
# divisional loss adjustment
- valuation_amount_as_per_doc = flt(d.base_net_amount, d.precision("base_net_amount")) + \
- flt(d.landed_cost_voucher_amount) + flt(d.rm_supp_cost) + flt(d.item_tax_amount)
+ valuation_amount_as_per_doc = (
+ flt(d.base_net_amount, d.precision("base_net_amount"))
+ + flt(d.landed_cost_voucher_amount)
+ + flt(d.rm_supp_cost)
+ + flt(d.item_tax_amount)
+ )
- divisional_loss = flt(valuation_amount_as_per_doc - stock_value_diff,
- d.precision("base_net_amount"))
+ divisional_loss = flt(
+ valuation_amount_as_per_doc - stock_value_diff, d.precision("base_net_amount")
+ )
if divisional_loss:
if self.is_return or flt(d.item_tax_amount):
loss_account = expenses_included_in_valuation
else:
- loss_account = self.get_company_default("default_expense_account", ignore_validation=True) or stock_rbnb
+ 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"
+ )
self.add_gl_entry(
gl_entries=gl_entries,
@@ -389,21 +455,35 @@ class PurchaseReceipt(BuyingController):
against_account=warehouse_account_name,
account_currency=credit_currency,
project=d.project,
- item=d)
+ item=d,
+ )
- elif d.warehouse not in warehouse_with_no_account or \
- d.rejected_warehouse not in warehouse_with_no_account:
- warehouse_with_no_account.append(d.warehouse)
- elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and provisional_accounting_for_non_stock_items:
- self.add_provisional_gl_entry(d, gl_entries, self.posting_date)
+ elif (
+ d.warehouse not in warehouse_with_no_account
+ or d.rejected_warehouse not in warehouse_with_no_account
+ ):
+ warehouse_with_no_account.append(d.warehouse)
+ elif (
+ d.item_code not in stock_items
+ and not d.is_fixed_asset
+ and flt(d.qty)
+ and provisional_accounting_for_non_stock_items
+ ):
+ self.add_provisional_gl_entry(
+ d, gl_entries, self.posting_date, d.get("provisional_expense_account")
+ )
if warehouse_with_no_account:
- frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" +
- "\n".join(warehouse_with_no_account))
+ frappe.msgprint(
+ _("No accounting entries for the following warehouses")
+ + ": \n"
+ + "\n".join(warehouse_with_no_account)
+ )
- def add_provisional_gl_entry(self, item, gl_entries, posting_date, reverse=0):
- provisional_expense_account = self.get('provisional_expense_account')
- credit_currency = get_account_currency(provisional_expense_account)
+ def add_provisional_gl_entry(
+ self, item, gl_entries, posting_date, provisional_account, reverse=0
+ ):
+ credit_currency = get_account_currency(provisional_account)
debit_currency = get_account_currency(item.expense_account)
expense_account = item.expense_account
remarks = self.get("remarks") or _("Accounting Entry for Service")
@@ -411,11 +491,13 @@ class PurchaseReceipt(BuyingController):
if reverse:
multiplication_factor = -1
- expense_account = frappe.db.get_value('Purchase Receipt Item', {'name': item.get('pr_detail')}, ['expense_account'])
+ expense_account = frappe.db.get_value(
+ "Purchase Receipt Item", {"name": item.get("pr_detail")}, ["expense_account"]
+ )
self.add_gl_entry(
gl_entries=gl_entries,
- account=provisional_expense_account,
+ account=provisional_account,
cost_center=item.cost_center,
debit=0.0,
credit=multiplication_factor * item.amount,
@@ -425,7 +507,8 @@ class PurchaseReceipt(BuyingController):
project=item.project,
voucher_detail_no=item.name,
item=item,
- posting_date=posting_date)
+ posting_date=posting_date,
+ )
self.add_gl_entry(
gl_entries=gl_entries,
@@ -434,28 +517,36 @@ class PurchaseReceipt(BuyingController):
debit=multiplication_factor * item.amount,
credit=0.0,
remarks=remarks,
- against_account=provisional_expense_account,
- account_currency = debit_currency,
+ against_account=provisional_account,
+ account_currency=debit_currency,
project=item.project,
voucher_detail_no=item.name,
item=item,
- posting_date=posting_date)
+ posting_date=posting_date,
+ )
def make_tax_gl_entries(self, gl_entries):
if erpnext.is_perpetual_inventory_enabled(self.company):
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
- negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get('items')])
+ negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")])
# Cost center-wise amount breakup for other charges included for valuation
valuation_tax = {}
for tax in self.get("taxes"):
- if tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount):
+ if tax.category in ("Valuation", "Valuation and Total") and flt(
+ tax.base_tax_amount_after_discount_amount
+ ):
if not tax.cost_center:
- frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category)))
+ frappe.throw(
+ _("Cost Center is required in row {0} in Taxes table for type {1}").format(
+ tax.idx, _(tax.category)
+ )
+ )
valuation_tax.setdefault(tax.name, 0)
- valuation_tax[tax.name] += \
- (tax.add_deduct_tax == "Add" and 1 or -1) * flt(tax.base_tax_amount_after_discount_amount)
+ valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt(
+ tax.base_tax_amount_after_discount_amount
+ )
if negative_expense_to_be_booked and valuation_tax:
# Backward compatibility:
@@ -464,10 +555,13 @@ class PurchaseReceipt(BuyingController):
# post valuation related charges on "Stock Received But Not Billed"
# introduced in 2014 for backward compatibility of expenses already booked in expenses_included_in_valuation account
- negative_expense_booked_in_pi = frappe.db.sql("""select name from `tabPurchase Invoice Item` pi
+ negative_expense_booked_in_pi = frappe.db.sql(
+ """select name from `tabPurchase Invoice Item` pi
where docstatus = 1 and purchase_receipt=%s
and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice'
- and voucher_no=pi.parent and account=%s)""", (self.name, expenses_included_in_valuation))
+ and voucher_no=pi.parent and account=%s)""",
+ (self.name, expenses_included_in_valuation),
+ )
against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0])
total_valuation_amount = sum(valuation_tax.values())
@@ -485,7 +579,9 @@ class PurchaseReceipt(BuyingController):
if i == len(valuation_tax):
applicable_amount = amount_including_divisional_loss
else:
- applicable_amount = negative_expense_to_be_booked * (valuation_tax[tax.name] / total_valuation_amount)
+ applicable_amount = negative_expense_to_be_booked * (
+ valuation_tax[tax.name] / total_valuation_amount
+ )
amount_including_divisional_loss -= applicable_amount
self.add_gl_entry(
@@ -496,13 +592,28 @@ class PurchaseReceipt(BuyingController):
credit=applicable_amount,
remarks=self.remarks or _("Accounting Entry for Stock"),
against_account=against_account,
- item=tax)
+ item=tax,
+ )
i += 1
- def add_gl_entry(self, gl_entries, account, cost_center, debit, credit, remarks, against_account,
- debit_in_account_currency=None, credit_in_account_currency=None, account_currency=None,
- project=None, voucher_detail_no=None, item=None, posting_date=None):
+ def add_gl_entry(
+ self,
+ gl_entries,
+ account,
+ cost_center,
+ debit,
+ credit,
+ remarks,
+ against_account,
+ debit_in_account_currency=None,
+ credit_in_account_currency=None,
+ account_currency=None,
+ project=None,
+ voucher_detail_no=None,
+ item=None,
+ posting_date=None,
+ ):
gl_entry = {
"account": account,
@@ -542,17 +653,19 @@ class PurchaseReceipt(BuyingController):
def add_asset_gl_entries(self, item, gl_entries):
arbnb_account = self.get_company_default("asset_received_but_not_billed")
# This returns category's cwip account if not then fallback to company's default cwip account
- cwip_account = get_asset_account("capital_work_in_progress_account", asset_category = item.asset_category, \
- company = self.company)
+ cwip_account = get_asset_account(
+ "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company
+ )
- asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate)
+ asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate)
base_asset_amount = flt(item.base_net_amount + item.item_tax_amount)
remarks = self.get("remarks") or _("Accounting Entry for Asset")
cwip_account_currency = get_account_currency(cwip_account)
# debit cwip account
- debit_in_account_currency = (base_asset_amount
- if cwip_account_currency == self.company_currency else asset_amount)
+ debit_in_account_currency = (
+ base_asset_amount if cwip_account_currency == self.company_currency else asset_amount
+ )
self.add_gl_entry(
gl_entries=gl_entries,
@@ -563,12 +676,14 @@ class PurchaseReceipt(BuyingController):
remarks=remarks,
against_account=arbnb_account,
debit_in_account_currency=debit_in_account_currency,
- item=item)
+ item=item,
+ )
asset_rbnb_currency = get_account_currency(arbnb_account)
# credit arbnb account
- credit_in_account_currency = (base_asset_amount
- if asset_rbnb_currency == self.company_currency else asset_amount)
+ credit_in_account_currency = (
+ base_asset_amount if asset_rbnb_currency == self.company_currency else asset_amount
+ )
self.add_gl_entry(
gl_entries=gl_entries,
@@ -579,13 +694,17 @@ class PurchaseReceipt(BuyingController):
remarks=remarks,
against_account=cwip_account,
credit_in_account_currency=credit_in_account_currency,
- item=item)
+ item=item,
+ )
def add_lcv_gl_entries(self, item, gl_entries):
- expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation")
+ expenses_included_in_asset_valuation = self.get_company_default(
+ "expenses_included_in_asset_valuation"
+ )
if not is_cwip_accounting_enabled(item.asset_category):
- asset_account = get_asset_category_account(asset_category=item.asset_category, \
- fieldname='fixed_asset_account', company=self.company)
+ asset_account = get_asset_category_account(
+ asset_category=item.asset_category, fieldname="fixed_asset_account", company=self.company
+ )
else:
# This returns company's default cwip account
asset_account = get_asset_account("capital_work_in_progress_account", company=self.company)
@@ -601,7 +720,8 @@ class PurchaseReceipt(BuyingController):
remarks=remarks,
against_account=asset_account,
project=item.project,
- item=item)
+ item=item,
+ )
self.add_gl_entry(
gl_entries=gl_entries,
@@ -612,11 +732,12 @@ class PurchaseReceipt(BuyingController):
remarks=remarks,
against_account=expenses_included_in_asset_valuation,
project=item.project,
- item=item)
+ item=item,
+ )
def update_assets(self, item, valuation_rate):
- assets = frappe.db.get_all('Asset',
- filters={ 'purchase_receipt': self.name, 'item_code': item.item_code }
+ assets = frappe.db.get_all(
+ "Asset", filters={"purchase_receipt": self.name, "item_code": item.item_code}
)
for asset in assets:
@@ -632,7 +753,7 @@ class PurchaseReceipt(BuyingController):
updated_pr = [self.name]
for d in self.get("items"):
if d.get("purchase_invoice") and d.get("purchase_invoice_item"):
- d.db_set('billed_amt', d.amount, update_modified=update_modified)
+ d.db_set("billed_amt", d.amount, update_modified=update_modified)
elif d.purchase_order_item:
updated_pr += update_billed_amount_based_on_po(d.purchase_order_item, update_modified)
@@ -642,24 +763,35 @@ class PurchaseReceipt(BuyingController):
self.load_from_db()
+
def update_billed_amount_based_on_po(po_detail, update_modified=True):
# Billed against Sales Order directly
- billed_against_po = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item`
- where po_detail=%s and (pr_detail is null or pr_detail = '') and docstatus=1""", po_detail)
+ billed_against_po = frappe.db.sql(
+ """select sum(amount) from `tabPurchase Invoice Item`
+ where po_detail=%s and (pr_detail is null or pr_detail = '') and docstatus=1""",
+ po_detail,
+ )
billed_against_po = billed_against_po and billed_against_po[0][0] or 0
# Get all Purchase Receipt Item rows against the Purchase Order Item row
- pr_details = frappe.db.sql("""select pr_item.name, pr_item.amount, pr_item.parent
+ pr_details = frappe.db.sql(
+ """select pr_item.name, pr_item.amount, pr_item.parent
from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr
where pr.name=pr_item.parent and pr_item.purchase_order_item=%s
and pr.docstatus=1 and pr.is_return = 0
- order by pr.posting_date asc, pr.posting_time asc, pr.name asc""", po_detail, as_dict=1)
+ order by pr.posting_date asc, pr.posting_time asc, pr.name asc""",
+ po_detail,
+ as_dict=1,
+ )
updated_pr = []
for pr_item in pr_details:
# Get billed amount directly against Purchase Receipt
- billed_amt_agianst_pr = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item`
- where pr_detail=%s and docstatus=1""", pr_item.name)
+ billed_amt_agianst_pr = frappe.db.sql(
+ """select sum(amount) from `tabPurchase Invoice Item`
+ where pr_detail=%s and docstatus=1""",
+ pr_item.name,
+ )
billed_amt_agianst_pr = billed_amt_agianst_pr and billed_amt_agianst_pr[0][0] or 0
# Distribute billed amount directly against PO between PRs based on FIFO
@@ -672,12 +804,19 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True):
billed_amt_agianst_pr += billed_against_po
billed_against_po = 0
- frappe.db.set_value("Purchase Receipt Item", pr_item.name, "billed_amt", billed_amt_agianst_pr, update_modified=update_modified)
+ frappe.db.set_value(
+ "Purchase Receipt Item",
+ pr_item.name,
+ "billed_amt",
+ billed_amt_agianst_pr,
+ update_modified=update_modified,
+ )
updated_pr.append(pr_item.parent)
return updated_pr
+
def update_billing_percentage(pr_doc, update_modified=True):
# Reload as billed amount was set in db directly
pr_doc.load_from_db()
@@ -685,15 +824,15 @@ def update_billing_percentage(pr_doc, update_modified=True):
# Update Billing % based on pending accepted qty
total_amount, total_billed_amount = 0, 0
for item in pr_doc.items:
- return_data = frappe.db.get_list("Purchase Receipt",
- fields = [
- "sum(abs(`tabPurchase Receipt Item`.qty)) as qty"
- ],
- filters = [
+ return_data = frappe.db.get_list(
+ "Purchase Receipt",
+ fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"],
+ filters=[
["Purchase Receipt", "docstatus", "=", 1],
["Purchase Receipt", "is_return", "=", 1],
- ["Purchase Receipt Item", "purchase_receipt_item", "=", item.name]
- ])
+ ["Purchase Receipt Item", "purchase_receipt_item", "=", item.name],
+ ],
+ )
returned_qty = return_data[0].qty if return_data else 0
returned_amount = flt(returned_qty) * flt(item.rate)
@@ -711,11 +850,12 @@ def update_billing_percentage(pr_doc, update_modified=True):
pr_doc.set_status(update=True)
pr_doc.notify_update()
+
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None):
from erpnext.accounts.party import get_payment_terms_template
- doc = frappe.get_doc('Purchase Receipt', source_name)
+ doc = frappe.get_doc("Purchase Receipt", source_name)
returned_qty_map = get_returned_qty_map(source_name)
invoiced_qty_map = get_invoiced_qty_map(source_name)
@@ -724,8 +864,9 @@ def make_purchase_invoice(source_name, target_doc=None):
frappe.throw(_("All items have already been Invoiced/Returned"))
doc = frappe.get_doc(target)
- doc.ignore_pricing_rule = 1
- doc.payment_terms_template = get_payment_terms_template(source.supplier, "Supplier", source.company)
+ doc.payment_terms_template = get_payment_terms_template(
+ source.supplier, "Supplier", source.company
+ )
doc.run_method("onload")
doc.run_method("set_missing_values")
doc.run_method("calculate_taxes_and_totals")
@@ -733,14 +874,20 @@ def make_purchase_invoice(source_name, target_doc=None):
def update_item(source_doc, target_doc, source_parent):
target_doc.qty, returned_qty = get_pending_qty(source_doc)
- if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
+ if frappe.db.get_single_value(
+ "Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
+ ):
target_doc.rejected_qty = 0
- target_doc.stock_qty = flt(target_doc.qty) * flt(target_doc.conversion_factor, target_doc.precision("conversion_factor"))
+ target_doc.stock_qty = flt(target_doc.qty) * flt(
+ target_doc.conversion_factor, target_doc.precision("conversion_factor")
+ )
returned_qty_map[source_doc.name] = returned_qty
def get_pending_qty(item_row):
qty = item_row.qty
- if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
+ if frappe.db.get_single_value(
+ "Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
+ ):
qty = item_row.received_qty
pending_qty = qty - invoiced_qty_map.get(item_row.name, 0)
returned_qty = flt(returned_qty_map.get(item_row.name, 0))
@@ -753,68 +900,85 @@ def make_purchase_invoice(source_name, target_doc=None):
returned_qty = 0
return pending_qty, returned_qty
-
- doclist = get_mapped_doc("Purchase Receipt", source_name, {
- "Purchase Receipt": {
- "doctype": "Purchase Invoice",
- "field_map": {
- "supplier_warehouse":"supplier_warehouse",
- "is_return": "is_return",
- "bill_date": "bill_date"
+ doclist = get_mapped_doc(
+ "Purchase Receipt",
+ source_name,
+ {
+ "Purchase Receipt": {
+ "doctype": "Purchase Invoice",
+ "field_map": {
+ "supplier_warehouse": "supplier_warehouse",
+ "is_return": "is_return",
+ "bill_date": "bill_date",
+ },
+ "validation": {
+ "docstatus": ["=", 1],
+ },
},
- "validation": {
- "docstatus": ["=", 1],
+ "Purchase Receipt Item": {
+ "doctype": "Purchase Invoice Item",
+ "field_map": {
+ "name": "pr_detail",
+ "parent": "purchase_receipt",
+ "purchase_order_item": "po_detail",
+ "purchase_order": "purchase_order",
+ "is_fixed_asset": "is_fixed_asset",
+ "asset_location": "asset_location",
+ "asset_category": "asset_category",
+ },
+ "postprocess": update_item,
+ "filter": lambda d: get_pending_qty(d)[0] <= 0
+ if not doc.get("is_return")
+ else get_pending_qty(d)[0] > 0,
},
+ "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True},
},
- "Purchase Receipt Item": {
- "doctype": "Purchase Invoice Item",
- "field_map": {
- "name": "pr_detail",
- "parent": "purchase_receipt",
- "purchase_order_item": "po_detail",
- "purchase_order": "purchase_order",
- "is_fixed_asset": "is_fixed_asset",
- "asset_location": "asset_location",
- "asset_category": 'asset_category'
- },
- "postprocess": update_item,
- "filter": lambda d: get_pending_qty(d)[0] <= 0 if not doc.get("is_return") else get_pending_qty(d)[0] > 0
- },
- "Purchase Taxes and Charges": {
- "doctype": "Purchase Taxes and Charges",
- "add_if_empty": True
- }
- }, target_doc, set_missing_values)
+ target_doc,
+ set_missing_values,
+ )
+ doclist.set_onload("ignore_price_list", True)
return doclist
+
def get_invoiced_qty_map(purchase_receipt):
"""returns a map: {pr_detail: invoiced_qty}"""
invoiced_qty_map = {}
- for pr_detail, qty in frappe.db.sql("""select pr_detail, qty from `tabPurchase Invoice Item`
- where purchase_receipt=%s and docstatus=1""", purchase_receipt):
- if not invoiced_qty_map.get(pr_detail):
- invoiced_qty_map[pr_detail] = 0
- invoiced_qty_map[pr_detail] += qty
+ for pr_detail, qty in frappe.db.sql(
+ """select pr_detail, qty from `tabPurchase Invoice Item`
+ where purchase_receipt=%s and docstatus=1""",
+ purchase_receipt,
+ ):
+ if not invoiced_qty_map.get(pr_detail):
+ invoiced_qty_map[pr_detail] = 0
+ invoiced_qty_map[pr_detail] += qty
return invoiced_qty_map
+
def get_returned_qty_map(purchase_receipt):
"""returns a map: {so_detail: returned_qty}"""
- returned_qty_map = frappe._dict(frappe.db.sql("""select pr_item.purchase_receipt_item, abs(pr_item.qty) as qty
+ returned_qty_map = frappe._dict(
+ frappe.db.sql(
+ """select pr_item.purchase_receipt_item, abs(pr_item.qty) as qty
from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr
where pr.name = pr_item.parent
and pr.docstatus = 1
and pr.is_return = 1
and pr.return_against = %s
- """, purchase_receipt))
+ """,
+ purchase_receipt,
+ )
+ )
return returned_qty_map
+
@frappe.whitelist()
def make_purchase_return(source_name, target_doc=None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
+
return make_return_doc("Purchase Receipt", source_name, target_doc)
@@ -823,35 +987,47 @@ def update_purchase_receipt_status(docname, status):
pr = frappe.get_doc("Purchase Receipt", docname)
pr.update_status(status)
+
@frappe.whitelist()
-def make_stock_entry(source_name,target_doc=None):
+def make_stock_entry(source_name, target_doc=None):
def set_missing_values(source, target):
target.stock_entry_type = "Material Transfer"
- target.purpose = "Material Transfer"
+ target.purpose = "Material Transfer"
- doclist = get_mapped_doc("Purchase Receipt", source_name,{
- "Purchase Receipt": {
- "doctype": "Stock Entry",
- },
- "Purchase Receipt Item": {
- "doctype": "Stock Entry Detail",
- "field_map": {
- "warehouse": "s_warehouse",
- "parent": "reference_purchase_receipt",
- "batch_no": "batch_no"
+ doclist = get_mapped_doc(
+ "Purchase Receipt",
+ source_name,
+ {
+ "Purchase Receipt": {
+ "doctype": "Stock Entry",
+ },
+ "Purchase Receipt Item": {
+ "doctype": "Stock Entry Detail",
+ "field_map": {
+ "warehouse": "s_warehouse",
+ "parent": "reference_purchase_receipt",
+ "batch_no": "batch_no",
+ },
},
},
- }, target_doc, set_missing_values)
+ target_doc,
+ set_missing_values,
+ )
return doclist
+
@frappe.whitelist()
def make_inter_company_delivery_note(source_name, target_doc=None):
return make_inter_company_transaction("Purchase Receipt", source_name, target_doc)
+
def get_item_account_wise_additional_cost(purchase_document):
- landed_cost_vouchers = frappe.get_all("Landed Cost Purchase Receipt", fields=["parent"],
- filters = {"receipt_document": purchase_document, "docstatus": 1})
+ landed_cost_vouchers = frappe.get_all(
+ "Landed Cost Purchase Receipt",
+ fields=["parent"],
+ filters={"receipt_document": purchase_document, "docstatus": 1},
+ )
if not landed_cost_vouchers:
return
@@ -861,9 +1037,9 @@ def get_item_account_wise_additional_cost(purchase_document):
for lcv in landed_cost_vouchers:
landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent)
- #Use amount field for total item cost for manually cost distributed LCVs
- if landed_cost_voucher_doc.distribute_charges_based_on == 'Distribute Manually':
- based_on_field = 'amount'
+ # Use amount field for total item cost for manually cost distributed LCVs
+ if landed_cost_voucher_doc.distribute_charges_based_on == "Distribute Manually":
+ based_on_field = "amount"
else:
based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
@@ -876,15 +1052,20 @@ def get_item_account_wise_additional_cost(purchase_document):
if item.receipt_document == purchase_document:
for account in landed_cost_voucher_doc.taxes:
item_account_wise_cost.setdefault((item.item_code, item.purchase_receipt_item), {})
- item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(account.expense_account, {
- "amount": 0.0,
- "base_amount": 0.0
- })
+ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(
+ account.expense_account, {"amount": 0.0, "base_amount": 0.0}
+ )
- item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account]["amount"] += \
- account.amount * item.get(based_on_field) / total_item_cost
+ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][
+ "amount"
+ ] += (account.amount * item.get(based_on_field) / total_item_cost)
- item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account]["base_amount"] += \
- account.base_amount * item.get(based_on_field) / total_item_cost
+ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][
+ "base_amount"
+ ] += (account.base_amount * item.get(based_on_field) / total_item_cost)
return item_account_wise_cost
+
+
+def on_doctype_update():
+ frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"])
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py
index 18da88c3753..06ba9365561 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py
@@ -1,38 +1,25 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'purchase_receipt_no',
- 'non_standard_fieldnames': {
- 'Purchase Invoice': 'purchase_receipt',
- 'Asset': 'purchase_receipt',
- 'Landed Cost Voucher': 'receipt_document',
- 'Auto Repeat': 'reference_document',
- 'Purchase Receipt': 'return_against'
+ "fieldname": "purchase_receipt_no",
+ "non_standard_fieldnames": {
+ "Purchase Invoice": "purchase_receipt",
+ "Asset": "purchase_receipt",
+ "Landed Cost Voucher": "receipt_document",
+ "Auto Repeat": "reference_document",
+ "Purchase Receipt": "return_against",
},
- 'internal_links': {
- 'Purchase Order': ['items', 'purchase_order'],
- 'Project': ['items', 'project'],
- 'Quality Inspection': ['items', 'quality_inspection'],
+ "internal_links": {
+ "Purchase Order": ["items", "purchase_order"],
+ "Project": ["items", "project"],
+ "Quality Inspection": ["items", "quality_inspection"],
},
- 'transactions': [
- {
- 'label': _('Related'),
- 'items': ['Purchase Invoice', 'Landed Cost Voucher', 'Asset']
- },
- {
- 'label': _('Reference'),
- 'items': ['Purchase Order', 'Quality Inspection', 'Project']
- },
- {
- 'label': _('Returns'),
- 'items': ['Purchase Receipt']
- },
- {
- 'label': _('Subscription'),
- 'items': ['Auto Repeat']
- },
- ]
+ "transactions": [
+ {"label": _("Related"), "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset"]},
+ {"label": _("Reference"), "items": ["Purchase Order", "Quality Inspection", "Project"]},
+ {"label": _("Returns"), "items": ["Purchase Receipt"]},
+ {"label": _("Subscription"), "items": ["Auto Repeat"]},
+ ],
}
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index b13d6d3d05a..65c30de0978 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -7,6 +7,7 @@ import unittest
from collections import defaultdict
import frappe
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, cint, cstr, flt, today
from six import iteritems
@@ -18,24 +19,19 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchas
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction
-from erpnext.tests.utils import ERPNextTestCase, change_settings
-class TestPurchaseReceipt(ERPNextTestCase):
+class TestPurchaseReceipt(FrappeTestCase):
def setUp(self):
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
def test_purchase_receipt_received_qty(self):
"""
- 1. Test if received qty is validated against accepted + rejected
- 2. Test if received qty is auto set on save
+ 1. Test if received qty is validated against accepted + rejected
+ 2. Test if received qty is auto set on save
"""
pr = make_purchase_receipt(
- qty=1,
- rejected_qty=1,
- received_qty=3,
- item_code="_Test Item Home Desktop 200",
- do_not_save=True
+ qty=1, rejected_qty=1, received_qty=3, item_code="_Test Item Home Desktop 200", do_not_save=True
)
self.assertRaises(QtyMismatchError, pr.save)
@@ -52,11 +48,8 @@ class TestPurchaseReceipt(ERPNextTestCase):
sl_entry = frappe.db.get_all(
"Stock Ledger Entry",
- {
- "voucher_type": "Purchase Receipt",
- "voucher_no": pr.name
- },
- ['actual_qty']
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ ["actual_qty"],
)
self.assertEqual(len(sl_entry), 1)
@@ -66,47 +59,44 @@ class TestPurchaseReceipt(ERPNextTestCase):
sl_entry_cancelled = frappe.db.get_all(
"Stock Ledger Entry",
- {
- "voucher_type": "Purchase Receipt",
- "voucher_no": pr.name
- },
- ['actual_qty'],
- order_by='creation'
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ ["actual_qty"],
+ order_by="creation",
)
self.assertEqual(len(sl_entry_cancelled), 2)
self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5)
def test_make_purchase_invoice(self):
- if not frappe.db.exists('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice'):
- frappe.get_doc({
- 'doctype': 'Payment Terms Template',
- 'template_name': '_Test Payment Terms Template For Purchase Invoice',
- 'allocate_payment_based_on_payment_terms': 1,
- 'terms': [
- {
- 'doctype': 'Payment Terms Template Detail',
- 'invoice_portion': 50.00,
- 'credit_days_based_on': 'Day(s) after invoice date',
- 'credit_days': 00
- },
- {
- 'doctype': 'Payment Terms Template Detail',
- 'invoice_portion': 50.00,
- 'credit_days_based_on': 'Day(s) after invoice date',
- 'credit_days': 30
- }]
- }).insert()
+ if not frappe.db.exists(
+ "Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice"
+ ):
+ frappe.get_doc(
+ {
+ "doctype": "Payment Terms Template",
+ "template_name": "_Test Payment Terms Template For Purchase Invoice",
+ "allocate_payment_based_on_payment_terms": 1,
+ "terms": [
+ {
+ "doctype": "Payment Terms Template Detail",
+ "invoice_portion": 50.00,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": 00,
+ },
+ {
+ "doctype": "Payment Terms Template Detail",
+ "invoice_portion": 50.00,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": 30,
+ },
+ ],
+ }
+ ).insert()
template = frappe.db.get_value(
- "Payment Terms Template",
- "_Test Payment Terms Template For Purchase Invoice"
- )
- old_template_in_supplier = frappe.db.get_value(
- "Supplier",
- "_Test Supplier",
- "payment_terms"
+ "Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice"
)
+ old_template_in_supplier = frappe.db.get_value("Supplier", "_Test Supplier", "payment_terms")
frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", template)
pr = make_purchase_receipt(do_not_save=True)
@@ -124,23 +114,17 @@ class TestPurchaseReceipt(ERPNextTestCase):
# test if payment terms are fetched and set in PI
self.assertEqual(pi.payment_terms_template, template)
- self.assertEqual(pi.payment_schedule[0].payment_amount, flt(pi.grand_total)/2)
+ self.assertEqual(pi.payment_schedule[0].payment_amount, flt(pi.grand_total) / 2)
self.assertEqual(pi.payment_schedule[0].invoice_portion, 50)
- self.assertEqual(pi.payment_schedule[1].payment_amount, flt(pi.grand_total)/2)
+ self.assertEqual(pi.payment_schedule[1].payment_amount, flt(pi.grand_total) / 2)
self.assertEqual(pi.payment_schedule[1].invoice_portion, 50)
# teardown
- pi.delete() # draft PI
+ pi.delete() # draft PI
pr.cancel()
- frappe.db.set_value(
- "Supplier",
- "_Test Supplier",
- "payment_terms",
- old_template_in_supplier
- )
+ frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", old_template_in_supplier)
frappe.get_doc(
- "Payment Terms Template",
- "_Test Payment Terms Template For Purchase Invoice"
+ "Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice"
).delete()
def test_purchase_receipt_no_gl_entry(self):
@@ -148,27 +132,19 @@ class TestPurchaseReceipt(ERPNextTestCase):
existing_bin_qty, existing_bin_stock_value = frappe.db.get_value(
"Bin",
- {
- "item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC"
- },
- ["actual_qty", "stock_value"]
+ {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
+ ["actual_qty", "stock_value"],
)
if existing_bin_qty < 0:
make_stock_entry(
- item_code="_Test Item",
- target="_Test Warehouse - _TC",
- qty=abs(existing_bin_qty)
+ item_code="_Test Item", target="_Test Warehouse - _TC", qty=abs(existing_bin_qty)
)
existing_bin_qty, existing_bin_stock_value = frappe.db.get_value(
"Bin",
- {
- "item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC"
- },
- ["actual_qty", "stock_value"]
+ {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
+ ["actual_qty", "stock_value"],
)
pr = make_purchase_receipt()
@@ -179,20 +155,15 @@ class TestPurchaseReceipt(ERPNextTestCase):
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC"
+ "warehouse": "_Test Warehouse - _TC",
},
- "stock_value_difference"
+ "stock_value_difference",
)
self.assertEqual(stock_value_difference, 250)
current_bin_stock_value = frappe.db.get_value(
- "Bin",
- {
- "item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC"
- },
- "stock_value"
+ "Bin", {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, "stock_value"
)
self.assertEqual(current_bin_stock_value, existing_bin_stock_value + 250)
@@ -201,7 +172,7 @@ class TestPurchaseReceipt(ERPNextTestCase):
pr.cancel()
def test_batched_serial_no_purchase(self):
- item = frappe.db.exists("Item", {'item_name': 'Batched Serialized Item'})
+ item = frappe.db.exists("Item", {"item_name": "Batched Serialized Item"})
if not item:
item = create_item("Batched Serialized Item")
item.has_batch_no = 1
@@ -211,34 +182,30 @@ class TestPurchaseReceipt(ERPNextTestCase):
item.serial_no_series = "BS-.####"
item.save()
else:
- item = frappe.get_doc("Item", {'item_name': 'Batched Serialized Item'})
+ item = frappe.get_doc("Item", {"item_name": "Batched Serialized Item"})
pr = make_purchase_receipt(item_code=item.name, qty=5, rate=500)
- self.assertTrue(
- frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name})
- )
+ self.assertTrue(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name}))
pr.load_from_db()
batch_no = pr.items[0].batch_no
pr.cancel()
- self.assertFalse(
- frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name})
- )
- self.assertFalse(frappe.db.get_all('Serial No', {'batch_no': batch_no}))
+ self.assertFalse(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name}))
+ self.assertFalse(frappe.db.get_all("Serial No", {"batch_no": batch_no}))
def test_duplicate_serial_nos(self):
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
- item = frappe.db.exists("Item", {'item_name': 'Test Serialized Item 123'})
+ item = frappe.db.exists("Item", {"item_name": "Test Serialized Item 123"})
if not item:
item = create_item("Test Serialized Item 123")
item.has_serial_no = 1
item.serial_no_series = "TSI123-.####"
item.save()
else:
- item = frappe.get_doc("Item", {'item_name': 'Test Serialized Item 123'})
+ item = frappe.get_doc("Item", {"item_name": "Test Serialized Item 123"})
# First make purchase receipt
pr = make_purchase_receipt(item_code=item.name, qty=2, rate=500)
@@ -246,12 +213,8 @@ class TestPurchaseReceipt(ERPNextTestCase):
serial_nos = frappe.db.get_value(
"Stock Ledger Entry",
- {
- "voucher_type": "Purchase Receipt",
- "voucher_no": pr.name,
- "item_code": item.name
- },
- "serial_no"
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "item_code": item.name},
+ "serial_no",
)
serial_nos = get_serial_nos(serial_nos)
@@ -263,21 +226,16 @@ class TestPurchaseReceipt(ERPNextTestCase):
item_code=item.name,
qty=2,
rate=500,
- serial_no='\n'.join(serial_nos),
- company='_Test Company 1',
+ serial_no="\n".join(serial_nos),
+ company="_Test Company 1",
do_not_submit=True,
- warehouse = 'Stores - _TC1'
+ warehouse="Stores - _TC1",
)
self.assertRaises(SerialNoDuplicateError, pr_different_company.submit)
# Then made delivery note to remove the serial nos from stock
- dn = create_delivery_note(
- item_code=item.name,
- qty=2,
- rate=1500,
- serial_no='\n'.join(serial_nos)
- )
+ dn = create_delivery_note(item_code=item.name, qty=2, rate=1500, serial_no="\n".join(serial_nos))
dn.load_from_db()
self.assertEquals(get_serial_nos(dn.items[0].serial_no), serial_nos)
@@ -289,8 +247,8 @@ class TestPurchaseReceipt(ERPNextTestCase):
qty=2,
rate=500,
posting_date=posting_date,
- serial_no='\n'.join(serial_nos),
- do_not_submit=True
+ serial_no="\n".join(serial_nos),
+ do_not_submit=True,
)
self.assertRaises(SerialNoExistsInFutureTransaction, pr1.submit)
@@ -301,29 +259,28 @@ class TestPurchaseReceipt(ERPNextTestCase):
qty=2,
rate=500,
posting_date=posting_date,
- serial_no='\n'.join(serial_nos),
+ serial_no="\n".join(serial_nos),
company="_Test Company 1",
do_not_submit=True,
- warehouse="Stores - _TC1"
+ warehouse="Stores - _TC1",
)
self.assertRaises(SerialNoExistsInFutureTransaction, pr2.submit)
# Receive the same serial nos after the delivery note posting date and time
- make_purchase_receipt(
- item_code=item.name,
- qty=2,
- rate=500,
- serial_no='\n'.join(serial_nos)
- )
+ make_purchase_receipt(item_code=item.name, qty=2, rate=500, serial_no="\n".join(serial_nos))
# Raise the error for backdated deliver note entry cancel
self.assertRaises(SerialNoExistsInFutureTransaction, dn.cancel)
def test_purchase_receipt_gl_entry(self):
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1",
- get_multiple_items = True, get_taxes_and_charges = True)
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ supplier_warehouse="Work in Progress - TCP1",
+ get_multiple_items=True,
+ get_taxes_and_charges=True,
+ )
self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1)
@@ -339,14 +296,14 @@ class TestPurchaseReceipt(ERPNextTestCase):
stock_in_hand_account: [750.0, 0.0],
"Stock Received But Not Billed - TCP1": [0.0, 500.0],
"_Test Account Shipping Charges - TCP1": [0.0, 100.0],
- "_Test Account Customs Duty - TCP1": [0.0, 150.0]
+ "_Test Account Customs Duty - TCP1": [0.0, 150.0],
}
else:
expected_values = {
stock_in_hand_account: [375.0, 0.0],
fixed_asset_account: [375.0, 0.0],
"Stock Received But Not Billed - TCP1": [0.0, 500.0],
- "_Test Account Shipping Charges - TCP1": [0.0, 250.0]
+ "_Test Account Shipping Charges - TCP1": [0.0, 250.0],
}
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.debit)
@@ -359,22 +316,19 @@ class TestPurchaseReceipt(ERPNextTestCase):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
frappe.db.set_value(
- "Buying Settings", None,
- "backflush_raw_materials_of_subcontract_based_on", "BOM"
+ "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM"
)
make_stock_entry(
- item_code="_Test Item", qty=100,
- target="_Test Warehouse 1 - _TC", basic_rate=100
+ item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100
)
make_stock_entry(
- item_code="_Test Item Home Desktop 100", qty=100,
- target="_Test Warehouse 1 - _TC", basic_rate=100
- )
- pr = make_purchase_receipt(
- item_code="_Test FG Item", qty=10,
- rate=500, is_subcontracted="Yes"
+ item_code="_Test Item Home Desktop 100",
+ qty=100,
+ target="_Test Warehouse 1 - _TC",
+ basic_rate=100,
)
+ pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted="Yes")
self.assertEqual(len(pr.get("supplied_items")), 2)
rm_supp_cost = sum(d.amount for d in pr.get("supplied_items"))
@@ -384,32 +338,35 @@ class TestPurchaseReceipt(ERPNextTestCase):
def test_subcontracting_gle_fg_item_rate_zero(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
frappe.db.set_value(
- "Buying Settings", None,
- "backflush_raw_materials_of_subcontract_based_on", "BOM"
+ "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM"
)
se1 = make_stock_entry(
item_code="_Test Item",
target="Work In Progress - TCP1",
- qty=100, basic_rate=100,
- company="_Test Company with perpetual inventory"
+ qty=100,
+ basic_rate=100,
+ company="_Test Company with perpetual inventory",
)
se2 = make_stock_entry(
item_code="_Test Item Home Desktop 100",
target="Work In Progress - TCP1",
- qty=100, basic_rate=100,
- company="_Test Company with perpetual inventory"
+ qty=100,
+ basic_rate=100,
+ company="_Test Company with perpetual inventory",
)
pr = make_purchase_receipt(
item_code="_Test FG Item",
- qty=10, rate=0,
+ qty=10,
+ rate=0,
is_subcontracted="Yes",
company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1",
- supplier_warehouse="Work In Progress - TCP1"
+ supplier_warehouse="Work In Progress - TCP1",
)
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
@@ -422,9 +379,9 @@ class TestPurchaseReceipt(ERPNextTestCase):
def test_subcontracting_over_receipt(self):
"""
- Behaviour: Raise multiple PRs against one PO that in total
- receive more than the required qty in the PO.
- Expected Result: Error Raised for Over Receipt against PO.
+ Behaviour: Raise multiple PRs against one PO that in total
+ receive more than the required qty in the PO.
+ Expected Result: Error Raised for Over Receipt against PO.
"""
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.buying.doctype.purchase_order.purchase_order import (
@@ -443,26 +400,21 @@ class TestPurchaseReceipt(ERPNextTestCase):
po = create_purchase_order(
item_code=item_code,
- qty=1, include_exploded_items=0,
+ qty=1,
+ include_exploded_items=0,
is_subcontracted="Yes",
- supplier_warehouse="_Test Warehouse 1 - _TC"
+ supplier_warehouse="_Test Warehouse 1 - _TC",
)
# stock raw materials in a warehouse before transfer
se1 = make_stock_entry(
- target="_Test Warehouse - _TC",
- item_code="Test Extra Item 1",
- qty=10, basic_rate=100
+ target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=10, basic_rate=100
)
se2 = make_stock_entry(
- target="_Test Warehouse - _TC",
- item_code="_Test FG Item",
- qty=1, basic_rate=100
+ target="_Test Warehouse - _TC", item_code="_Test FG Item", qty=1, basic_rate=100
)
se3 = make_stock_entry(
- target="_Test Warehouse - _TC",
- item_code="Test Extra Item 2",
- qty=1, basic_rate=100
+ target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=1, basic_rate=100
)
rm_items = [
@@ -472,7 +424,7 @@ class TestPurchaseReceipt(ERPNextTestCase):
"item_name": "_Test FG Item",
"qty": po.supplied_items[0].required_qty,
"warehouse": "_Test Warehouse - _TC",
- "stock_uom": "Nos"
+ "stock_uom": "Nos",
},
{
"item_code": item_code,
@@ -480,8 +432,8 @@ class TestPurchaseReceipt(ERPNextTestCase):
"item_name": "Test Extra Item 1",
"qty": po.supplied_items[1].required_qty,
"warehouse": "_Test Warehouse - _TC",
- "stock_uom": "Nos"
- }
+ "stock_uom": "Nos",
+ },
]
rm_item_string = json.dumps(rm_items)
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
@@ -503,8 +455,9 @@ class TestPurchaseReceipt(ERPNextTestCase):
po.reload()
pr2.load_from_db()
- if pr2.docstatus == 1 and frappe.db.get_value('Stock Ledger Entry',
- {'voucher_no': pr2.name, 'is_cancelled': 0}, 'name'):
+ if pr2.docstatus == 1 and frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "name"
+ ):
pr2.cancel()
po.load_from_db()
@@ -514,15 +467,10 @@ class TestPurchaseReceipt(ERPNextTestCase):
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
pr_row_1_serial_no = pr.get("items")[0].serial_no
- self.assertEqual(
- frappe.db.get_value("Serial No", pr_row_1_serial_no, "supplier"),
- pr.supplier
- )
+ self.assertEqual(frappe.db.get_value("Serial No", pr_row_1_serial_no, "supplier"), pr.supplier)
pr.cancel()
- self.assertFalse(
- frappe.db.get_value("Serial No", pr_row_1_serial_no, "warehouse")
- )
+ self.assertFalse(frappe.db.get_value("Serial No", pr_row_1_serial_no, "warehouse"))
def test_rejected_serial_no(self):
pr = frappe.copy_doc(test_records[0])
@@ -537,32 +485,34 @@ class TestPurchaseReceipt(ERPNextTestCase):
accepted_serial_nos = pr.get("items")[0].serial_no.split("\n")
self.assertEqual(len(accepted_serial_nos), 3)
for serial_no in accepted_serial_nos:
- self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"),
- pr.get("items")[0].warehouse)
+ self.assertEqual(
+ frappe.db.get_value("Serial No", serial_no, "warehouse"), pr.get("items")[0].warehouse
+ )
rejected_serial_nos = pr.get("items")[0].rejected_serial_no.split("\n")
self.assertEqual(len(rejected_serial_nos), 2)
for serial_no in rejected_serial_nos:
- self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"),
- pr.get("items")[0].rejected_warehouse)
+ self.assertEqual(
+ frappe.db.get_value("Serial No", serial_no, "warehouse"), pr.get("items")[0].rejected_warehouse
+ )
pr.cancel()
def test_purchase_return_partial(self):
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1",
- supplier_warehouse = "Work in Progress - TCP1"
+ warehouse="Stores - TCP1",
+ supplier_warehouse="Work in Progress - TCP1",
)
return_pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1",
- supplier_warehouse = "Work in Progress - TCP1",
+ warehouse="Stores - TCP1",
+ supplier_warehouse="Work in Progress - TCP1",
is_return=1,
return_against=pr.name,
qty=-2,
- do_not_submit=1
+ do_not_submit=1,
)
return_pr.items[0].purchase_receipt_item = pr.items[0].name
return_pr.submit()
@@ -570,16 +520,12 @@ class TestPurchaseReceipt(ERPNextTestCase):
# check sle
outgoing_rate = frappe.db.get_value(
"Stock Ledger Entry",
- {
- "voucher_type": "Purchase Receipt",
- "voucher_no": return_pr.name
- },
- "outgoing_rate"
+ {"voucher_type": "Purchase Receipt", "voucher_no": return_pr.name},
+ "outgoing_rate",
)
self.assertEqual(outgoing_rate, 50)
-
# check gl entries for return
gl_entries = get_gl_entries("Purchase Receipt", return_pr.name)
@@ -605,6 +551,7 @@ class TestPurchaseReceipt(ERPNextTestCase):
self.assertEqual(pr.per_returned, 40)
from erpnext.controllers.sales_and_purchase_return import make_return_doc
+
return_pr_2 = make_return_doc("Purchase Receipt", pr.name)
# Check if unreturned amount is mapped in 2nd return
@@ -627,7 +574,7 @@ class TestPurchaseReceipt(ERPNextTestCase):
# PR should be completed on billing all unreturned amount
self.assertEqual(pr.items[0].billed_amt, 150)
self.assertEqual(pr.per_billed, 100)
- self.assertEqual(pr.status, 'Completed')
+ self.assertEqual(pr.status, "Completed")
pi.load_from_db()
pi.cancel()
@@ -641,18 +588,18 @@ class TestPurchaseReceipt(ERPNextTestCase):
def test_purchase_return_full(self):
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1",
- supplier_warehouse = "Work in Progress - TCP1"
+ warehouse="Stores - TCP1",
+ supplier_warehouse="Work in Progress - TCP1",
)
return_pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1",
- supplier_warehouse = "Work in Progress - TCP1",
+ warehouse="Stores - TCP1",
+ supplier_warehouse="Work in Progress - TCP1",
is_return=1,
return_against=pr.name,
qty=-5,
- do_not_submit=1
+ do_not_submit=1,
)
return_pr.items[0].purchase_receipt_item = pr.items[0].name
return_pr.submit()
@@ -665,7 +612,7 @@ class TestPurchaseReceipt(ERPNextTestCase):
# Check if Original PR updated
self.assertEqual(pr.items[0].returned_qty, 5)
self.assertEqual(pr.per_returned, 100)
- self.assertEqual(pr.status, 'Return Issued')
+ self.assertEqual(pr.status, "Return Issued")
return_pr.cancel()
pr.cancel()
@@ -673,32 +620,32 @@ class TestPurchaseReceipt(ERPNextTestCase):
def test_purchase_return_for_rejected_qty(self):
from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
- rejected_warehouse="_Test Rejected Warehouse - TCP1"
+ rejected_warehouse = "_Test Rejected Warehouse - TCP1"
if not frappe.db.exists("Warehouse", rejected_warehouse):
get_warehouse(
- company = "_Test Company with perpetual inventory",
- abbr = " - TCP1",
- warehouse_name = "_Test Rejected Warehouse"
+ company="_Test Company with perpetual inventory",
+ abbr=" - TCP1",
+ warehouse_name="_Test Rejected Warehouse",
).name
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1",
- supplier_warehouse = "Work in Progress - TCP1",
+ warehouse="Stores - TCP1",
+ supplier_warehouse="Work in Progress - TCP1",
qty=2,
rejected_qty=2,
- rejected_warehouse=rejected_warehouse
+ rejected_warehouse=rejected_warehouse,
)
return_pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1",
- supplier_warehouse = "Work in Progress - TCP1",
+ warehouse="Stores - TCP1",
+ supplier_warehouse="Work in Progress - TCP1",
is_return=1,
return_against=pr.name,
qty=-2,
- rejected_qty = -2,
- rejected_warehouse=rejected_warehouse
+ rejected_qty=-2,
+ rejected_warehouse=rejected_warehouse,
)
actual_qty = frappe.db.get_value(
@@ -706,9 +653,9 @@ class TestPurchaseReceipt(ERPNextTestCase):
{
"voucher_type": "Purchase Receipt",
"voucher_no": return_pr.name,
- "warehouse": return_pr.items[0].rejected_warehouse
+ "warehouse": return_pr.items[0].rejected_warehouse,
},
- "actual_qty"
+ "actual_qty",
)
self.assertEqual(actual_qty, -2)
@@ -716,6 +663,44 @@ class TestPurchaseReceipt(ERPNextTestCase):
return_pr.cancel()
pr.cancel()
+ def test_purchase_receipt_for_rejected_gle_without_accepted_warehouse(self):
+ from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
+
+ rejected_warehouse = "_Test Rejected Warehouse - TCP1"
+ if not frappe.db.exists("Warehouse", rejected_warehouse):
+ get_warehouse(
+ company="_Test Company with perpetual inventory",
+ abbr=" - TCP1",
+ warehouse_name="_Test Rejected Warehouse",
+ ).name
+
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ received_qty=2,
+ rejected_qty=2,
+ rejected_warehouse=rejected_warehouse,
+ do_not_save=True,
+ )
+
+ pr.items[0].qty = 0.0
+ pr.items[0].warehouse = ""
+ pr.submit()
+
+ actual_qty = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": pr.name,
+ "warehouse": pr.items[0].rejected_warehouse,
+ "is_cancelled": 0,
+ },
+ "actual_qty",
+ )
+
+ self.assertEqual(actual_qty, 2)
+ self.assertFalse(pr.items[0].warehouse)
+ pr.cancel()
def test_purchase_return_for_serialized_items(self):
def _check_serial_no_values(serial_no, field_values):
@@ -729,24 +714,22 @@ class TestPurchaseReceipt(ERPNextTestCase):
serial_no = get_serial_nos(pr.get("items")[0].serial_no)[0]
- _check_serial_no_values(serial_no, {
- "warehouse": "_Test Warehouse - _TC",
- "purchase_document_no": pr.name
- })
+ _check_serial_no_values(
+ serial_no, {"warehouse": "_Test Warehouse - _TC", "purchase_document_no": pr.name}
+ )
return_pr = make_purchase_receipt(
item_code="_Test Serialized Item With Series",
qty=-1,
is_return=1,
return_against=pr.name,
- serial_no=serial_no
+ serial_no=serial_no,
)
- _check_serial_no_values(serial_no, {
- "warehouse": "",
- "purchase_document_no": pr.name,
- "delivery_document_no": return_pr.name
- })
+ _check_serial_no_values(
+ serial_no,
+ {"warehouse": "", "purchase_document_no": pr.name, "delivery_document_no": return_pr.name},
+ )
return_pr.cancel()
pr.reload()
@@ -754,20 +737,12 @@ class TestPurchaseReceipt(ERPNextTestCase):
def test_purchase_return_for_multi_uom(self):
item_code = "_Test Purchase Return For Multi-UOM"
- if not frappe.db.exists('Item', item_code):
- item = make_item(item_code, {'stock_uom': 'Box'})
- row = item.append('uoms', {
- 'uom': 'Unit',
- 'conversion_factor': 0.1
- })
+ if not frappe.db.exists("Item", item_code):
+ item = make_item(item_code, {"stock_uom": "Box"})
+ row = item.append("uoms", {"uom": "Unit", "conversion_factor": 0.1})
row.db_update()
- pr = make_purchase_receipt(
- item_code=item_code,
- qty=1,
- uom="Box",
- conversion_factor=1.0
- )
+ pr = make_purchase_receipt(item_code=item_code, qty=1, uom="Box", conversion_factor=1.0)
return_pr = make_purchase_receipt(
item_code=item_code,
qty=-10,
@@ -775,7 +750,7 @@ class TestPurchaseReceipt(ERPNextTestCase):
stock_uom="Box",
conversion_factor=0.1,
is_return=1,
- return_against=pr.name
+ return_against=pr.name,
)
self.assertEqual(abs(return_pr.items[0].stock_qty), 1.0)
@@ -788,22 +763,19 @@ class TestPurchaseReceipt(ERPNextTestCase):
update_purchase_receipt_status,
)
- pr = make_purchase_receipt(do_not_submit=True)
- pr.submit()
+ pr = make_purchase_receipt()
update_purchase_receipt_status(pr.name, "Closed")
- self.assertEqual(
- frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed"
- )
+ self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed")
pr.reload()
pr.cancel()
def test_pr_billing_status(self):
"""Flow:
- 1. PO -> PR1 -> PI
- 2. PO -> PI
- 3. PO -> PR2.
+ 1. PO -> PR1 -> PI
+ 2. PO -> PI
+ 3. PO -> PR2.
"""
from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_invoice as make_purchase_invoice_from_po,
@@ -865,19 +837,15 @@ class TestPurchaseReceipt(ERPNextTestCase):
item = make_item(item_code, dict(has_serial_no=1))
serial_no = "12903812901"
- pr_doc = make_purchase_receipt(item_code=item_code,
- qty=1, serial_no = serial_no)
+ pr_doc = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no)
self.assertEqual(
serial_no,
frappe.db.get_value(
"Serial No",
- {
- "purchase_document_type": "Purchase Receipt",
- "purchase_document_no": pr_doc.name
- },
- "name"
- )
+ {"purchase_document_type": "Purchase Receipt", "purchase_document_no": pr_doc.name},
+ "name",
+ ),
)
pr_doc.cancel()
@@ -894,12 +862,9 @@ class TestPurchaseReceipt(ERPNextTestCase):
serial_no,
frappe.db.get_value(
"Serial No",
- {
- "purchase_document_type": "Purchase Receipt",
- "purchase_document_no": new_pr_doc.name
- },
- "name"
- )
+ {"purchase_document_type": "Purchase Receipt", "purchase_document_no": new_pr_doc.name},
+ "name",
+ ),
)
new_pr_doc.cancel()
@@ -907,40 +872,52 @@ class TestPurchaseReceipt(ERPNextTestCase):
def test_auto_asset_creation(self):
asset_item = "Test Asset Item"
- if not frappe.db.exists('Item', asset_item):
- asset_category = frappe.get_all('Asset Category')
+ if not frappe.db.exists("Item", asset_item):
+ asset_category = frappe.get_all("Asset Category")
if asset_category:
asset_category = asset_category[0].name
if not asset_category:
- doc = frappe.get_doc({
- 'doctype': 'Asset Category',
- 'asset_category_name': 'Test Asset Category',
- 'depreciation_method': 'Straight Line',
- 'total_number_of_depreciations': 12,
- 'frequency_of_depreciation': 1,
- 'accounts': [{
- 'company_name': '_Test Company',
- 'fixed_asset_account': '_Test Fixed Asset - _TC',
- 'accumulated_depreciation_account': '_Test Accumulated Depreciations - _TC',
- 'depreciation_expense_account': '_Test Depreciations - _TC'
- }]
- }).insert()
+ doc = frappe.get_doc(
+ {
+ "doctype": "Asset Category",
+ "asset_category_name": "Test Asset Category",
+ "depreciation_method": "Straight Line",
+ "total_number_of_depreciations": 12,
+ "frequency_of_depreciation": 1,
+ "accounts": [
+ {
+ "company_name": "_Test Company",
+ "fixed_asset_account": "_Test Fixed Asset - _TC",
+ "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC",
+ "depreciation_expense_account": "_Test Depreciations - _TC",
+ }
+ ],
+ }
+ ).insert()
asset_category = doc.name
- item_data = make_item(asset_item, {'is_stock_item':0,
- 'stock_uom': 'Box', 'is_fixed_asset': 1, 'auto_create_assets': 1,
- 'asset_category': asset_category, 'asset_naming_series': 'ABC.###'})
+ item_data = make_item(
+ asset_item,
+ {
+ "is_stock_item": 0,
+ "stock_uom": "Box",
+ "is_fixed_asset": 1,
+ "auto_create_assets": 1,
+ "asset_category": asset_category,
+ "asset_naming_series": "ABC.###",
+ },
+ )
asset_item = item_data.item_code
pr = make_purchase_receipt(item_code=asset_item, qty=3)
- assets = frappe.db.get_all('Asset', filters={'purchase_receipt': pr.name})
+ assets = frappe.db.get_all("Asset", filters={"purchase_receipt": pr.name})
self.assertEqual(len(assets), 3)
- location = frappe.db.get_value('Asset', assets[0].name, 'location')
+ location = frappe.db.get_value("Asset", assets[0].name, "location")
self.assertEqual(location, "Test Location")
pr.cancel()
@@ -950,17 +927,18 @@ class TestPurchaseReceipt(ERPNextTestCase):
pr = make_purchase_receipt(item_code="Test Asset Item", qty=1)
- asset = frappe.get_doc("Asset", {
- 'purchase_receipt': pr.name
- })
+ asset = frappe.get_doc("Asset", {"purchase_receipt": pr.name})
asset.available_for_use_date = frappe.utils.nowdate()
asset.gross_purchase_amount = 50.0
- asset.append("finance_books", {
- "expected_value_after_useful_life": 10,
- "depreciation_method": "Straight Line",
- "total_number_of_depreciations": 3,
- "frequency_of_depreciation": 1
- })
+ asset.append(
+ "finance_books",
+ {
+ "expected_value_after_useful_life": 10,
+ "depreciation_method": "Straight Line",
+ "total_number_of_depreciations": 3,
+ "frequency_of_depreciation": 1,
+ },
+ )
asset.submit()
pr_return = make_purchase_return(pr.name)
@@ -980,36 +958,27 @@ class TestPurchaseReceipt(ERPNextTestCase):
cost_center = "_Test Cost Center for BS Account - TCP1"
create_cost_center(
cost_center_name="_Test Cost Center for BS Account",
- company="_Test Company with perpetual inventory"
+ company="_Test Company with perpetual inventory",
)
- if not frappe.db.exists('Location', 'Test Location'):
- frappe.get_doc({
- 'doctype': 'Location',
- 'location_name': 'Test Location'
- }).insert()
+ if not frappe.db.exists("Location", "Test Location"):
+ frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert()
pr = make_purchase_receipt(
cost_center=cost_center,
company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1",
- supplier_warehouse = "Work in Progress - TCP1"
+ warehouse="Stores - TCP1",
+ supplier_warehouse="Work in Progress - TCP1",
)
- stock_in_hand_account = get_inventory_account(
- pr.company, pr.get("items")[0].warehouse
- )
+ stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse)
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
self.assertTrue(gl_entries)
expected_values = {
- "Stock Received But Not Billed - TCP1": {
- "cost_center": cost_center
- },
- stock_in_hand_account: {
- "cost_center": cost_center
- }
+ "Stock Received But Not Billed - TCP1": {"cost_center": cost_center},
+ stock_in_hand_account: {"cost_center": cost_center},
}
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
@@ -1017,33 +986,24 @@ class TestPurchaseReceipt(ERPNextTestCase):
pr.cancel()
def test_purchase_receipt_cost_center_with_balance_sheet_account(self):
- if not frappe.db.exists('Location', 'Test Location'):
- frappe.get_doc({
- 'doctype': 'Location',
- 'location_name': 'Test Location'
- }).insert()
+ if not frappe.db.exists("Location", "Test Location"):
+ frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert()
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1",
- supplier_warehouse = "Work in Progress - TCP1"
+ warehouse="Stores - TCP1",
+ supplier_warehouse="Work in Progress - TCP1",
)
- stock_in_hand_account = get_inventory_account(
- pr.company, pr.get("items")[0].warehouse
- )
+ stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse)
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
self.assertTrue(gl_entries)
- cost_center = pr.get('items')[0].cost_center
+ cost_center = pr.get("items")[0].cost_center
expected_values = {
- "Stock Received But Not Billed - TCP1": {
- "cost_center": cost_center
- },
- stock_in_hand_account: {
- "cost_center": cost_center
- }
+ "Stock Received But Not Billed - TCP1": {"cost_center": cost_center},
+ stock_in_hand_account: {"cost_center": cost_center},
}
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
@@ -1059,11 +1019,7 @@ class TestPurchaseReceipt(ERPNextTestCase):
po = create_purchase_order()
pr = create_pr_against_po(po.name)
- pr1 = make_purchase_receipt(
- qty=-1,
- is_return=1, return_against=pr.name,
- do_not_submit=True
- )
+ pr1 = make_purchase_receipt(qty=-1, is_return=1, return_against=pr.name, do_not_submit=True)
pr1.items[0].purchase_order = po.name
pr1.items[0].purchase_order_item = po.items[0].name
pr1.items[0].purchase_receipt_item = pr.items[0].name
@@ -1080,14 +1036,17 @@ class TestPurchaseReceipt(ERPNextTestCase):
def test_make_purchase_invoice_from_pr_with_returned_qty_duplicate_items(self):
pr1 = make_purchase_receipt(qty=8, do_not_submit=True)
- pr1.append("items", {
- "item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC",
- "qty": 1,
- "received_qty": 1,
- "rate": 100,
- "conversion_factor": 1.0,
- })
+ pr1.append(
+ "items",
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 1,
+ "received_qty": 1,
+ "rate": 100,
+ "conversion_factor": 1.0,
+ },
+ )
pr1.submit()
pi1 = make_purchase_invoice(pr1.name)
@@ -1096,11 +1055,7 @@ class TestPurchaseReceipt(ERPNextTestCase):
pi1.save()
pi1.submit()
- pr2 = make_purchase_receipt(
- qty=-2,
- is_return=1, return_against=pr1.name,
- do_not_submit=True
- )
+ pr2 = make_purchase_receipt(qty=-2, is_return=1, return_against=pr1.name, do_not_submit=True)
pr2.items[0].purchase_receipt_item = pr1.items[0].name
pr2.submit()
@@ -1114,26 +1069,25 @@ class TestPurchaseReceipt(ERPNextTestCase):
pr1.cancel()
def test_stock_transfer_from_purchase_receipt(self):
- pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1',
- company="_Test Company with perpetual inventory")
+ pr1 = make_purchase_receipt(
+ warehouse="Work In Progress - TCP1", company="_Test Company with perpetual inventory"
+ )
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1", do_not_save=1)
+ pr = make_purchase_receipt(
+ company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1
+ )
- pr.supplier_warehouse = ''
- pr.items[0].from_warehouse = 'Work In Progress - TCP1'
+ pr.supplier_warehouse = ""
+ pr.items[0].from_warehouse = "Work In Progress - TCP1"
pr.submit()
- gl_entries = get_gl_entries('Purchase Receipt', pr.name)
- sl_entries = get_sl_entries('Purchase Receipt', pr.name)
+ gl_entries = get_gl_entries("Purchase Receipt", pr.name)
+ sl_entries = get_sl_entries("Purchase Receipt", pr.name)
self.assertFalse(gl_entries)
- expected_sle = {
- 'Work In Progress - TCP1': -5,
- 'Stores - TCP1': 5
- }
+ expected_sle = {"Work In Progress - TCP1": -5, "Stores - TCP1": 5}
for sle in sl_entries:
self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty)
@@ -1145,48 +1099,45 @@ class TestPurchaseReceipt(ERPNextTestCase):
create_warehouse(
"_Test Warehouse for Valuation",
company="_Test Company with perpetual inventory",
- properties={"account": '_Test Account Stock In Hand - TCP1'}
+ properties={"account": "_Test Account Stock In Hand - TCP1"},
)
pr1 = make_purchase_receipt(
- warehouse = '_Test Warehouse for Valuation - TCP1',
- company="_Test Company with perpetual inventory"
+ warehouse="_Test Warehouse for Valuation - TCP1",
+ company="_Test Company with perpetual inventory",
)
pr = make_purchase_receipt(
- company="_Test Company with perpetual inventory",
- warehouse = "Stores - TCP1",
- do_not_save=1
+ company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1
)
- pr.items[0].from_warehouse = '_Test Warehouse for Valuation - TCP1'
- pr.supplier_warehouse = ''
+ pr.items[0].from_warehouse = "_Test Warehouse for Valuation - TCP1"
+ pr.supplier_warehouse = ""
-
- pr.append('taxes', {
- 'charge_type': 'On Net Total',
- 'account_head': '_Test Account Shipping Charges - TCP1',
- 'category': 'Valuation and Total',
- 'cost_center': 'Main - TCP1',
- 'description': 'Test',
- 'rate': 9
- })
+ pr.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Shipping Charges - TCP1",
+ "category": "Valuation and Total",
+ "cost_center": "Main - TCP1",
+ "description": "Test",
+ "rate": 9,
+ },
+ )
pr.submit()
- gl_entries = get_gl_entries('Purchase Receipt', pr.name)
- sl_entries = get_sl_entries('Purchase Receipt', pr.name)
+ gl_entries = get_gl_entries("Purchase Receipt", pr.name)
+ sl_entries = get_sl_entries("Purchase Receipt", pr.name)
expected_gle = [
- ['Stock In Hand - TCP1', 272.5, 0.0],
- ['_Test Account Stock In Hand - TCP1', 0.0, 250.0],
- ['_Test Account Shipping Charges - TCP1', 0.0, 22.5]
+ ["Stock In Hand - TCP1", 272.5, 0.0],
+ ["_Test Account Stock In Hand - TCP1", 0.0, 250.0],
+ ["_Test Account Shipping Charges - TCP1", 0.0, 22.5],
]
- expected_sle = {
- '_Test Warehouse for Valuation - TCP1': -5,
- 'Stores - TCP1': 5
- }
+ expected_sle = {"_Test Warehouse for Valuation - TCP1": -5, "Stores - TCP1": 5}
for sle in sl_entries:
self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty)
@@ -1199,7 +1150,6 @@ class TestPurchaseReceipt(ERPNextTestCase):
pr.cancel()
pr1.cancel()
-
def test_subcontracted_pr_for_multi_transfer_batches(self):
from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_receipt,
@@ -1214,49 +1164,57 @@ class TestPurchaseReceipt(ERPNextTestCase):
update_backflush_based_on("Material Transferred for Subcontract")
item_code = "_Test Subcontracted FG Item 3"
- make_item('Sub Contracted Raw Material 3', {
- 'is_stock_item': 1,
- 'is_sub_contracted_item': 1,
- 'has_batch_no': 1,
- 'create_new_batch': 1
- })
+ make_item(
+ "Sub Contracted Raw Material 3",
+ {"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1, "create_new_batch": 1},
+ )
- create_subcontracted_item(item_code=item_code, has_batch_no=1,
- raw_materials=["Sub Contracted Raw Material 3"])
+ create_subcontracted_item(
+ item_code=item_code, has_batch_no=1, raw_materials=["Sub Contracted Raw Material 3"]
+ )
order_qty = 500
- po = create_purchase_order(item_code=item_code, qty=order_qty,
- is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
+ po = create_purchase_order(
+ item_code=item_code,
+ qty=order_qty,
+ is_subcontracted="Yes",
+ supplier_warehouse="_Test Warehouse 1 - _TC",
+ )
- ste1=make_stock_entry(target="_Test Warehouse - _TC",
- item_code = "Sub Contracted Raw Material 3", qty=300, basic_rate=100)
- ste2=make_stock_entry(target="_Test Warehouse - _TC",
- item_code = "Sub Contracted Raw Material 3", qty=200, basic_rate=100)
+ ste1 = make_stock_entry(
+ target="_Test Warehouse - _TC",
+ item_code="Sub Contracted Raw Material 3",
+ qty=300,
+ basic_rate=100,
+ )
+ ste2 = make_stock_entry(
+ target="_Test Warehouse - _TC",
+ item_code="Sub Contracted Raw Material 3",
+ qty=200,
+ basic_rate=100,
+ )
- transferred_batch = {
- ste1.items[0].batch_no : 300,
- ste2.items[0].batch_no : 200
- }
+ transferred_batch = {ste1.items[0].batch_no: 300, ste2.items[0].batch_no: 200}
rm_items = [
{
- "item_code":item_code,
- "rm_item_code":"Sub Contracted Raw Material 3",
- "item_name":"_Test Item",
- "qty":300,
- "warehouse":"_Test Warehouse - _TC",
- "stock_uom":"Nos",
- "name": po.supplied_items[0].name
+ "item_code": item_code,
+ "rm_item_code": "Sub Contracted Raw Material 3",
+ "item_name": "_Test Item",
+ "qty": 300,
+ "warehouse": "_Test Warehouse - _TC",
+ "stock_uom": "Nos",
+ "name": po.supplied_items[0].name,
},
{
- "item_code":item_code,
- "rm_item_code":"Sub Contracted Raw Material 3",
- "item_name":"_Test Item",
- "qty":200,
- "warehouse":"_Test Warehouse - _TC",
- "stock_uom":"Nos",
- "name": po.supplied_items[0].name
- }
+ "item_code": item_code,
+ "rm_item_code": "Sub Contracted Raw Material 3",
+ "item_name": "_Test Item",
+ "qty": 200,
+ "warehouse": "_Test Warehouse - _TC",
+ "stock_uom": "Nos",
+ "name": po.supplied_items[0].name,
+ },
]
rm_item_string = json.dumps(rm_items)
@@ -1268,11 +1226,8 @@ class TestPurchaseReceipt(ERPNextTestCase):
supplied_qty = frappe.db.get_value(
"Purchase Order Item Supplied",
- {
- "parent": po.name,
- "rm_item_code": "Sub Contracted Raw Material 3"
- },
- "supplied_qty"
+ {"parent": po.name, "rm_item_code": "Sub Contracted Raw Material 3"},
+ "supplied_qty",
)
self.assertEqual(supplied_qty, 500.00)
@@ -1292,12 +1247,11 @@ class TestPurchaseReceipt(ERPNextTestCase):
ste1.cancel()
po.cancel()
-
def test_po_to_pi_and_po_to_pr_worflow_full(self):
"""Test following behaviour:
- - Create PO
- - Create PI from PO and submit
- - Create PR from PO and submit
+ - Create PO
+ - Create PI from PO and submit
+ - Create PR from PO and submit
"""
from erpnext.buying.doctype.purchase_order import purchase_order, test_purchase_order
@@ -1316,16 +1270,16 @@ class TestPurchaseReceipt(ERPNextTestCase):
def test_po_to_pi_and_po_to_pr_worflow_partial(self):
"""Test following behaviour:
- - Create PO
- - Create partial PI from PO and submit
- - Create PR from PO and submit
+ - Create PO
+ - Create partial PI from PO and submit
+ - Create PR from PO and submit
"""
from erpnext.buying.doctype.purchase_order import purchase_order, test_purchase_order
po = test_purchase_order.create_purchase_order()
pi = purchase_order.make_purchase_invoice(po.name)
- pi.items[0].qty /= 2 # roughly 50%, ^ this function only creates PI with 1 item.
+ pi.items[0].qty /= 2 # roughly 50%, ^ this function only creates PI with 1 item.
pi.submit()
pr = purchase_order.make_purchase_receipt(po.name)
@@ -1357,10 +1311,9 @@ class TestPurchaseReceipt(ERPNextTestCase):
automatically_fetch_payment_terms()
-
po = create_purchase_order(qty=10, rate=100, do_not_save=1)
create_payment_terms_template()
- po.payment_terms_template = 'Test Receivable Template'
+ po.payment_terms_template = "Test Receivable Template"
po.submit()
pr = make_pr_against_po(po.name, received_qty=10)
@@ -1387,7 +1340,9 @@ class TestPurchaseReceipt(ERPNextTestCase):
account = "Stock Received But Not Billed - TCP1"
make_item(item_code)
- se = make_stock_entry(item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0)
+ se = make_stock_entry(
+ item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0
+ )
se.items[0].allow_zero_valuation_rate = 1
se.save()
se.submit()
@@ -1408,93 +1363,112 @@ class TestPurchaseReceipt(ERPNextTestCase):
def get_sl_entries(voucher_type, voucher_no):
- return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
+ return frappe.db.sql(
+ """ select actual_qty, warehouse, stock_value_difference
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s
- order by posting_time desc""", (voucher_type, voucher_no), as_dict=1)
+ order by posting_time desc""",
+ (voucher_type, voucher_no),
+ as_dict=1,
+ )
+
def get_gl_entries(voucher_type, voucher_no):
- return frappe.db.sql("""select account, debit, credit, cost_center, is_cancelled
+ return frappe.db.sql(
+ """select account, debit, credit, cost_center, is_cancelled
from `tabGL Entry` where voucher_type=%s and voucher_no=%s
- order by account desc""", (voucher_type, voucher_no), as_dict=1)
+ order by account desc""",
+ (voucher_type, voucher_no),
+ as_dict=1,
+ )
+
def get_taxes(**args):
args = frappe._dict(args)
- return [{'account_head': '_Test Account Shipping Charges - TCP1',
- 'add_deduct_tax': 'Add',
- 'category': 'Valuation and Total',
- 'charge_type': 'Actual',
- 'cost_center': args.cost_center or 'Main - TCP1',
- 'description': 'Shipping Charges',
- 'doctype': 'Purchase Taxes and Charges',
- 'parentfield': 'taxes',
- 'rate': 100.0,
- 'tax_amount': 100.0},
- {'account_head': '_Test Account VAT - TCP1',
- 'add_deduct_tax': 'Add',
- 'category': 'Total',
- 'charge_type': 'Actual',
- 'cost_center': args.cost_center or 'Main - TCP1',
- 'description': 'VAT',
- 'doctype': 'Purchase Taxes and Charges',
- 'parentfield': 'taxes',
- 'rate': 120.0,
- 'tax_amount': 120.0},
- {'account_head': '_Test Account Customs Duty - TCP1',
- 'add_deduct_tax': 'Add',
- 'category': 'Valuation',
- 'charge_type': 'Actual',
- 'cost_center': args.cost_center or 'Main - TCP1',
- 'description': 'Customs Duty',
- 'doctype': 'Purchase Taxes and Charges',
- 'parentfield': 'taxes',
- 'rate': 150.0,
- 'tax_amount': 150.0}]
+ return [
+ {
+ "account_head": "_Test Account Shipping Charges - TCP1",
+ "add_deduct_tax": "Add",
+ "category": "Valuation and Total",
+ "charge_type": "Actual",
+ "cost_center": args.cost_center or "Main - TCP1",
+ "description": "Shipping Charges",
+ "doctype": "Purchase Taxes and Charges",
+ "parentfield": "taxes",
+ "rate": 100.0,
+ "tax_amount": 100.0,
+ },
+ {
+ "account_head": "_Test Account VAT - TCP1",
+ "add_deduct_tax": "Add",
+ "category": "Total",
+ "charge_type": "Actual",
+ "cost_center": args.cost_center or "Main - TCP1",
+ "description": "VAT",
+ "doctype": "Purchase Taxes and Charges",
+ "parentfield": "taxes",
+ "rate": 120.0,
+ "tax_amount": 120.0,
+ },
+ {
+ "account_head": "_Test Account Customs Duty - TCP1",
+ "add_deduct_tax": "Add",
+ "category": "Valuation",
+ "charge_type": "Actual",
+ "cost_center": args.cost_center or "Main - TCP1",
+ "description": "Customs Duty",
+ "doctype": "Purchase Taxes and Charges",
+ "parentfield": "taxes",
+ "rate": 150.0,
+ "tax_amount": 150.0,
+ },
+ ]
+
def get_items(**args):
args = frappe._dict(args)
- return [{
- "base_amount": 250.0,
- "conversion_factor": 1.0,
- "description": "_Test Item",
- "doctype": "Purchase Receipt Item",
- "item_code": "_Test Item",
- "item_name": "_Test Item",
- "parentfield": "items",
- "qty": 5.0,
- "rate": 50.0,
- "received_qty": 5.0,
- "rejected_qty": 0.0,
- "stock_uom": "_Test UOM",
- "uom": "_Test UOM",
- "warehouse": args.warehouse or "_Test Warehouse - _TC",
- "cost_center": args.cost_center or "Main - _TC"
- },
- {
- "base_amount": 250.0,
- "conversion_factor": 1.0,
- "description": "_Test Item Home Desktop 100",
- "doctype": "Purchase Receipt Item",
- "item_code": "_Test Item Home Desktop 100",
- "item_name": "_Test Item Home Desktop 100",
- "parentfield": "items",
- "qty": 5.0,
- "rate": 50.0,
- "received_qty": 5.0,
- "rejected_qty": 0.0,
- "stock_uom": "_Test UOM",
- "uom": "_Test UOM",
- "warehouse": args.warehouse or "_Test Warehouse 1 - _TC",
- "cost_center": args.cost_center or "Main - _TC"
- }]
+ return [
+ {
+ "base_amount": 250.0,
+ "conversion_factor": 1.0,
+ "description": "_Test Item",
+ "doctype": "Purchase Receipt Item",
+ "item_code": "_Test Item",
+ "item_name": "_Test Item",
+ "parentfield": "items",
+ "qty": 5.0,
+ "rate": 50.0,
+ "received_qty": 5.0,
+ "rejected_qty": 0.0,
+ "stock_uom": "_Test UOM",
+ "uom": "_Test UOM",
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "cost_center": args.cost_center or "Main - _TC",
+ },
+ {
+ "base_amount": 250.0,
+ "conversion_factor": 1.0,
+ "description": "_Test Item Home Desktop 100",
+ "doctype": "Purchase Receipt Item",
+ "item_code": "_Test Item Home Desktop 100",
+ "item_name": "_Test Item Home Desktop 100",
+ "parentfield": "items",
+ "qty": 5.0,
+ "rate": 50.0,
+ "received_qty": 5.0,
+ "rejected_qty": 0.0,
+ "stock_uom": "_Test UOM",
+ "uom": "_Test UOM",
+ "warehouse": args.warehouse or "_Test Warehouse 1 - _TC",
+ "cost_center": args.cost_center or "Main - _TC",
+ },
+ ]
+
def make_purchase_receipt(**args):
- if not frappe.db.exists('Location', 'Test Location'):
- frappe.get_doc({
- 'doctype': 'Location',
- 'location_name': 'Test Location'
- }).insert()
+ if not frappe.db.exists("Location", "Test Location"):
+ frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert()
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
pr = frappe.new_doc("Purchase Receipt")
@@ -1518,27 +1492,33 @@ def make_purchase_receipt(**args):
item_code = args.item or args.item_code or "_Test Item"
uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM"
- pr.append("items", {
- "item_code": item_code,
- "warehouse": args.warehouse or "_Test Warehouse - _TC",
- "qty": qty,
- "received_qty": received_qty,
- "rejected_qty": rejected_qty,
- "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "",
- "rate": args.rate if args.rate != None else 50,
- "conversion_factor": args.conversion_factor or 1.0,
- "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0),
- "serial_no": args.serial_no,
- "stock_uom": args.stock_uom or "_Test UOM",
- "uom": uom,
- "cost_center": args.cost_center or frappe.get_cached_value('Company', pr.company, 'cost_center'),
- "asset_location": args.location or "Test Location"
- })
+ pr.append(
+ "items",
+ {
+ "item_code": item_code,
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "qty": qty,
+ "received_qty": received_qty,
+ "rejected_qty": rejected_qty,
+ "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC"
+ if rejected_qty != 0
+ else "",
+ "rate": args.rate if args.rate != None else 50,
+ "conversion_factor": args.conversion_factor or 1.0,
+ "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0),
+ "serial_no": args.serial_no,
+ "stock_uom": args.stock_uom or "_Test UOM",
+ "uom": uom,
+ "cost_center": args.cost_center
+ or frappe.get_cached_value("Company", pr.company, "cost_center"),
+ "asset_location": args.location or "Test Location",
+ },
+ )
if args.get_multiple_items:
pr.items = []
- company_cost_center = frappe.get_cached_value('Company', pr.company, 'cost_center')
+ company_cost_center = frappe.get_cached_value("Company", pr.company, "cost_center")
cost_center = args.cost_center or company_cost_center
for item in get_items(warehouse=args.warehouse, cost_center=cost_center):
@@ -1554,33 +1534,44 @@ def make_purchase_receipt(**args):
pr.submit()
return pr
+
def create_subcontracted_item(**args):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
args = frappe._dict(args)
- if not frappe.db.exists('Item', args.item_code):
- make_item(args.item_code, {
- 'is_stock_item': 1,
- 'is_sub_contracted_item': 1,
- 'has_batch_no': args.get("has_batch_no") or 0
- })
+ if not frappe.db.exists("Item", args.item_code):
+ make_item(
+ args.item_code,
+ {
+ "is_stock_item": 1,
+ "is_sub_contracted_item": 1,
+ "has_batch_no": args.get("has_batch_no") or 0,
+ },
+ )
if not args.raw_materials:
- if not frappe.db.exists('Item', "Test Extra Item 1"):
- make_item("Test Extra Item 1", {
- 'is_stock_item': 1,
- })
+ if not frappe.db.exists("Item", "Test Extra Item 1"):
+ make_item(
+ "Test Extra Item 1",
+ {
+ "is_stock_item": 1,
+ },
+ )
- if not frappe.db.exists('Item', "Test Extra Item 2"):
- make_item("Test Extra Item 2", {
- 'is_stock_item': 1,
- })
+ if not frappe.db.exists("Item", "Test Extra Item 2"):
+ make_item(
+ "Test Extra Item 2",
+ {
+ "is_stock_item": 1,
+ },
+ )
- args.raw_materials = ['_Test FG Item', 'Test Extra Item 1']
+ args.raw_materials = ["_Test FG Item", "Test Extra Item 1"]
+
+ if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"):
+ make_bom(item=args.item_code, raw_materials=args.get("raw_materials"))
- if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'):
- make_bom(item = args.item_code, raw_materials = args.get("raw_materials"))
test_dependencies = ["BOM", "Item Price", "Location"]
-test_records = frappe.get_test_records('Purchase Receipt')
+test_records = frappe.get_test_records("Purchase Receipt")
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index e5994b2dd48..aa217441c0b 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -96,7 +96,6 @@
"include_exploded_items",
"batch_no",
"rejected_serial_no",
- "expense_account",
"item_tax_rate",
"item_weight_details",
"weight_per_unit",
@@ -107,6 +106,10 @@
"manufacturer",
"column_break_16",
"manufacturer_part_no",
+ "accounting_details_section",
+ "expense_account",
+ "column_break_102",
+ "provisional_expense_account",
"accounting_dimensions_section",
"project",
"dimension_col_break",
@@ -971,12 +974,27 @@
"label": "Product Bundle",
"options": "Product Bundle",
"read_only": 1
+ },
+ {
+ "fieldname": "provisional_expense_account",
+ "fieldtype": "Link",
+ "label": "Provisional Expense Account",
+ "options": "Account"
+ },
+ {
+ "fieldname": "accounting_details_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fieldname": "column_break_102",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2022-02-01 11:32:27.980524",
+ "modified": "2022-04-11 13:07:32.061402",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py
index 4e472a92dc1..623fbde2b0b 100644
--- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py
@@ -24,11 +24,16 @@ class PutawayRule(Document):
self.set_stock_capacity()
def validate_duplicate_rule(self):
- existing_rule = frappe.db.exists("Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse})
+ existing_rule = frappe.db.exists(
+ "Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse}
+ )
if existing_rule and existing_rule != self.name:
- frappe.throw(_("Putaway Rule already exists for Item {0} in Warehouse {1}.")
- .format(frappe.bold(self.item_code), frappe.bold(self.warehouse)),
- title=_("Duplicate"))
+ frappe.throw(
+ _("Putaway Rule already exists for Item {0} in Warehouse {1}.").format(
+ frappe.bold(self.item_code), frappe.bold(self.warehouse)
+ ),
+ title=_("Duplicate"),
+ )
def validate_priority(self):
if self.priority < 1:
@@ -37,18 +42,24 @@ class PutawayRule(Document):
def validate_warehouse_and_company(self):
company = frappe.db.get_value("Warehouse", self.warehouse, "company")
if company != self.company:
- frappe.throw(_("Warehouse {0} does not belong to Company {1}.")
- .format(frappe.bold(self.warehouse), frappe.bold(self.company)),
- title=_("Invalid Warehouse"))
+ frappe.throw(
+ _("Warehouse {0} does not belong to Company {1}.").format(
+ frappe.bold(self.warehouse), frappe.bold(self.company)
+ ),
+ title=_("Invalid Warehouse"),
+ )
def validate_capacity(self):
stock_uom = frappe.db.get_value("Item", self.item_code, "stock_uom")
balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate())
if flt(self.stock_capacity) < flt(balance_qty):
- frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}.")
- .format(self.item_code, frappe.bold(balance_qty), stock_uom),
- title=_("Insufficient Capacity"))
+ frappe.throw(
+ _(
+ "Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}."
+ ).format(self.item_code, frappe.bold(balance_qty), stock_uom),
+ title=_("Insufficient Capacity"),
+ )
if not self.capacity:
frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid"))
@@ -56,23 +67,26 @@ class PutawayRule(Document):
def set_stock_capacity(self):
self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity)
+
@frappe.whitelist()
def get_available_putaway_capacity(rule):
- stock_capacity, item_code, warehouse = frappe.db.get_value("Putaway Rule", rule,
- ["stock_capacity", "item_code", "warehouse"])
+ stock_capacity, item_code, warehouse = frappe.db.get_value(
+ "Putaway Rule", rule, ["stock_capacity", "item_code", "warehouse"]
+ )
balance_qty = get_stock_balance(item_code, warehouse, nowdate())
free_space = flt(stock_capacity) - flt(balance_qty)
return free_space if free_space > 0 else 0
+
@frappe.whitelist()
def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
- """ Applies Putaway Rule on line items.
+ """Applies Putaway Rule on line items.
- items: List of Purchase Receipt/Stock Entry Items
- company: Company in the Purchase Receipt/Stock Entry
- doctype: Doctype to apply rule on
- purpose: Purpose of Stock Entry
- sync (optional): Sync with client side only for client side calls
+ items: List of Purchase Receipt/Stock Entry Items
+ company: Company in the Purchase Receipt/Stock Entry
+ doctype: Doctype to apply rule on
+ purpose: Purpose of Stock Entry
+ sync (optional): Sync with client side only for client side calls
"""
if isinstance(items, str):
items = json.loads(items)
@@ -89,16 +103,18 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
item.conversion_factor = flt(item.conversion_factor) or 1.0
pending_qty, item_code = flt(item.qty), item.item_code
pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty)
- uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number')
+ uom_must_be_whole_number = frappe.db.get_value("UOM", item.uom, "must_be_whole_number")
if not pending_qty or not item_code:
updated_table = add_row(item, pending_qty, source_warehouse or item.warehouse, updated_table)
continue
- at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse)
+ at_capacity, rules = get_ordered_putaway_rules(
+ item_code, company, source_warehouse=source_warehouse
+ )
if not rules:
- warehouse = source_warehouse or item.get('warehouse')
+ warehouse = source_warehouse or item.get("warehouse")
if at_capacity:
# rules available, but no free space
items_not_accomodated.append([item_code, pending_qty])
@@ -117,23 +133,28 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
for rule in item_wise_rules[key]:
if pending_stock_qty > 0 and rule.free_space:
- stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty
+ stock_qty_to_allocate = (
+ flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty
+ )
qty_to_allocate = stock_qty_to_allocate / item.conversion_factor
if uom_must_be_whole_number:
qty_to_allocate = floor(qty_to_allocate)
stock_qty_to_allocate = qty_to_allocate * item.conversion_factor
- if not qty_to_allocate: break
+ if not qty_to_allocate:
+ break
- updated_table = add_row(item, qty_to_allocate, rule.warehouse, updated_table,
- rule.name, serial_nos=serial_nos)
+ updated_table = add_row(
+ item, qty_to_allocate, rule.warehouse, updated_table, rule.name, serial_nos=serial_nos
+ )
pending_stock_qty -= stock_qty_to_allocate
pending_qty -= qty_to_allocate
rule["free_space"] -= stock_qty_to_allocate
- if not pending_stock_qty > 0: break
+ if not pending_stock_qty > 0:
+ break
# if pending qty after applying all rules, add row without warehouse
if pending_stock_qty > 0:
@@ -146,13 +167,14 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
items[:] = updated_table
frappe.msgprint(_("Applied putaway rules."), alert=True)
- if sync and json.loads(sync): # sync with client side
+ if sync and json.loads(sync): # sync with client side
return items
-def _items_changed(old, new, doctype: str) -> bool:
- """ Check if any items changed by application of putaway rules.
- If not, changing item table can have side effects since `name` items also changes.
+def _items_changed(old, new, doctype: str) -> bool:
+ """Check if any items changed by application of putaway rules.
+
+ If not, changing item table can have side effects since `name` items also changes.
"""
if len(old) != len(new):
return True
@@ -161,13 +183,22 @@ def _items_changed(old, new, doctype: str) -> bool:
if doctype == "Stock Entry":
compare_keys = ("item_code", "t_warehouse", "transfer_qty", "serial_no")
- sort_key = lambda item: (item.item_code, cstr(item.t_warehouse), # noqa
- flt(item.transfer_qty), cstr(item.serial_no))
+ sort_key = lambda item: ( # noqa
+ item.item_code,
+ cstr(item.t_warehouse),
+ flt(item.transfer_qty),
+ cstr(item.serial_no),
+ )
else:
# purchase receipt / invoice
compare_keys = ("item_code", "warehouse", "stock_qty", "received_qty", "serial_no")
- sort_key = lambda item: (item.item_code, cstr(item.warehouse), # noqa
- flt(item.stock_qty), flt(item.received_qty), cstr(item.serial_no))
+ sort_key = lambda item: ( # noqa
+ item.item_code,
+ cstr(item.warehouse),
+ flt(item.stock_qty),
+ flt(item.received_qty),
+ cstr(item.serial_no),
+ )
old_sorted = sorted(old, key=sort_key)
new_sorted = sorted(new, key=sort_key)
@@ -182,18 +213,16 @@ def _items_changed(old, new, doctype: str) -> bool:
def get_ordered_putaway_rules(item_code, company, source_warehouse=None):
"""Returns an ordered list of putaway rules to apply on an item."""
- filters = {
- "item_code": item_code,
- "company": company,
- "disable": 0
- }
+ filters = {"item_code": item_code, "company": company, "disable": 0}
if source_warehouse:
filters.update({"warehouse": ["!=", source_warehouse]})
- rules = frappe.get_all("Putaway Rule",
+ rules = frappe.get_all(
+ "Putaway Rule",
fields=["name", "item_code", "stock_capacity", "priority", "warehouse"],
filters=filters,
- order_by="priority asc, capacity desc")
+ order_by="priority asc, capacity desc",
+ )
if not rules:
return False, None
@@ -211,10 +240,11 @@ def get_ordered_putaway_rules(item_code, company, source_warehouse=None):
# then there is not enough space left in any rule
return True, None
- vacant_rules = sorted(vacant_rules, key = lambda i: (i['priority'], -i['free_space']))
+ vacant_rules = sorted(vacant_rules, key=lambda i: (i["priority"], -i["free_space"]))
return False, vacant_rules
+
def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=None):
new_updated_table_row = copy.deepcopy(item)
new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1
@@ -223,7 +253,9 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=N
if item.doctype == "Stock Entry Detail":
new_updated_table_row.t_warehouse = warehouse
- new_updated_table_row.transfer_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor)
+ new_updated_table_row.transfer_qty = flt(to_allocate) * flt(
+ new_updated_table_row.conversion_factor
+ )
else:
new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor)
new_updated_table_row.warehouse = warehouse
@@ -238,6 +270,7 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=N
updated_table.append(new_updated_table_row)
return updated_table
+
def show_unassigned_items_message(items_not_accomodated):
msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "
- '''.format(item_code, len(serial_nos), body)
+ """.format(
+ item_code, len(serial_nos), body
+ )
def get_item_details(item_code):
- return frappe.db.sql("""select name, has_batch_no, docstatus,
+ return frappe.db.sql(
+ """select name, has_batch_no, docstatus,
is_stock_item, has_serial_no, serial_no_series
- from tabItem where name=%s""", item_code, as_dict=True)[0]
+ from tabItem where name=%s""",
+ item_code,
+ as_dict=True,
+ )[0]
+
def get_serial_nos(serial_no):
if isinstance(serial_no, list):
return serial_no
- return [s.strip() for s in cstr(serial_no).strip().upper().replace(',', '\n').split('\n')
- if s.strip()]
+ return [
+ s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip()
+ ]
+
def clean_serial_no_string(serial_no: str) -> str:
if not serial_no:
@@ -491,20 +665,23 @@ def clean_serial_no_string(serial_no: str) -> str:
serial_no_list = get_serial_nos(serial_no)
return "\n".join(serial_no_list)
+
def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False):
for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]:
if args.get(field):
serial_no_doc.set(field, args.get(field))
serial_no_doc.via_stock_ledger = args.get("via_stock_ledger") or True
- serial_no_doc.warehouse = (args.get("warehouse")
- if args.get("actual_qty", 0) > 0 else None)
+ serial_no_doc.warehouse = args.get("warehouse") if args.get("actual_qty", 0) > 0 else None
if is_new:
serial_no_doc.serial_no = serial_no
- if (serial_no_doc.sales_order and args.get("voucher_type") == "Stock Entry"
- and not args.get("actual_qty", 0) > 0):
+ if (
+ serial_no_doc.sales_order
+ and args.get("voucher_type") == "Stock Entry"
+ and not args.get("actual_qty", 0) > 0
+ ):
serial_no_doc.sales_order = None
serial_no_doc.validate_item()
@@ -517,146 +694,224 @@ def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False):
return serial_no_doc
-def update_serial_nos_after_submit(controller, parentfield):
- stock_ledger_entries = frappe.db.sql("""select voucher_detail_no, serial_no, actual_qty, warehouse
- from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s""",
- (controller.doctype, controller.name), as_dict=True)
- if not stock_ledger_entries: return
+def update_serial_nos_after_submit(controller, parentfield):
+ stock_ledger_entries = frappe.db.sql(
+ """select voucher_detail_no, serial_no, actual_qty, warehouse
+ from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s""",
+ (controller.doctype, controller.name),
+ as_dict=True,
+ )
+
+ if not stock_ledger_entries:
+ return
for d in controller.get(parentfield):
if d.serial_no:
continue
- update_rejected_serial_nos = True if (controller.doctype in ("Purchase Receipt", "Purchase Invoice")
- and d.rejected_qty) else False
+ update_rejected_serial_nos = (
+ True
+ if (controller.doctype in ("Purchase Receipt", "Purchase Invoice") and d.rejected_qty)
+ else False
+ )
accepted_serial_nos_updated = False
if controller.doctype == "Stock Entry":
warehouse = d.t_warehouse
qty = d.transfer_qty
+ elif controller.doctype in ("Sales Invoice", "Delivery Note"):
+ warehouse = d.warehouse
+ qty = d.stock_qty
else:
warehouse = d.warehouse
- qty = (d.qty if controller.doctype == "Stock Reconciliation"
- else d.stock_qty)
+ qty = d.qty if controller.doctype == "Stock Reconciliation" else d.stock_qty
for sle in stock_ledger_entries:
- if sle.voucher_detail_no==d.name:
- if not accepted_serial_nos_updated and qty and abs(sle.actual_qty)==qty \
- and sle.warehouse == warehouse and sle.serial_no != d.serial_no:
- d.serial_no = sle.serial_no
- frappe.db.set_value(d.doctype, d.name, "serial_no", sle.serial_no)
- accepted_serial_nos_updated = True
- if not update_rejected_serial_nos:
- break
- elif update_rejected_serial_nos and abs(sle.actual_qty)==d.rejected_qty \
- and sle.warehouse == d.rejected_warehouse and sle.serial_no != d.rejected_serial_no:
- d.rejected_serial_no = sle.serial_no
- frappe.db.set_value(d.doctype, d.name, "rejected_serial_no", sle.serial_no)
- update_rejected_serial_nos = False
- if accepted_serial_nos_updated:
- break
+ if sle.voucher_detail_no == d.name:
+ if (
+ not accepted_serial_nos_updated
+ and qty
+ and abs(sle.actual_qty) == abs(qty)
+ and sle.warehouse == warehouse
+ and sle.serial_no != d.serial_no
+ ):
+ d.serial_no = sle.serial_no
+ frappe.db.set_value(d.doctype, d.name, "serial_no", sle.serial_no)
+ accepted_serial_nos_updated = True
+ if not update_rejected_serial_nos:
+ break
+ elif (
+ update_rejected_serial_nos
+ and abs(sle.actual_qty) == d.rejected_qty
+ and sle.warehouse == d.rejected_warehouse
+ and sle.serial_no != d.rejected_serial_no
+ ):
+ d.rejected_serial_no = sle.serial_no
+ frappe.db.set_value(d.doctype, d.name, "rejected_serial_no", sle.serial_no)
+ update_rejected_serial_nos = False
+ if accepted_serial_nos_updated:
+ break
+
def update_maintenance_status():
- serial_nos = frappe.db.sql('''select name from `tabSerial No` where (amc_expiry_date<%s or
- warranty_expiry_date<%s) and maintenance_status not in ('Out of Warranty', 'Out of AMC')''',
- (nowdate(), nowdate()))
+ serial_nos = frappe.db.sql(
+ """select name from `tabSerial No` where (amc_expiry_date<%s or
+ warranty_expiry_date<%s) and maintenance_status not in ('Out of Warranty', 'Out of AMC')""",
+ (nowdate(), nowdate()),
+ )
for serial_no in serial_nos:
doc = frappe.get_doc("Serial No", serial_no[0])
doc.set_maintenance_status()
- frappe.db.set_value('Serial No', doc.name, 'maintenance_status', doc.maintenance_status)
+ frappe.db.set_value("Serial No", doc.name, "maintenance_status", doc.maintenance_status)
+
def get_delivery_note_serial_no(item_code, qty, delivery_note):
- serial_nos = ''
- dn_serial_nos = frappe.db.sql_list(""" select name from `tabSerial No`
+ serial_nos = ""
+ dn_serial_nos = frappe.db.sql_list(
+ """ select name from `tabSerial No`
where item_code = %(item_code)s and delivery_document_no = %(delivery_note)s
- and sales_invoice is null limit {0}""".format(cint(qty)), {
- 'item_code': item_code,
- 'delivery_note': delivery_note
- })
+ and sales_invoice is null limit {0}""".format(
+ cint(qty)
+ ),
+ {"item_code": item_code, "delivery_note": delivery_note},
+ )
- if dn_serial_nos and len(dn_serial_nos)>0:
- serial_nos = '\n'.join(dn_serial_nos)
+ if dn_serial_nos and len(dn_serial_nos) > 0:
+ serial_nos = "\n".join(dn_serial_nos)
return serial_nos
+
@frappe.whitelist()
-def auto_fetch_serial_number(qty, item_code, warehouse, posting_date=None, batch_nos=None, for_doctype=None):
- filters = { "item_code": item_code, "warehouse": warehouse }
+def auto_fetch_serial_number(
+ qty: float,
+ item_code: str,
+ warehouse: str,
+ posting_date: Optional[str] = None,
+ batch_nos: Optional[Union[str, List[str]]] = None,
+ for_doctype: Optional[str] = None,
+ exclude_sr_nos: Optional[List[str]] = None,
+) -> List[str]:
+
+ filters = frappe._dict({"item_code": item_code, "warehouse": warehouse})
+
+ if exclude_sr_nos is None:
+ exclude_sr_nos = []
+ else:
+ exclude_sr_nos = safe_json_loads(exclude_sr_nos)
+ exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos)))
if batch_nos:
- try:
- filters["batch_no"] = json.loads(batch_nos) if (type(json.loads(batch_nos)) == list) else [json.loads(batch_nos)]
- except Exception:
- filters["batch_no"] = [batch_nos]
+ batch_nos_list = safe_json_loads(batch_nos)
+ if isinstance(batch_nos_list, list):
+ filters.batch_no = batch_nos_list
+ else:
+ filters.batch_no = [batch_nos]
if posting_date:
- filters["expiry_date"] = posting_date
+ filters.expiry_date = posting_date
serial_numbers = []
- if for_doctype == 'POS Invoice':
- reserved_sr_nos = get_pos_reserved_serial_nos(filters)
- serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=reserved_sr_nos)
- else:
- serial_numbers = fetch_serial_numbers(filters, qty)
+ if for_doctype == "POS Invoice":
+ exclude_sr_nos.extend(get_pos_reserved_serial_nos(filters))
+
+ serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=exclude_sr_nos)
+
+ return sorted([d.get("name") for d in serial_numbers])
+
+
+def get_delivered_serial_nos(serial_nos):
+ """
+ Returns serial numbers that delivered from the list of serial numbers
+ """
+ from frappe.query_builder.functions import Coalesce
+
+ SerialNo = frappe.qb.DocType("Serial No")
+ serial_nos = get_serial_nos(serial_nos)
+ query = (
+ frappe.qb.select(SerialNo.name)
+ .from_(SerialNo)
+ .where((SerialNo.name.isin(serial_nos)) & (Coalesce(SerialNo.delivery_document_type, "") != ""))
+ )
+
+ result = query.run()
+ if result and len(result) > 0:
+ delivered_serial_nos = [row[0] for row in result]
+ return delivered_serial_nos
- return [d.get('name') for d in serial_numbers]
@frappe.whitelist()
def get_pos_reserved_serial_nos(filters):
- if isinstance(filters, string_types):
+ if isinstance(filters, str):
filters = json.loads(filters)
- pos_transacted_sr_nos = frappe.db.sql("""select item.serial_no as serial_no
- from `tabPOS Invoice` p, `tabPOS Invoice Item` item
- where p.name = item.parent
- and p.consolidated_invoice is NULL
- and p.docstatus = 1
- and item.docstatus = 1
- and item.item_code = %(item_code)s
- and item.warehouse = %(warehouse)s
- and item.serial_no is NOT NULL and item.serial_no != ''
- """, filters, as_dict=1)
+ POSInvoice = frappe.qb.DocType("POS Invoice")
+ POSInvoiceItem = frappe.qb.DocType("POS Invoice Item")
+ query = (
+ frappe.qb.from_(POSInvoice)
+ .from_(POSInvoiceItem)
+ .select(POSInvoice.is_return, POSInvoiceItem.serial_no)
+ .where(
+ (POSInvoice.name == POSInvoiceItem.parent)
+ & (POSInvoice.docstatus == 1)
+ & (POSInvoiceItem.docstatus == 1)
+ & (POSInvoiceItem.item_code == filters.get("item_code"))
+ & (POSInvoiceItem.warehouse == filters.get("warehouse"))
+ & (POSInvoiceItem.serial_no.isnotnull())
+ & (POSInvoiceItem.serial_no != "")
+ )
+ )
+
+ pos_transacted_sr_nos = query.run(as_dict=True)
reserved_sr_nos = []
+ returned_sr_nos = []
for d in pos_transacted_sr_nos:
- reserved_sr_nos += get_serial_nos(d.serial_no)
+ if d.is_return == 0:
+ reserved_sr_nos += get_serial_nos(d.serial_no)
+ elif d.is_return == 1:
+ returned_sr_nos += get_serial_nos(d.serial_no)
+
+ for sr_no in returned_sr_nos:
+ reserved_sr_nos.remove(sr_no)
return reserved_sr_nos
+
def fetch_serial_numbers(filters, qty, do_not_include=None):
if do_not_include is None:
do_not_include = []
- batch_join_selection = ""
- batch_no_condition = ""
+
batch_nos = filters.get("batch_no")
expiry_date = filters.get("expiry_date")
+ serial_no = frappe.qb.DocType("Serial No")
+
+ query = (
+ frappe.qb.from_(serial_no)
+ .select(serial_no.name)
+ .where(
+ (serial_no.item_code == filters["item_code"])
+ & (serial_no.warehouse == filters["warehouse"])
+ & (Coalesce(serial_no.sales_invoice, "") == "")
+ & (Coalesce(serial_no.delivery_document_no, "") == "")
+ )
+ .orderby(serial_no.creation)
+ .limit(qty or 1)
+ )
+
+ if do_not_include:
+ query = query.where(serial_no.name.notin(do_not_include))
+
if batch_nos:
- batch_no_condition = """and sr.batch_no in ({}) """.format(', '.join("'%s'" % d for d in batch_nos))
+ query = query.where(serial_no.batch_no.isin(batch_nos))
if expiry_date:
- batch_join_selection = "LEFT JOIN `tabBatch` batch on sr.batch_no = batch.name "
- expiry_date_cond = "AND ifnull(batch.expiry_date, '2500-12-31') >= %(expiry_date)s "
- batch_no_condition += expiry_date_cond
-
- excluded_sr_nos = ", ".join(["" + frappe.db.escape(sr) + "" for sr in do_not_include]) or "''"
- serial_numbers = frappe.db.sql("""
- SELECT sr.name FROM `tabSerial No` sr {batch_join_selection}
- WHERE
- sr.name not in ({excluded_sr_nos}) AND
- sr.item_code = %(item_code)s AND
- sr.warehouse = %(warehouse)s AND
- ifnull(sr.sales_invoice,'') = '' AND
- ifnull(sr.delivery_document_no, '') = ''
- {batch_no_condition}
- ORDER BY
- sr.creation
- LIMIT
- {qty}
- """.format(
- excluded_sr_nos=excluded_sr_nos,
- qty=qty or 1,
- batch_join_selection=batch_join_selection,
- batch_no_condition=batch_no_condition
- ), filters, as_dict=1)
+ batch = frappe.qb.DocType("Batch")
+ query = (
+ query.left_join(batch)
+ .on(serial_no.batch_no == batch.name)
+ .where(Coalesce(batch.expiry_date, "4000-12-31") >= expiry_date)
+ )
+ serial_numbers = query.run(as_dict=True)
return serial_numbers
diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py
index f8cea717251..68623fba11e 100644
--- a/erpnext/stock/doctype/serial_no/test_serial_no.py
+++ b/erpnext/stock/doctype/serial_no/test_serial_no.py
@@ -6,24 +6,22 @@
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+from erpnext.stock.doctype.serial_no.serial_no import *
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
test_dependencies = ["Item"]
-test_records = frappe.get_test_records('Serial No')
-
-from erpnext.stock.doctype.serial_no.serial_no import *
-from erpnext.tests.utils import ERPNextTestCase
+test_records = frappe.get_test_records("Serial No")
-class TestSerialNo(ERPNextTestCase):
-
+class TestSerialNo(FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
@@ -48,7 +46,9 @@ class TestSerialNo(ERPNextTestCase):
se = make_serialized_item(target_warehouse="_Test Warehouse - _TC")
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
- dn = create_delivery_note(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0])
+ dn = create_delivery_note(
+ item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0]
+ )
serial_no = frappe.get_doc("Serial No", serial_nos[0])
@@ -60,8 +60,13 @@ class TestSerialNo(ERPNextTestCase):
self.assertEqual(serial_no.delivery_document_no, dn.name)
wh = create_warehouse("_Test Warehouse", company="_Test Company 1")
- pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0],
- company="_Test Company 1", warehouse=wh)
+ pr = make_purchase_receipt(
+ item_code="_Test Serialized Item With Series",
+ qty=1,
+ serial_no=serial_nos[0],
+ company="_Test Company 1",
+ warehouse=wh,
+ )
serial_no.reload()
@@ -74,9 +79,9 @@ class TestSerialNo(ERPNextTestCase):
def test_inter_company_transfer_intermediate_cancellation(self):
"""
- Receive into and Deliver Serial No from one company.
- Then Receive into and Deliver from second company.
- Try to cancel intermediate receipts/deliveries to test if it is blocked.
+ Receive into and Deliver Serial No from one company.
+ Then Receive into and Deliver from second company.
+ Try to cancel intermediate receipts/deliveries to test if it is blocked.
"""
se = make_serialized_item(target_warehouse="_Test Warehouse - _TC")
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
@@ -89,8 +94,9 @@ class TestSerialNo(ERPNextTestCase):
self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC")
self.assertEqual(sn_doc.purchase_document_no, se.name)
- dn = create_delivery_note(item_code="_Test Serialized Item With Series",
- qty=1, serial_no=serial_nos[0])
+ dn = create_delivery_note(
+ item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0]
+ )
sn_doc.reload()
# check Serial No details after delivery from **first** company
self.assertEqual(sn_doc.status, "Delivered")
@@ -104,8 +110,13 @@ class TestSerialNo(ERPNextTestCase):
# receive serial no in second company
wh = create_warehouse("_Test Warehouse", company="_Test Company 1")
- pr = make_purchase_receipt(item_code="_Test Serialized Item With Series",
- qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh)
+ pr = make_purchase_receipt(
+ item_code="_Test Serialized Item With Series",
+ qty=1,
+ serial_no=serial_nos[0],
+ company="_Test Company 1",
+ warehouse=wh,
+ )
sn_doc.reload()
self.assertEqual(sn_doc.warehouse, wh)
@@ -114,8 +125,13 @@ class TestSerialNo(ERPNextTestCase):
self.assertRaises(frappe.ValidationError, dn.cancel)
# deliver from second company
- dn_2 = create_delivery_note(item_code="_Test Serialized Item With Series",
- qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh)
+ dn_2 = create_delivery_note(
+ item_code="_Test Serialized Item With Series",
+ qty=1,
+ serial_no=serial_nos[0],
+ company="_Test Company 1",
+ warehouse=wh,
+ )
sn_doc.reload()
# check Serial No details after delivery from **second** company
@@ -131,9 +147,9 @@ class TestSerialNo(ERPNextTestCase):
def test_inter_company_transfer_fallback_on_cancel(self):
"""
- Test Serial No state changes on cancellation.
- If Delivery cancelled, it should fall back on last Receipt in the same company.
- If Receipt is cancelled, it should be Inactive in the same company.
+ Test Serial No state changes on cancellation.
+ If Delivery cancelled, it should fall back on last Receipt in the same company.
+ If Receipt is cancelled, it should be Inactive in the same company.
"""
# Receipt in **first** company
se = make_serialized_item(target_warehouse="_Test Warehouse - _TC")
@@ -141,17 +157,28 @@ class TestSerialNo(ERPNextTestCase):
sn_doc = frappe.get_doc("Serial No", serial_nos[0])
# Delivery from first company
- dn = create_delivery_note(item_code="_Test Serialized Item With Series",
- qty=1, serial_no=serial_nos[0])
+ dn = create_delivery_note(
+ item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0]
+ )
# Receipt in **second** company
wh = create_warehouse("_Test Warehouse", company="_Test Company 1")
- pr = make_purchase_receipt(item_code="_Test Serialized Item With Series",
- qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh)
+ pr = make_purchase_receipt(
+ item_code="_Test Serialized Item With Series",
+ qty=1,
+ serial_no=serial_nos[0],
+ company="_Test Company 1",
+ warehouse=wh,
+ )
# Delivery from second company
- dn_2 = create_delivery_note(item_code="_Test Serialized Item With Series",
- qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh)
+ dn_2 = create_delivery_note(
+ item_code="_Test Serialized Item With Series",
+ qty=1,
+ serial_no=serial_nos[0],
+ company="_Test Company 1",
+ warehouse=wh,
+ )
sn_doc.reload()
self.assertEqual(sn_doc.status, "Delivered")
@@ -184,12 +211,11 @@ class TestSerialNo(ERPNextTestCase):
def test_auto_creation_of_serial_no(self):
"""
- Test if auto created Serial No excludes existing serial numbers
+ Test if auto created Serial No excludes existing serial numbers
"""
- item_code = make_item("_Test Auto Serial Item ", {
- "has_serial_no": 1,
- "serial_no_series": "XYZ.###"
- }).item_code
+ item_code = make_item(
+ "_Test Auto Serial Item ", {"has_serial_no": 1, "serial_no_series": "XYZ.###"}
+ ).item_code
# Reserve XYZ005
pr_1 = make_purchase_receipt(item_code=item_code, qty=1, serial_no="XYZ005")
@@ -203,7 +229,7 @@ class TestSerialNo(ERPNextTestCase):
def test_serial_no_sanitation(self):
"Test if Serial No input is sanitised before entering the DB."
item_code = "_Test Serialized Item"
- test_records = frappe.get_test_records('Stock Entry')
+ test_records = frappe.get_test_records("Stock Entry")
se = frappe.copy_doc(test_records[0])
se.get("items")[0].item_code = item_code
@@ -217,27 +243,94 @@ class TestSerialNo(ERPNextTestCase):
self.assertEqual(se.get("items")[0].serial_no, "_TS1\n_TS2\n_TS3\n_TS4 - 2021")
def test_correct_serial_no_incoming_rate(self):
- """ Check correct consumption rate based on serial no record.
- """
+ """Check correct consumption rate based on serial no record."""
item_code = "_Test Serialized Item"
warehouse = "_Test Warehouse - _TC"
serial_nos = ["LOWVALUATION", "HIGHVALUATION"]
- in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=42,
- serial_no=serial_nos[0])
- in2 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=113,
- serial_no=serial_nos[1])
+ in1 = make_stock_entry(
+ item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, serial_no=serial_nos[0]
+ )
+ in2 = make_stock_entry(
+ item_code=item_code, to_warehouse=warehouse, qty=1, rate=113, serial_no=serial_nos[1]
+ )
- out = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True)
+ out = create_delivery_note(
+ item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True
+ )
# change serial no
out.items[0].serial_no = serial_nos[1]
out.save()
out.submit()
- value_diff = frappe.db.get_value("Stock Ledger Entry",
- {"voucher_no": out.name, "voucher_type": "Delivery Note"},
- "stock_value_difference"
- )
+ value_diff = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_no": out.name, "voucher_type": "Delivery Note"},
+ "stock_value_difference",
+ )
self.assertEqual(value_diff, -113)
+ def test_auto_fetch(self):
+ item_code = make_item(
+ properties={
+ "has_serial_no": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "serial_no_series": "TEST.#######",
+ }
+ ).name
+ warehouse = "_Test Warehouse - _TC"
+
+ in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=5)
+ in2 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=5)
+
+ in1.reload()
+ in2.reload()
+
+ batch1 = in1.items[0].batch_no
+ batch2 = in2.items[0].batch_no
+
+ batch_wise_serials = {
+ batch1: get_serial_nos(in1.items[0].serial_no),
+ batch2: get_serial_nos(in2.items[0].serial_no),
+ }
+
+ # Test FIFO
+ first_fetch = auto_fetch_serial_number(5, item_code, warehouse)
+ self.assertEqual(first_fetch, batch_wise_serials[batch1])
+
+ # partial FIFO
+ partial_fetch = auto_fetch_serial_number(2, item_code, warehouse)
+ self.assertTrue(
+ set(partial_fetch).issubset(set(first_fetch)),
+ msg=f"{partial_fetch} should be subset of {first_fetch}",
+ )
+
+ # exclusion
+ remaining = auto_fetch_serial_number(
+ 3, item_code, warehouse, exclude_sr_nos=json.dumps(partial_fetch)
+ )
+ self.assertEqual(sorted(remaining + partial_fetch), first_fetch)
+
+ # batchwise
+ for batch, expected_serials in batch_wise_serials.items():
+ fetched_sr = auto_fetch_serial_number(5, item_code, warehouse, batch_nos=batch)
+ self.assertEqual(fetched_sr, sorted(expected_serials))
+
+ # non existing warehouse
+ self.assertEqual(auto_fetch_serial_number(10, item_code, warehouse="Nonexisting"), [])
+
+ # multi batch
+ all_serials = [sr for sr_list in batch_wise_serials.values() for sr in sr_list]
+ fetched_serials = auto_fetch_serial_number(
+ 10, item_code, warehouse, batch_nos=list(batch_wise_serials.keys())
+ )
+ self.assertEqual(sorted(all_serials), fetched_serials)
+
+ # expiry date
+ frappe.db.set_value("Batch", batch1, "expiry_date", "1980-01-01")
+ non_expired_serials = auto_fetch_serial_number(
+ 5, item_code, warehouse, posting_date="2021-01-01", batch_nos=batch1
+ )
+ self.assertEqual(non_expired_serials, [])
diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py
index 666de57f34f..42a67f42bec 100644
--- a/erpnext/stock/doctype/shipment/shipment.py
+++ b/erpnext/stock/doctype/shipment/shipment.py
@@ -17,22 +17,22 @@ class Shipment(Document):
self.validate_pickup_time()
self.set_value_of_goods()
if self.docstatus == 0:
- self.status = 'Draft'
+ self.status = "Draft"
def on_submit(self):
if not self.shipment_parcel:
- frappe.throw(_('Please enter Shipment Parcel information'))
+ frappe.throw(_("Please enter Shipment Parcel information"))
if self.value_of_goods == 0:
- frappe.throw(_('Value of goods cannot be 0'))
- self.db_set('status', 'Submitted')
+ frappe.throw(_("Value of goods cannot be 0"))
+ self.db_set("status", "Submitted")
def on_cancel(self):
- self.db_set('status', 'Cancelled')
+ self.db_set("status", "Cancelled")
def validate_weight(self):
for parcel in self.shipment_parcel:
if flt(parcel.weight) <= 0:
- frappe.throw(_('Parcel weight cannot be 0'))
+ frappe.throw(_("Parcel weight cannot be 0"))
def validate_pickup_time(self):
if self.pickup_from and self.pickup_to and get_time(self.pickup_to) < get_time(self.pickup_from):
@@ -44,26 +44,34 @@ class Shipment(Document):
value_of_goods += flt(entry.get("grand_total"))
self.value_of_goods = value_of_goods if value_of_goods else self.value_of_goods
+
@frappe.whitelist()
def get_address_name(ref_doctype, docname):
# Return address name
return get_party_shipping_address(ref_doctype, docname)
+
@frappe.whitelist()
def get_contact_name(ref_doctype, docname):
# Return address name
return get_default_contact(ref_doctype, docname)
+
@frappe.whitelist()
def get_company_contact(user):
- contact = frappe.db.get_value('User', user, [
- 'first_name',
- 'last_name',
- 'email',
- 'phone',
- 'mobile_no',
- 'gender',
- ], as_dict=1)
+ contact = frappe.db.get_value(
+ "User",
+ user,
+ [
+ "first_name",
+ "last_name",
+ "email",
+ "phone",
+ "mobile_no",
+ "gender",
+ ],
+ as_dict=1,
+ )
if not contact.phone:
contact.phone = contact.mobile_no
return contact
diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py
index afe821845ae..ae97e7af361 100644
--- a/erpnext/stock/doctype/shipment/test_shipment.py
+++ b/erpnext/stock/doctype/shipment/test_shipment.py
@@ -4,22 +4,23 @@
from datetime import date, timedelta
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.stock.doctype.delivery_note.delivery_note import make_shipment
-from erpnext.tests.utils import ERPNextTestCase
-class TestShipment(ERPNextTestCase):
+class TestShipment(FrappeTestCase):
def test_shipment_from_delivery_note(self):
delivery_note = create_test_delivery_note()
delivery_note.submit()
- shipment = create_test_shipment([ delivery_note ])
+ shipment = create_test_shipment([delivery_note])
shipment.submit()
second_shipment = make_shipment(delivery_note.name)
self.assertEqual(second_shipment.value_of_goods, delivery_note.grand_total)
self.assertEqual(len(second_shipment.shipment_delivery_note), 1)
self.assertEqual(second_shipment.shipment_delivery_note[0].delivery_note, delivery_note.name)
+
def create_test_delivery_note():
company = get_shipment_company()
customer = get_shipment_customer()
@@ -30,25 +31,26 @@ def create_test_delivery_note():
delivery_note = frappe.new_doc("Delivery Note")
delivery_note.company = company.name
delivery_note.posting_date = posting_date.strftime("%Y-%m-%d")
- delivery_note.posting_time = '10:00'
+ delivery_note.posting_time = "10:00"
delivery_note.customer = customer.name
- delivery_note.append('items',
+ delivery_note.append(
+ "items",
{
"item_code": item.name,
"item_name": item.item_name,
- "description": 'Test delivery note for shipment',
+ "description": "Test delivery note for shipment",
"qty": 5,
- "uom": 'Nos',
- "warehouse": 'Stores - _TC',
+ "uom": "Nos",
+ "warehouse": "Stores - _TC",
"rate": item.standard_rate,
- "cost_center": 'Main - _TC'
- }
+ "cost_center": "Main - _TC",
+ },
)
delivery_note.insert()
return delivery_note
-def create_test_shipment(delivery_notes = None):
+def create_test_shipment(delivery_notes=None):
company = get_shipment_company()
company_address = get_shipment_company_address(company.name)
customer = get_shipment_customer()
@@ -57,45 +59,35 @@ def create_test_shipment(delivery_notes = None):
posting_date = date.today() + timedelta(days=5)
shipment = frappe.new_doc("Shipment")
- shipment.pickup_from_type = 'Company'
+ shipment.pickup_from_type = "Company"
shipment.pickup_company = company.name
shipment.pickup_address_name = company_address.name
- shipment.delivery_to_type = 'Customer'
+ shipment.delivery_to_type = "Customer"
shipment.delivery_customer = customer.name
shipment.delivery_address_name = customer_address.name
shipment.delivery_contact_name = customer_contact.name
- shipment.pallets = 'No'
- shipment.shipment_type = 'Goods'
+ shipment.pallets = "No"
+ shipment.shipment_type = "Goods"
shipment.value_of_goods = 1000
- shipment.pickup_type = 'Pickup'
+ shipment.pickup_type = "Pickup"
shipment.pickup_date = posting_date.strftime("%Y-%m-%d")
- shipment.pickup_from = '09:00'
- shipment.pickup_to = '17:00'
- shipment.description_of_content = 'unit test entry'
+ shipment.pickup_from = "09:00"
+ shipment.pickup_to = "17:00"
+ shipment.description_of_content = "unit test entry"
for delivery_note in delivery_notes:
- shipment.append('shipment_delivery_note',
- {
- "delivery_note": delivery_note.name
- }
- )
- shipment.append('shipment_parcel',
- {
- "length": 5,
- "width": 5,
- "height": 5,
- "weight": 5,
- "count": 5
- }
+ shipment.append("shipment_delivery_note", {"delivery_note": delivery_note.name})
+ shipment.append(
+ "shipment_parcel", {"length": 5, "width": 5, "height": 5, "weight": 5, "count": 5}
)
shipment.insert()
return shipment
def get_shipment_customer_contact(customer_name):
- contact_fname = 'Customer Shipment'
- contact_lname = 'Testing'
- customer_name = contact_fname + ' ' + contact_lname
- contacts = frappe.get_all("Contact", fields=["name"], filters = {"name": customer_name})
+ contact_fname = "Customer Shipment"
+ contact_lname = "Testing"
+ customer_name = contact_fname + " " + contact_lname
+ contacts = frappe.get_all("Contact", fields=["name"], filters={"name": customer_name})
if len(contacts):
return contacts[0]
else:
@@ -103,104 +95,106 @@ def get_shipment_customer_contact(customer_name):
def get_shipment_customer_address(customer_name):
- address_title = customer_name + ' address 123'
- customer_address = frappe.get_all("Address", fields=["name"], filters = {"address_title": address_title})
+ address_title = customer_name + " address 123"
+ customer_address = frappe.get_all(
+ "Address", fields=["name"], filters={"address_title": address_title}
+ )
if len(customer_address):
return customer_address[0]
else:
return create_shipment_address(address_title, customer_name, 81929)
+
def get_shipment_customer():
- customer_name = 'Shipment Customer'
- customer = frappe.get_all("Customer", fields=["name"], filters = {"name": customer_name})
+ customer_name = "Shipment Customer"
+ customer = frappe.get_all("Customer", fields=["name"], filters={"name": customer_name})
if len(customer):
return customer[0]
else:
return create_shipment_customer(customer_name)
+
def get_shipment_company_address(company_name):
- address_title = company_name + ' address 123'
- addresses = frappe.get_all("Address", fields=["name"], filters = {"address_title": address_title})
+ address_title = company_name + " address 123"
+ addresses = frappe.get_all("Address", fields=["name"], filters={"address_title": address_title})
if len(addresses):
return addresses[0]
else:
return create_shipment_address(address_title, company_name, 80331)
+
def get_shipment_company():
return frappe.get_doc("Company", "_Test Company")
+
def get_shipment_item(company_name):
- item_name = 'Testing Shipment item'
- items = frappe.get_all("Item",
+ item_name = "Testing Shipment item"
+ items = frappe.get_all(
+ "Item",
fields=["name", "item_name", "item_code", "standard_rate"],
- filters = {"item_name": item_name}
+ filters={"item_name": item_name},
)
if len(items):
return items[0]
else:
return create_shipment_item(item_name, company_name)
+
def create_shipment_address(address_title, company_name, postal_code):
address = frappe.new_doc("Address")
address.address_title = address_title
- address.address_type = 'Shipping'
- address.address_line1 = company_name + ' address line 1'
- address.city = 'Random City'
+ address.address_type = "Shipping"
+ address.address_line1 = company_name + " address line 1"
+ address.city = "Random City"
address.postal_code = postal_code
- address.country = 'Germany'
+ address.country = "Germany"
address.insert()
return address
def create_customer_contact(fname, lname):
customer = frappe.new_doc("Contact")
- customer.customer_name = fname + ' ' + lname
+ customer.customer_name = fname + " " + lname
customer.first_name = fname
customer.last_name = lname
customer.is_primary_contact = 1
customer.is_billing_contact = 1
- customer.append('email_ids',
- {
- 'email_id': 'randomme@email.com',
- 'is_primary': 1
- }
+ customer.append("email_ids", {"email_id": "randomme@email.com", "is_primary": 1})
+ customer.append(
+ "phone_nos", {"phone": "123123123", "is_primary_phone": 1, "is_primary_mobile_no": 1}
)
- customer.append('phone_nos',
- {
- 'phone': '123123123',
- 'is_primary_phone': 1,
- 'is_primary_mobile_no': 1
- }
- )
- customer.status = 'Passive'
+ customer.status = "Passive"
customer.insert()
return customer
+
def create_shipment_customer(customer_name):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
- customer.customer_type = 'Company'
- customer.customer_group = 'All Customer Groups'
- customer.territory = 'All Territories'
- customer.gst_category = 'Unregistered'
+ customer.customer_type = "Company"
+ customer.customer_group = "All Customer Groups"
+ customer.territory = "All Territories"
+ customer.gst_category = "Unregistered"
customer.insert()
return customer
+
def create_material_receipt(item, company):
posting_date = date.today()
stock = frappe.new_doc("Stock Entry")
stock.company = company
- stock.stock_entry_type = 'Material Receipt'
+ stock.stock_entry_type = "Material Receipt"
stock.posting_date = posting_date.strftime("%Y-%m-%d")
- stock.append('items',
+ stock.append(
+ "items",
{
- "t_warehouse": 'Stores - _TC',
+ "t_warehouse": "Stores - _TC",
"item_code": item.name,
"qty": 5,
- "uom": 'Nos',
+ "uom": "Nos",
"basic_rate": item.standard_rate,
- "cost_center": 'Main - _TC'
- }
+ "cost_center": "Main - _TC",
+ },
)
stock.insert()
stock.submit()
@@ -210,14 +204,9 @@ def create_shipment_item(item_name, company_name):
item = frappe.new_doc("Item")
item.item_name = item_name
item.item_code = item_name
- item.item_group = 'All Item Groups'
- item.stock_uom = 'Nos'
+ item.item_group = "All Item Groups"
+ item.stock_uom = "Nos"
item.standard_rate = 50
- item.append('item_defaults',
- {
- "company": company_name,
- "default_warehouse": 'Stores - _TC'
- }
- )
+ item.append("item_defaults", {"company": company_name, "default_warehouse": "Stores - _TC"})
item.insert()
return item
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 61466cff032..4ec9f1f220f 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -631,7 +631,7 @@ frappe.ui.form.on('Stock Entry Detail', {
// set allow_zero_valuation_rate to 0 if s_warehouse is selected.
let item = frappe.get_doc(cdt, cdn);
if (item.s_warehouse) {
- item.allow_zero_valuation_rate = 0;
+ frappe.model.set_value(cdt, cdn, "allow_zero_valuation_rate", 0);
}
},
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json
index c38dfaa1c84..f56e059f81c 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.json
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -46,9 +46,9 @@
"items",
"get_stock_and_rate",
"section_break_19",
- "total_incoming_value",
- "column_break_22",
"total_outgoing_value",
+ "column_break_22",
+ "total_incoming_value",
"value_difference",
"additional_costs_section",
"additional_costs",
@@ -374,7 +374,7 @@
{
"fieldname": "total_incoming_value",
"fieldtype": "Currency",
- "label": "Total Incoming Value",
+ "label": "Total Incoming Value (Receipt)",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
@@ -386,7 +386,7 @@
{
"fieldname": "total_outgoing_value",
"fieldtype": "Currency",
- "label": "Total Outgoing Value",
+ "label": "Total Outgoing Value (Consumption)",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
@@ -394,7 +394,7 @@
{
"fieldname": "value_difference",
"fieldtype": "Currency",
- "label": "Total Value Difference (Out - In)",
+ "label": "Total Value Difference (Incoming - Outgoing)",
"options": "Company:company:default_currency",
"print_hide_if_no_value": 1,
"read_only": 1
@@ -619,7 +619,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-02-07 12:55:14.614077",
+ "modified": "2022-05-02 05:21:39.060501",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index f7109ab6b0d..f0566b08897 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -39,20 +39,28 @@ from erpnext.stock.utils import get_bin, get_incoming_rate
class FinishedGoodError(frappe.ValidationError):
pass
+
+
class IncorrectValuationRateError(frappe.ValidationError):
pass
+
+
class DuplicateEntryForWorkOrderError(frappe.ValidationError):
pass
+
+
class OperationsNotCompleteError(frappe.ValidationError):
pass
+
+
class MaxSampleAlreadyRetainedError(frappe.ValidationError):
pass
+
from erpnext.controllers.stock_controller import StockController
-form_grid_templates = {
- "items": "templates/form_grid/stock_entry_grid.html"
-}
+form_grid_templates = {"items": "templates/form_grid/stock_entry_grid.html"}
+
class StockEntry(StockController):
def get_feed(self):
@@ -64,16 +72,18 @@ class StockEntry(StockController):
def before_validate(self):
from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule
- apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"])
+
+ apply_rule = self.apply_putaway_rule and (
+ self.purpose in ["Material Transfer", "Material Receipt"]
+ )
if self.get("items") and apply_rule:
- apply_putaway_rule(self.doctype, self.get("items"), self.company,
- purpose=self.purpose)
+ apply_putaway_rule(self.doctype, self.get("items"), self.company, purpose=self.purpose)
def validate(self):
self.pro_doc = frappe._dict()
if self.work_order:
- self.pro_doc = frappe.get_doc('Work Order', self.work_order)
+ self.pro_doc = frappe.get_doc("Work Order", self.work_order)
self.validate_posting_time()
self.validate_purpose()
@@ -104,10 +114,10 @@ class StockEntry(StockController):
if not self.from_bom:
self.fg_completed_qty = 0.0
- if self._action == 'submit':
- self.make_batches('t_warehouse')
+ if self._action == "submit":
+ self.make_batches("t_warehouse")
else:
- set_batch_nos(self, 's_warehouse')
+ set_batch_nos(self, "s_warehouse")
self.validate_serialized_batch()
self.set_actual_qty()
@@ -139,10 +149,10 @@ class StockEntry(StockController):
if self.work_order and self.purpose == "Manufacture":
self.update_so_in_serial_number()
- if self.purpose == 'Material Transfer' and self.add_to_transit:
- self.set_material_request_transfer_status('In Transit')
- if self.purpose == 'Material Transfer' and self.outgoing_stock_entry:
- self.set_material_request_transfer_status('Completed')
+ if self.purpose == "Material Transfer" and self.add_to_transit:
+ self.set_material_request_transfer_status("In Transit")
+ if self.purpose == "Material Transfer" and self.outgoing_stock_entry:
+ self.set_material_request_transfer_status("Completed")
def on_cancel(self):
self.update_purchase_order_supplied_items()
@@ -153,7 +163,7 @@ class StockEntry(StockController):
self.update_work_order()
self.update_stock_ledger()
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
@@ -163,15 +173,16 @@ class StockEntry(StockController):
self.delete_auto_created_batches()
self.delete_linked_stock_entry()
- if self.purpose == 'Material Transfer' and self.add_to_transit:
- self.set_material_request_transfer_status('Not Started')
- if self.purpose == 'Material Transfer' and self.outgoing_stock_entry:
- self.set_material_request_transfer_status('In Transit')
+ if self.purpose == "Material Transfer" and self.add_to_transit:
+ self.set_material_request_transfer_status("Not Started")
+ if self.purpose == "Material Transfer" and self.outgoing_stock_entry:
+ self.set_material_request_transfer_status("In Transit")
def set_job_card_data(self):
if self.job_card and not self.work_order:
- data = frappe.db.get_value('Job Card',
- self.job_card, ['for_quantity', 'work_order', 'bom_no'], as_dict=1)
+ data = frappe.db.get_value(
+ "Job Card", self.job_card, ["for_quantity", "work_order", "bom_no"], as_dict=1
+ )
self.fg_completed_qty = data.for_quantity
self.work_order = data.work_order
self.from_bom = 1
@@ -179,107 +190,160 @@ class StockEntry(StockController):
def validate_work_order_status(self):
pro_doc = frappe.get_doc("Work Order", self.work_order)
- if pro_doc.status == 'Completed':
+ if pro_doc.status == "Completed":
frappe.throw(_("Cannot cancel transaction for Completed Work Order."))
def validate_purpose(self):
- valid_purposes = ["Material Issue", "Material Receipt", "Material Transfer",
- "Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor",
- "Material Consumption for Manufacture"]
+ valid_purposes = [
+ "Material Issue",
+ "Material Receipt",
+ "Material Transfer",
+ "Material Transfer for Manufacture",
+ "Manufacture",
+ "Repack",
+ "Send to Subcontractor",
+ "Material Consumption for Manufacture",
+ ]
if self.purpose not in valid_purposes:
frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes)))
- if self.job_card and self.purpose not in ['Material Transfer for Manufacture', 'Repack']:
- frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry")
- .format(self.job_card))
+ if self.job_card and self.purpose not in ["Material Transfer for Manufacture", "Repack"]:
+ frappe.throw(
+ _(
+ "For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry"
+ ).format(self.job_card)
+ )
def delete_linked_stock_entry(self):
if self.purpose == "Send to Warehouse":
- for d in frappe.get_all("Stock Entry", filters={"docstatus": 0,
- "outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"}):
+ for d in frappe.get_all(
+ "Stock Entry",
+ filters={"docstatus": 0, "outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"},
+ ):
frappe.delete_doc("Stock Entry", d.name)
def set_transfer_qty(self):
for item in self.get("items"):
if not flt(item.qty):
- frappe.throw(_("Row {0}: Qty is mandatory").format(item.idx))
+ frappe.throw(_("Row {0}: Qty is mandatory").format(item.idx), title=_("Zero quantity"))
if not flt(item.conversion_factor):
frappe.throw(_("Row {0}: UOM Conversion Factor is mandatory").format(item.idx))
- item.transfer_qty = flt(flt(item.qty) * flt(item.conversion_factor),
- self.precision("transfer_qty", item))
+ item.transfer_qty = flt(
+ flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item)
+ )
+ if not flt(item.transfer_qty):
+ frappe.throw(
+ _("Row {0}: Qty in Stock UOM can not be zero.").format(item.idx), title=_("Zero quantity")
+ )
def update_cost_in_project(self):
- if (self.work_order and not frappe.db.get_value("Work Order",
- self.work_order, "update_consumed_material_cost_in_project")):
+ if self.work_order and not frappe.db.get_value(
+ "Work Order", self.work_order, "update_consumed_material_cost_in_project"
+ ):
return
if self.project:
- amount = frappe.db.sql(""" select ifnull(sum(sed.amount), 0)
+ amount = frappe.db.sql(
+ """ select ifnull(sum(sed.amount), 0)
from
`tabStock Entry` se, `tabStock Entry Detail` sed
where
se.docstatus = 1 and se.project = %s and sed.parent = se.name
- and (sed.t_warehouse is null or sed.t_warehouse = '')""", self.project, as_list=1)
+ and (sed.t_warehouse is null or sed.t_warehouse = '')""",
+ self.project,
+ as_list=1,
+ )
amount = amount[0][0] if amount else 0
- additional_costs = frappe.db.sql(""" select ifnull(sum(sed.base_amount), 0)
+ additional_costs = frappe.db.sql(
+ """ select ifnull(sum(sed.base_amount), 0)
from
`tabStock Entry` se, `tabLanded Cost Taxes and Charges` sed
where
se.docstatus = 1 and se.project = %s and sed.parent = se.name
- and se.purpose = 'Manufacture'""", self.project, as_list=1)
+ and se.purpose = 'Manufacture'""",
+ self.project,
+ as_list=1,
+ )
additional_cost_amt = additional_costs[0][0] if additional_costs else 0
amount += additional_cost_amt
- frappe.db.set_value('Project', self.project, 'total_consumed_material_cost', amount)
+ frappe.db.set_value("Project", self.project, "total_consumed_material_cost", amount)
def validate_item(self):
stock_items = self.get_stock_items()
serialized_items = self.get_serialized_items()
for item in self.get("items"):
if flt(item.qty) and flt(item.qty) < 0:
- frappe.throw(_("Row {0}: The item {1}, quantity must be positive number")
- .format(item.idx, frappe.bold(item.item_code)))
+ frappe.throw(
+ _("Row {0}: The item {1}, quantity must be positive number").format(
+ item.idx, frappe.bold(item.item_code)
+ )
+ )
if item.item_code not in stock_items:
frappe.throw(_("{0} is not a stock Item").format(item.item_code))
- item_details = self.get_item_details(frappe._dict(
- {"item_code": item.item_code, "company": self.company,
- "project": self.project, "uom": item.uom, 's_warehouse': item.s_warehouse}),
- for_update=True)
+ item_details = self.get_item_details(
+ frappe._dict(
+ {
+ "item_code": item.item_code,
+ "company": self.company,
+ "project": self.project,
+ "uom": item.uom,
+ "s_warehouse": item.s_warehouse,
+ }
+ ),
+ for_update=True,
+ )
- for f in ("uom", "stock_uom", "description", "item_name", "expense_account",
- "cost_center", "conversion_factor"):
- if f == "stock_uom" or not item.get(f):
- item.set(f, item_details.get(f))
- if f == 'conversion_factor' and item.uom == item_details.get('stock_uom'):
- item.set(f, item_details.get(f))
+ for f in (
+ "uom",
+ "stock_uom",
+ "description",
+ "item_name",
+ "expense_account",
+ "cost_center",
+ "conversion_factor",
+ ):
+ if f == "stock_uom" or not item.get(f):
+ item.set(f, item_details.get(f))
+ if f == "conversion_factor" and item.uom == item_details.get("stock_uom"):
+ item.set(f, item_details.get(f))
if not item.transfer_qty and item.qty:
- item.transfer_qty = flt(flt(item.qty) * flt(item.conversion_factor),
- self.precision("transfer_qty", item))
+ item.transfer_qty = flt(
+ flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item)
+ )
- if (self.purpose in ("Material Transfer", "Material Transfer for Manufacture")
+ if (
+ self.purpose in ("Material Transfer", "Material Transfer for Manufacture")
and not item.serial_no
- and item.item_code in serialized_items):
- frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code),
- frappe.MandatoryError)
+ and item.item_code in serialized_items
+ ):
+ frappe.throw(
+ _("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code),
+ frappe.MandatoryError,
+ )
def validate_qty(self):
manufacture_purpose = ["Manufacture", "Material Consumption for Manufacture"]
if self.purpose in manufacture_purpose and self.work_order:
- if not frappe.get_value('Work Order', self.work_order, 'skip_transfer'):
+ if not frappe.get_value("Work Order", self.work_order, "skip_transfer"):
item_code = []
for item in self.items:
- if cstr(item.t_warehouse) == '':
- req_items = frappe.get_all('Work Order Item',
- filters={'parent': self.work_order, 'item_code': item.item_code}, fields=["item_code"])
+ if cstr(item.t_warehouse) == "":
+ req_items = frappe.get_all(
+ "Work Order Item",
+ filters={"parent": self.work_order, "item_code": item.item_code},
+ fields=["item_code"],
+ )
- transferred_materials = frappe.db.sql("""
+ transferred_materials = frappe.db.sql(
+ """
select
sum(qty) as qty
from `tabStock Entry` se,`tabStock Entry Detail` sed
@@ -287,7 +351,10 @@ class StockEntry(StockController):
se.name = sed.parent and se.docstatus=1 and
(se.purpose='Material Transfer for Manufacture' or se.purpose='Manufacture')
and sed.item_code=%s and se.work_order= %s and ifnull(sed.t_warehouse, '') != ''
- """, (item.item_code, self.work_order), as_dict=1)
+ """,
+ (item.item_code, self.work_order),
+ as_dict=1,
+ )
stock_qty = flt(item.qty)
trans_qty = flt(transferred_materials[0].qty)
@@ -305,8 +372,11 @@ class StockEntry(StockController):
for item_code, qty_list in iteritems(item_wise_qty):
total = flt(sum(qty_list), frappe.get_precision("Stock Entry Detail", "qty"))
if self.fg_completed_qty != total:
- frappe.throw(_("The finished product {0} quantity {1} and For Quantity {2} cannot be different")
- .format(frappe.bold(item_code), frappe.bold(total), frappe.bold(self.fg_completed_qty)))
+ frappe.throw(
+ _("The finished product {0} quantity {1} and For Quantity {2} cannot be different").format(
+ frappe.bold(item_code), frappe.bold(total), frappe.bold(self.fg_completed_qty)
+ )
+ )
def validate_difference_account(self):
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
@@ -314,33 +384,53 @@ class StockEntry(StockController):
for d in self.get("items"):
if not d.expense_account:
- frappe.throw(_("Please enter Difference Account or set default Stock Adjustment Account for company {0}")
- .format(frappe.bold(self.company)))
+ frappe.throw(
+ _(
+ "Please enter Difference Account or set default Stock Adjustment Account for company {0}"
+ ).format(frappe.bold(self.company))
+ )
- elif self.is_opening == "Yes" and frappe.db.get_value("Account", d.expense_account, "report_type") == "Profit and Loss":
- frappe.throw(_("Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry"), OpeningEntryAccountError)
+ elif (
+ self.is_opening == "Yes"
+ and frappe.db.get_value("Account", d.expense_account, "report_type") == "Profit and Loss"
+ ):
+ frappe.throw(
+ _(
+ "Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry"
+ ),
+ OpeningEntryAccountError,
+ )
def validate_warehouse(self):
"""perform various (sometimes conditional) validations on warehouse"""
- source_mandatory = ["Material Issue", "Material Transfer", "Send to Subcontractor", "Material Transfer for Manufacture",
- "Material Consumption for Manufacture"]
+ source_mandatory = [
+ "Material Issue",
+ "Material Transfer",
+ "Send to Subcontractor",
+ "Material Transfer for Manufacture",
+ "Material Consumption for Manufacture",
+ ]
- target_mandatory = ["Material Receipt", "Material Transfer", "Send to Subcontractor",
- "Material Transfer for Manufacture"]
+ target_mandatory = [
+ "Material Receipt",
+ "Material Transfer",
+ "Send to Subcontractor",
+ "Material Transfer for Manufacture",
+ ]
validate_for_manufacture = any([d.bom_no for d in self.get("items")])
if self.purpose in source_mandatory and self.purpose not in target_mandatory:
self.to_warehouse = None
- for d in self.get('items'):
+ for d in self.get("items"):
d.t_warehouse = None
elif self.purpose in target_mandatory and self.purpose not in source_mandatory:
self.from_warehouse = None
- for d in self.get('items'):
+ for d in self.get("items"):
d.s_warehouse = None
- for d in self.get('items'):
+ for d in self.get("items"):
if not d.s_warehouse and not d.t_warehouse:
d.s_warehouse = self.from_warehouse
d.t_warehouse = self.to_warehouse
@@ -357,7 +447,6 @@ class StockEntry(StockController):
else:
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
-
if self.purpose == "Manufacture":
if validate_for_manufacture:
if d.is_finished_item or d.is_scrap_item or d.is_process_loss:
@@ -369,18 +458,26 @@ class StockEntry(StockController):
if not d.s_warehouse:
frappe.throw(_("Source warehouse is mandatory for row {0}").format(d.idx))
- if cstr(d.s_warehouse) == cstr(d.t_warehouse) and not self.purpose == "Material Transfer for Manufacture":
+ if (
+ cstr(d.s_warehouse) == cstr(d.t_warehouse)
+ and not self.purpose == "Material Transfer for Manufacture"
+ ):
frappe.throw(_("Source and target warehouse cannot be same for row {0}").format(d.idx))
if not (d.s_warehouse or d.t_warehouse):
frappe.throw(_("Atleast one warehouse is mandatory"))
def validate_work_order(self):
- if self.purpose in ("Manufacture", "Material Transfer for Manufacture", "Material Consumption for Manufacture"):
+ if self.purpose in (
+ "Manufacture",
+ "Material Transfer for Manufacture",
+ "Material Consumption for Manufacture",
+ ):
# check if work order is entered
- if (self.purpose=="Manufacture" or self.purpose=="Material Consumption for Manufacture") \
- and self.work_order:
+ if (
+ self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture"
+ ) and self.work_order:
if not self.fg_completed_qty:
frappe.throw(_("For Quantity (Manufactured Qty) is mandatory"))
self.check_if_operations_completed()
@@ -391,40 +488,66 @@ class StockEntry(StockController):
def check_if_operations_completed(self):
"""Check if Time Sheets are completed against before manufacturing to capture operating costs."""
prod_order = frappe.get_doc("Work Order", self.work_order)
- allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
- "overproduction_percentage_for_work_order"))
+ allowance_percentage = flt(
+ frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order")
+ )
for d in prod_order.get("operations"):
total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty)
- completed_qty = d.completed_qty + (allowance_percentage/100 * d.completed_qty)
+ completed_qty = d.completed_qty + (allowance_percentage / 100 * d.completed_qty)
if total_completed_qty > flt(completed_qty):
- job_card = frappe.db.get_value('Job Card', {'operation_id': d.name}, 'name')
+ job_card = frappe.db.get_value("Job Card", {"operation_id": d.name}, "name")
if not job_card:
- frappe.throw(_("Work Order {0}: Job Card not found for the operation {1}")
- .format(self.work_order, d.operation))
+ frappe.throw(
+ _("Work Order {0}: Job Card not found for the operation {1}").format(
+ self.work_order, d.operation
+ )
+ )
- work_order_link = frappe.utils.get_link_to_form('Work Order', self.work_order)
- job_card_link = frappe.utils.get_link_to_form('Job Card', job_card)
- frappe.throw(_("Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}.")
- .format(d.idx, frappe.bold(d.operation), frappe.bold(total_completed_qty), work_order_link, job_card_link), OperationsNotCompleteError)
+ work_order_link = frappe.utils.get_link_to_form("Work Order", self.work_order)
+ job_card_link = frappe.utils.get_link_to_form("Job Card", job_card)
+ frappe.throw(
+ _(
+ "Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}."
+ ).format(
+ d.idx,
+ frappe.bold(d.operation),
+ frappe.bold(total_completed_qty),
+ work_order_link,
+ job_card_link,
+ ),
+ OperationsNotCompleteError,
+ )
def check_duplicate_entry_for_work_order(self):
- other_ste = [t[0] for t in frappe.db.get_values("Stock Entry", {
- "work_order": self.work_order,
- "purpose": self.purpose,
- "docstatus": ["!=", 2],
- "name": ["!=", self.name]
- }, "name")]
+ other_ste = [
+ t[0]
+ for t in frappe.db.get_values(
+ "Stock Entry",
+ {
+ "work_order": self.work_order,
+ "purpose": self.purpose,
+ "docstatus": ["!=", 2],
+ "name": ["!=", self.name],
+ },
+ "name",
+ )
+ ]
if other_ste:
- production_item, qty = frappe.db.get_value("Work Order",
- self.work_order, ["production_item", "qty"])
+ production_item, qty = frappe.db.get_value(
+ "Work Order", self.work_order, ["production_item", "qty"]
+ )
args = other_ste + [production_item]
- fg_qty_already_entered = frappe.db.sql("""select sum(transfer_qty)
+ fg_qty_already_entered = frappe.db.sql(
+ """select sum(transfer_qty)
from `tabStock Entry Detail`
where parent in (%s)
and item_code = %s
- and ifnull(s_warehouse,'')='' """ % (", ".join(["%s" * len(other_ste)]), "%s"), args)[0][0]
+ and ifnull(s_warehouse,'')='' """
+ % (", ".join(["%s" * len(other_ste)]), "%s"),
+ args,
+ )[0][0]
if fg_qty_already_entered and fg_qty_already_entered >= qty:
frappe.throw(
_("Stock Entries already created for Work Order {0}: {1}").format(
@@ -434,35 +557,58 @@ class StockEntry(StockController):
)
def set_actual_qty(self):
- allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock"))
+ allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
- for d in self.get('items'):
- previous_sle = get_previous_sle({
- "item_code": d.item_code,
- "warehouse": d.s_warehouse or d.t_warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time
- })
+ for d in self.get("items"):
+ previous_sle = get_previous_sle(
+ {
+ "item_code": d.item_code,
+ "warehouse": d.s_warehouse or d.t_warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ }
+ )
# get actual stock at source warehouse
d.actual_qty = previous_sle.get("qty_after_transaction") or 0
# validate qty during submit
- if d.docstatus==1 and d.s_warehouse and not allow_negative_stock and flt(d.actual_qty, d.precision("actual_qty")) < flt(d.transfer_qty, d.precision("actual_qty")):
- frappe.throw(_("Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3})").format(d.idx,
- frappe.bold(d.s_warehouse), formatdate(self.posting_date),
- format_time(self.posting_time), frappe.bold(d.item_code))
- + '
' + _("Available quantity is {0}, you need {1}").format(frappe.bold(d.actual_qty),
- frappe.bold(d.transfer_qty)),
- NegativeStockError, title=_('Insufficient Stock'))
+ if (
+ d.docstatus == 1
+ and d.s_warehouse
+ and not allow_negative_stock
+ and flt(d.actual_qty, d.precision("actual_qty"))
+ < flt(d.transfer_qty, d.precision("actual_qty"))
+ ):
+ frappe.throw(
+ _(
+ "Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3})"
+ ).format(
+ d.idx,
+ frappe.bold(d.s_warehouse),
+ formatdate(self.posting_date),
+ format_time(self.posting_time),
+ frappe.bold(d.item_code),
+ )
+ + "
"
+ + _("Available quantity is {0}, you need {1}").format(
+ frappe.bold(d.actual_qty), frappe.bold(d.transfer_qty)
+ ),
+ NegativeStockError,
+ title=_("Insufficient Stock"),
+ )
def set_serial_nos(self, work_order):
- previous_se = frappe.db.get_value("Stock Entry", {"work_order": work_order,
- "purpose": "Material Transfer for Manufacture"}, "name")
+ previous_se = frappe.db.get_value(
+ "Stock Entry",
+ {"work_order": work_order, "purpose": "Material Transfer for Manufacture"},
+ "name",
+ )
- for d in self.get('items'):
- transferred_serial_no = frappe.db.get_value("Stock Entry Detail",{"parent": previous_se,
- "item_code": d.item_code}, "serial_no")
+ for d in self.get("items"):
+ transferred_serial_no = frappe.db.get_value(
+ "Stock Entry Detail", {"parent": previous_se, "item_code": d.item_code}, "serial_no"
+ )
if transferred_serial_no:
d.serial_no = transferred_serial_no
@@ -470,8 +616,8 @@ class StockEntry(StockController):
@frappe.whitelist()
def get_stock_and_rate(self):
"""
- Updates rate and availability of all the items.
- Called from Update Rate and Availability button.
+ Updates rate and availability of all the items.
+ Called from Update Rate and Availability button.
"""
self.set_work_order_details()
self.set_transfer_qty()
@@ -488,38 +634,51 @@ class StockEntry(StockController):
def set_basic_rate(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
"""
- Set rate for outgoing, scrapped and finished items
+ Set rate for outgoing, scrapped and finished items
"""
# Set rate for outgoing items
- outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate)
- finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss)
+ outgoing_items_cost = self.set_rate_for_outgoing_items(
+ reset_outgoing_rate, raise_error_if_no_rate
+ )
+ finished_item_qty = sum(
+ d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss
+ )
# Set basic rate for incoming items
- for d in self.get('items'):
- if d.s_warehouse or d.set_basic_rate_manually: continue
+ for d in self.get("items"):
+ if d.s_warehouse or d.set_basic_rate_manually:
+ continue
if d.allow_zero_valuation_rate:
d.basic_rate = 0.0
elif d.is_finished_item:
if self.purpose == "Manufacture":
- d.basic_rate = self.get_basic_rate_for_manufactured_item(finished_item_qty, outgoing_items_cost)
+ d.basic_rate = self.get_basic_rate_for_manufactured_item(
+ finished_item_qty, outgoing_items_cost
+ )
elif self.purpose == "Repack":
d.basic_rate = self.get_basic_rate_for_repacked_items(d.transfer_qty, outgoing_items_cost)
if not d.basic_rate and not d.allow_zero_valuation_rate:
- d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse,
- self.doctype, self.name, d.allow_zero_valuation_rate,
- currency=erpnext.get_company_currency(self.company), company=self.company,
- raise_error_if_no_rate=raise_error_if_no_rate)
+ d.basic_rate = get_valuation_rate(
+ d.item_code,
+ d.t_warehouse,
+ self.doctype,
+ self.name,
+ d.allow_zero_valuation_rate,
+ currency=erpnext.get_company_currency(self.company),
+ company=self.company,
+ raise_error_if_no_rate=raise_error_if_no_rate,
+ )
d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
if d.is_process_loss:
- d.basic_rate = flt(0.)
+ d.basic_rate = flt(0.0)
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
outgoing_items_cost = 0.0
- for d in self.get('items'):
+ for d in self.get("items"):
if d.s_warehouse:
if reset_outgoing_rate:
args = self.get_args_for_incoming_rate(d)
@@ -534,18 +693,20 @@ class StockEntry(StockController):
return outgoing_items_cost
def get_args_for_incoming_rate(self, item):
- return frappe._dict({
- "item_code": item.item_code,
- "warehouse": item.s_warehouse or item.t_warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "qty": item.s_warehouse and -1*flt(item.transfer_qty) or flt(item.transfer_qty),
- "serial_no": item.serial_no,
- "voucher_type": self.doctype,
- "voucher_no": self.name,
- "company": self.company,
- "allow_zero_valuation": item.allow_zero_valuation_rate,
- })
+ return frappe._dict(
+ {
+ "item_code": item.item_code,
+ "warehouse": item.s_warehouse or item.t_warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "qty": item.s_warehouse and -1 * flt(item.transfer_qty) or flt(item.transfer_qty),
+ "serial_no": item.serial_no,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "company": self.company,
+ "allow_zero_valuation": item.allow_zero_valuation_rate,
+ }
+ )
def get_basic_rate_for_repacked_items(self, finished_item_qty, outgoing_items_cost):
finished_items = [d.item_code for d in self.get("items") if d.is_finished_item]
@@ -561,9 +722,11 @@ class StockEntry(StockController):
scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item])
# Get raw materials cost from BOM if multiple material consumption entries
- if not outgoing_items_cost and frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True):
+ if not outgoing_items_cost and frappe.db.get_single_value(
+ "Manufacturing Settings", "material_consumption", cache=True
+ ):
bom_items = self.get_bom_raw_materials(finished_item_qty)
- outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])
+ outgoing_items_cost = sum([flt(row.qty) * flt(row.rate) for row in bom_items.values()])
return flt((outgoing_items_cost - scrap_items_cost) / finished_item_qty)
@@ -595,8 +758,10 @@ class StockEntry(StockController):
for d in self.get("items"):
if d.transfer_qty:
d.amount = flt(flt(d.basic_amount) + flt(d.additional_cost), d.precision("amount"))
- d.valuation_rate = flt(flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty)),
- d.precision("valuation_rate"))
+ d.valuation_rate = flt(
+ flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty)),
+ d.precision("valuation_rate"),
+ )
def set_total_incoming_outgoing_value(self):
self.total_incoming_value = self.total_outgoing_value = 0.0
@@ -610,92 +775,120 @@ class StockEntry(StockController):
def set_total_amount(self):
self.total_amount = None
- if self.purpose not in ['Manufacture', 'Repack']:
+ if self.purpose not in ["Manufacture", "Repack"]:
self.total_amount = sum([flt(item.amount) for item in self.get("items")])
def set_stock_entry_type(self):
if self.purpose:
- self.stock_entry_type = frappe.get_cached_value('Stock Entry Type',
- {'purpose': self.purpose}, 'name')
+ self.stock_entry_type = frappe.get_cached_value(
+ "Stock Entry Type", {"purpose": self.purpose}, "name"
+ )
def set_purpose_for_stock_entry(self):
if self.stock_entry_type and not self.purpose:
- self.purpose = frappe.get_cached_value('Stock Entry Type',
- self.stock_entry_type, 'purpose')
+ self.purpose = frappe.get_cached_value("Stock Entry Type", self.stock_entry_type, "purpose")
def validate_duplicate_serial_no(self):
warehouse_wise_serial_nos = {}
# In case of repack the source and target serial nos could be same
- for warehouse in ['s_warehouse', 't_warehouse']:
+ for warehouse in ["s_warehouse", "t_warehouse"]:
serial_nos = []
for row in self.items:
- if not (row.serial_no and row.get(warehouse)): continue
+ if not (row.serial_no and row.get(warehouse)):
+ continue
for sn in get_serial_nos(row.serial_no):
if sn in serial_nos:
- frappe.throw(_('The serial no {0} has added multiple times in the stock entry {1}')
- .format(frappe.bold(sn), self.name))
+ frappe.throw(
+ _("The serial no {0} has added multiple times in the stock entry {1}").format(
+ frappe.bold(sn), self.name
+ )
+ )
serial_nos.append(sn)
def validate_purchase_order(self):
"""Throw exception if more raw material is transferred against Purchase Order than in
the raw materials supplied table"""
- backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings",
- "backflush_raw_materials_of_subcontract_based_on")
+ backflush_raw_materials_based_on = frappe.db.get_single_value(
+ "Buying Settings", "backflush_raw_materials_of_subcontract_based_on"
+ )
- qty_allowance = flt(frappe.db.get_single_value("Buying Settings",
- "over_transfer_allowance"))
+ qty_allowance = flt(frappe.db.get_single_value("Buying Settings", "over_transfer_allowance"))
- if not (self.purpose == "Send to Subcontractor" and self.purchase_order): return
+ if not (self.purpose == "Send to Subcontractor" and self.purchase_order):
+ return
- if (backflush_raw_materials_based_on == 'BOM'):
+ if backflush_raw_materials_based_on == "BOM":
purchase_order = frappe.get_doc("Purchase Order", self.purchase_order)
for se_item in self.items:
item_code = se_item.original_item or se_item.item_code
precision = cint(frappe.db.get_default("float_precision")) or 3
- required_qty = sum([flt(d.required_qty) for d in purchase_order.supplied_items \
- if d.rm_item_code == item_code])
+ required_qty = sum(
+ [flt(d.required_qty) for d in purchase_order.supplied_items if d.rm_item_code == item_code]
+ )
- total_allowed = required_qty + (required_qty * (qty_allowance/100))
+ total_allowed = required_qty + (required_qty * (qty_allowance / 100))
if not required_qty:
- bom_no = frappe.db.get_value("Purchase Order Item",
+ bom_no = frappe.db.get_value(
+ "Purchase Order Item",
{"parent": self.purchase_order, "item_code": se_item.subcontracted_item},
- "bom")
+ "bom",
+ )
if se_item.allow_alternative_item:
- original_item_code = frappe.get_value("Item Alternative", {"alternative_item_code": item_code}, "item_code")
+ original_item_code = frappe.get_value(
+ "Item Alternative", {"alternative_item_code": item_code}, "item_code"
+ )
- required_qty = sum([flt(d.required_qty) for d in purchase_order.supplied_items \
- if d.rm_item_code == original_item_code])
+ required_qty = sum(
+ [
+ flt(d.required_qty)
+ for d in purchase_order.supplied_items
+ if d.rm_item_code == original_item_code
+ ]
+ )
- total_allowed = required_qty + (required_qty * (qty_allowance/100))
+ total_allowed = required_qty + (required_qty * (qty_allowance / 100))
if not required_qty:
- frappe.throw(_("Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1}")
- .format(se_item.item_code, self.purchase_order))
- total_supplied = frappe.db.sql("""select sum(transfer_qty)
+ frappe.throw(
+ _("Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1}").format(
+ se_item.item_code, self.purchase_order
+ )
+ )
+ total_supplied = frappe.db.sql(
+ """select sum(transfer_qty)
from `tabStock Entry Detail`, `tabStock Entry`
where `tabStock Entry`.purchase_order = %s
and `tabStock Entry`.docstatus = 1
and `tabStock Entry Detail`.item_code = %s
and `tabStock Entry Detail`.parent = `tabStock Entry`.name""",
- (self.purchase_order, se_item.item_code))[0][0]
+ (self.purchase_order, se_item.item_code),
+ )[0][0]
if flt(total_supplied, precision) > flt(total_allowed, precision):
- frappe.throw(_("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}")
- .format(se_item.idx, se_item.item_code, total_allowed, self.purchase_order))
+ frappe.throw(
+ _("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}").format(
+ se_item.idx, se_item.item_code, total_allowed, self.purchase_order
+ )
+ )
elif backflush_raw_materials_based_on == "Material Transferred for Subcontract":
for row in self.items:
if not row.subcontracted_item:
- frappe.throw(_("Row {0}: Subcontracted Item is mandatory for the raw material {1}")
- .format(row.idx, frappe.bold(row.item_code)))
+ frappe.throw(
+ _("Row {0}: Subcontracted Item is mandatory for the raw material {1}").format(
+ row.idx, frappe.bold(row.item_code)
+ )
+ )
elif not row.po_detail:
filters = {
- "parent": self.purchase_order, "docstatus": 1,
- "rm_item_code": row.item_code, "main_item_code": row.subcontracted_item
+ "parent": self.purchase_order,
+ "docstatus": 1,
+ "rm_item_code": row.item_code,
+ "main_item_code": row.subcontracted_item,
}
po_detail = frappe.db.get_value("Purchase Order Item Supplied", filters, "name")
@@ -703,7 +896,7 @@ class StockEntry(StockController):
row.db_set("po_detail", po_detail)
def validate_bom(self):
- for d in self.get('items'):
+ for d in self.get("items"):
if d.bom_no and d.is_finished_item:
item_code = d.original_item or d.item_code
validate_bom_no(item_code, d.bom_no)
@@ -721,7 +914,7 @@ class StockEntry(StockController):
for d in self.items:
if d.t_warehouse and not d.s_warehouse:
- if self.purpose=="Repack" or d.item_code == finished_item:
+ if self.purpose == "Repack" or d.item_code == finished_item:
d.is_finished_item = 1
else:
d.is_scrap_item = 1
@@ -740,19 +933,17 @@ class StockEntry(StockController):
def validate_finished_goods(self):
"""
- 1. Check if FG exists (mfg, repack)
- 2. Check if Multiple FG Items are present (mfg)
- 3. Check FG Item and Qty against WO if present (mfg)
+ 1. Check if FG exists (mfg, repack)
+ 2. Check if Multiple FG Items are present (mfg)
+ 3. Check FG Item and Qty against WO if present (mfg)
"""
production_item, wo_qty, finished_items = None, 0, []
- wo_details = frappe.db.get_value(
- "Work Order", self.work_order, ["production_item", "qty"]
- )
+ wo_details = frappe.db.get_value("Work Order", self.work_order, ["production_item", "qty"])
if wo_details:
production_item, wo_qty = wo_details
- for d in self.get('items'):
+ for d in self.get("items"):
if d.is_finished_item:
if not self.work_order:
# Independent MFG Entry/ Repack Entry, no WO to match against
@@ -760,12 +951,16 @@ class StockEntry(StockController):
continue
if d.item_code != production_item:
- frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
- .format(d.item_code, self.work_order)
+ frappe.throw(
+ _("Finished Item {0} does not match with Work Order {1}").format(
+ d.item_code, self.work_order
+ )
)
elif flt(d.transfer_qty) > flt(self.fg_completed_qty):
- frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}")
- .format(d.idx, d.transfer_qty, self.fg_completed_qty)
+ frappe.throw(
+ _("Quantity in row {0} ({1}) must be same as manufactured quantity {2}").format(
+ d.idx, d.transfer_qty, self.fg_completed_qty
+ )
)
finished_items.append(d.item_code)
@@ -773,28 +968,31 @@ class StockEntry(StockController):
if not finished_items:
frappe.throw(
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
- title=_("Missing Finished Good"), exc=FinishedGoodError
+ title=_("Missing Finished Good"),
+ exc=FinishedGoodError,
)
if self.purpose == "Manufacture":
if len(set(finished_items)) > 1:
frappe.throw(
msg=_("Multiple items cannot be marked as finished item"),
- title=_("Note"), exc=FinishedGoodError
+ title=_("Note"),
+ exc=FinishedGoodError,
)
allowance_percentage = flt(
frappe.db.get_single_value(
- "Manufacturing Settings","overproduction_percentage_for_work_order"
+ "Manufacturing Settings", "overproduction_percentage_for_work_order"
)
)
- allowed_qty = wo_qty + ((allowance_percentage/100) * wo_qty)
+ allowed_qty = wo_qty + ((allowance_percentage / 100) * wo_qty)
# No work order could mean independent Manufacture entry, if so skip validation
if self.work_order and self.fg_completed_qty > allowed_qty:
frappe.throw(
- _("For quantity {0} should not be greater than work order quantity {1}")
- .format(flt(self.fg_completed_qty), wo_qty)
+ _("For quantity {0} should not be greater than work order quantity {1}").format(
+ flt(self.fg_completed_qty), wo_qty
+ )
)
def update_stock_ledger(self):
@@ -816,35 +1014,38 @@ class StockEntry(StockController):
def get_finished_item_row(self):
finished_item_row = None
if self.purpose in ("Manufacture", "Repack"):
- for d in self.get('items'):
+ for d in self.get("items"):
if d.is_finished_item:
finished_item_row = d
return finished_item_row
def get_sle_for_source_warehouse(self, sl_entries, finished_item_row):
- for d in self.get('items'):
+ for d in self.get("items"):
if cstr(d.s_warehouse):
- sle = self.get_sl_entries(d, {
- "warehouse": cstr(d.s_warehouse),
- "actual_qty": -flt(d.transfer_qty),
- "incoming_rate": 0
- })
+ sle = self.get_sl_entries(
+ d, {"warehouse": cstr(d.s_warehouse), "actual_qty": -flt(d.transfer_qty), "incoming_rate": 0}
+ )
if cstr(d.t_warehouse):
sle.dependant_sle_voucher_detail_no = d.name
- elif finished_item_row and (finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse):
+ elif finished_item_row and (
+ finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse
+ ):
sle.dependant_sle_voucher_detail_no = finished_item_row.name
sl_entries.append(sle)
def get_sle_for_target_warehouse(self, sl_entries, finished_item_row):
- for d in self.get('items'):
+ for d in self.get("items"):
if cstr(d.t_warehouse):
- sle = self.get_sl_entries(d, {
- "warehouse": cstr(d.t_warehouse),
- "actual_qty": flt(d.transfer_qty),
- "incoming_rate": flt(d.valuation_rate)
- })
+ sle = self.get_sl_entries(
+ d,
+ {
+ "warehouse": cstr(d.t_warehouse),
+ "actual_qty": flt(d.transfer_qty),
+ "incoming_rate": flt(d.valuation_rate),
+ },
+ )
if cstr(d.s_warehouse) or (finished_item_row and d.name == finished_item_row.name):
sle.recalculate_rate = 1
@@ -874,40 +1075,55 @@ class StockEntry(StockController):
continue
item_account_wise_additional_cost.setdefault((d.item_code, d.name), {})
- item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, {
- "amount": 0.0,
- "base_amount": 0.0
- })
+ item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(
+ t.expense_account, {"amount": 0.0, "base_amount": 0.0}
+ )
multiply_based_on = d.basic_amount if total_basic_amount else d.qty
- item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += \
+ item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += (
flt(t.amount * multiply_based_on) / divide_based_on
+ )
- item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += \
+ item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += (
flt(t.base_amount * multiply_based_on) / divide_based_on
+ )
if item_account_wise_additional_cost:
for d in self.get("items"):
- for account, amount in iteritems(item_account_wise_additional_cost.get((d.item_code, d.name), {})):
- if not amount: continue
+ for account, amount in iteritems(
+ item_account_wise_additional_cost.get((d.item_code, d.name), {})
+ ):
+ if not amount:
+ continue
- gl_entries.append(self.get_gl_dict({
- "account": account,
- "against": d.expense_account,
- "cost_center": d.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit_in_account_currency": flt(amount["amount"]),
- "credit": flt(amount["base_amount"])
- }, item=d))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": account,
+ "against": d.expense_account,
+ "cost_center": d.cost_center,
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
+ "credit_in_account_currency": flt(amount["amount"]),
+ "credit": flt(amount["base_amount"]),
+ },
+ item=d,
+ )
+ )
- gl_entries.append(self.get_gl_dict({
- "account": d.expense_account,
- "against": account,
- "cost_center": d.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": -1 * amount['base_amount'] # put it as negative credit instead of debit purposefully
- }, item=d))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": d.expense_account,
+ "against": account,
+ "cost_center": d.cost_center,
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
+ "credit": -1
+ * amount["base_amount"], # put it as negative credit instead of debit purposefully
+ },
+ item=d,
+ )
+ )
return process_gl_map(gl_entries)
@@ -916,11 +1132,13 @@ class StockEntry(StockController):
if flt(pro_doc.docstatus) != 1:
frappe.throw(_("Work Order {0} must be submitted").format(self.work_order))
- if pro_doc.status == 'Stopped':
- frappe.throw(_("Transaction not allowed against stopped Work Order {0}").format(self.work_order))
+ if pro_doc.status == "Stopped":
+ frappe.throw(
+ _("Transaction not allowed against stopped Work Order {0}").format(self.work_order)
+ )
if self.job_card:
- job_doc = frappe.get_doc('Job Card', self.job_card)
+ job_doc = frappe.get_doc("Job Card", self.job_card)
job_doc.set_transferred_qty(update_status=True)
job_doc.set_transferred_qty_in_job_card(self)
@@ -940,73 +1158,95 @@ class StockEntry(StockController):
@frappe.whitelist()
def get_item_details(self, args=None, for_update=False):
- item = frappe.db.sql("""select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group,
+ item = frappe.db.sql(
+ """select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group,
i.has_batch_no, i.sample_quantity, i.has_serial_no, i.allow_alternative_item,
id.expense_account, id.buying_cost_center
from `tabItem` i LEFT JOIN `tabItem Default` id ON i.name=id.parent and id.company=%s
where i.name=%s
and i.disabled=0
and (i.end_of_life is null or i.end_of_life='0000-00-00' or i.end_of_life > %s)""",
- (self.company, args.get('item_code'), nowdate()), as_dict = 1)
+ (self.company, args.get("item_code"), nowdate()),
+ as_dict=1,
+ )
if not item:
- frappe.throw(_("Item {0} is not active or end of life has been reached").format(args.get("item_code")))
+ frappe.throw(
+ _("Item {0} is not active or end of life has been reached").format(args.get("item_code"))
+ )
item = item[0]
item_group_defaults = get_item_group_defaults(item.name, self.company)
brand_defaults = get_brand_defaults(item.name, self.company)
- ret = frappe._dict({
- 'uom' : item.stock_uom,
- 'stock_uom' : item.stock_uom,
- 'description' : item.description,
- 'image' : item.image,
- 'item_name' : item.item_name,
- 'cost_center' : get_default_cost_center(args, item, item_group_defaults, brand_defaults, self.company),
- 'qty' : args.get("qty"),
- 'transfer_qty' : args.get('qty'),
- 'conversion_factor' : 1,
- 'batch_no' : '',
- 'actual_qty' : 0,
- 'basic_rate' : 0,
- 'serial_no' : '',
- 'has_serial_no' : item.has_serial_no,
- 'has_batch_no' : item.has_batch_no,
- 'sample_quantity' : item.sample_quantity,
- 'expense_account' : item.expense_account
- })
+ ret = frappe._dict(
+ {
+ "uom": item.stock_uom,
+ "stock_uom": item.stock_uom,
+ "description": item.description,
+ "image": item.image,
+ "item_name": item.item_name,
+ "cost_center": get_default_cost_center(
+ args, item, item_group_defaults, brand_defaults, self.company
+ ),
+ "qty": args.get("qty"),
+ "transfer_qty": args.get("qty"),
+ "conversion_factor": 1,
+ "batch_no": "",
+ "actual_qty": 0,
+ "basic_rate": 0,
+ "serial_no": "",
+ "has_serial_no": item.has_serial_no,
+ "has_batch_no": item.has_batch_no,
+ "sample_quantity": item.sample_quantity,
+ "expense_account": item.expense_account,
+ }
+ )
- if self.purpose == 'Send to Subcontractor':
+ if self.purpose == "Send to Subcontractor":
ret["allow_alternative_item"] = item.allow_alternative_item
# update uom
if args.get("uom") and for_update:
- ret.update(get_uom_details(args.get('item_code'), args.get('uom'), args.get('qty')))
+ ret.update(get_uom_details(args.get("item_code"), args.get("uom"), args.get("qty")))
- if self.purpose == 'Material Issue':
- ret["expense_account"] = (item.get("expense_account") or
- item_group_defaults.get("expense_account") or
- frappe.get_cached_value('Company', self.company, "default_expense_account"))
+ if self.purpose == "Material Issue":
+ ret["expense_account"] = (
+ item.get("expense_account")
+ or item_group_defaults.get("expense_account")
+ or frappe.get_cached_value("Company", self.company, "default_expense_account")
+ )
- for company_field, field in {'stock_adjustment_account': 'expense_account',
- 'cost_center': 'cost_center'}.items():
+ for company_field, field in {
+ "stock_adjustment_account": "expense_account",
+ "cost_center": "cost_center",
+ }.items():
if not ret.get(field):
- ret[field] = frappe.get_cached_value('Company', self.company, company_field)
+ ret[field] = frappe.get_cached_value("Company", self.company, company_field)
- args['posting_date'] = self.posting_date
- args['posting_time'] = self.posting_time
+ args["posting_date"] = self.posting_date
+ args["posting_time"] = self.posting_time
- stock_and_rate = get_warehouse_details(args) if args.get('warehouse') else {}
+ stock_and_rate = get_warehouse_details(args) if args.get("warehouse") else {}
ret.update(stock_and_rate)
# automatically select batch for outgoing item
- if (args.get('s_warehouse', None) and args.get('qty') and
- ret.get('has_batch_no') and not args.get('batch_no')):
- args.batch_no = get_batch_no(args['item_code'], args['s_warehouse'], args['qty'])
+ if (
+ args.get("s_warehouse", None)
+ and args.get("qty")
+ and ret.get("has_batch_no")
+ and not args.get("batch_no")
+ ):
+ args.batch_no = get_batch_no(args["item_code"], args["s_warehouse"], args["qty"])
- if self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get('item_code'):
- subcontract_items = frappe.get_all("Purchase Order Item Supplied",
- {"parent": self.purchase_order, "rm_item_code": args.get('item_code')}, "main_item_code")
+ if (
+ self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get("item_code")
+ ):
+ subcontract_items = frappe.get_all(
+ "Purchase Order Item Supplied",
+ {"parent": self.purchase_order, "rm_item_code": args.get("item_code")},
+ "main_item_code",
+ )
if subcontract_items and len(subcontract_items) == 1:
ret["subcontracted_item"] = subcontract_items[0].main_item_code
@@ -1017,46 +1257,57 @@ class StockEntry(StockController):
def set_items_for_stock_in(self):
self.items = []
- if self.outgoing_stock_entry and self.purpose == 'Material Transfer':
- doc = frappe.get_doc('Stock Entry', self.outgoing_stock_entry)
+ if self.outgoing_stock_entry and self.purpose == "Material Transfer":
+ doc = frappe.get_doc("Stock Entry", self.outgoing_stock_entry)
if doc.per_transferred == 100:
- frappe.throw(_("Goods are already received against the outward entry {0}")
- .format(doc.name))
+ frappe.throw(_("Goods are already received against the outward entry {0}").format(doc.name))
for d in doc.items:
- self.append('items', {
- 's_warehouse': d.t_warehouse,
- 'item_code': d.item_code,
- 'qty': d.qty,
- 'uom': d.uom,
- 'against_stock_entry': d.parent,
- 'ste_detail': d.name,
- 'stock_uom': d.stock_uom,
- 'conversion_factor': d.conversion_factor,
- 'serial_no': d.serial_no,
- 'batch_no': d.batch_no
- })
+ self.append(
+ "items",
+ {
+ "s_warehouse": d.t_warehouse,
+ "item_code": d.item_code,
+ "qty": d.qty,
+ "uom": d.uom,
+ "against_stock_entry": d.parent,
+ "ste_detail": d.name,
+ "stock_uom": d.stock_uom,
+ "conversion_factor": d.conversion_factor,
+ "serial_no": d.serial_no,
+ "batch_no": d.batch_no,
+ },
+ )
@frappe.whitelist()
def get_items(self):
- self.set('items', [])
+ self.set("items", [])
self.validate_work_order()
if not self.posting_date or not self.posting_time:
frappe.throw(_("Posting date and posting time is mandatory"))
self.set_work_order_details()
- self.flags.backflush_based_on = frappe.db.get_single_value("Manufacturing Settings",
- "backflush_raw_materials_based_on")
+ self.flags.backflush_based_on = frappe.db.get_single_value(
+ "Manufacturing Settings", "backflush_raw_materials_based_on"
+ )
if self.bom_no:
- backflush_based_on = frappe.db.get_single_value("Manufacturing Settings",
- "backflush_raw_materials_based_on")
+ backflush_based_on = frappe.db.get_single_value(
+ "Manufacturing Settings", "backflush_raw_materials_based_on"
+ )
- if self.purpose in ["Material Issue", "Material Transfer", "Manufacture", "Repack",
- "Send to Subcontractor", "Material Transfer for Manufacture", "Material Consumption for Manufacture"]:
+ if self.purpose in [
+ "Material Issue",
+ "Material Transfer",
+ "Manufacture",
+ "Repack",
+ "Send to Subcontractor",
+ "Material Transfer for Manufacture",
+ "Material Consumption for Manufacture",
+ ]:
if self.work_order and self.purpose == "Material Transfer for Manufacture":
item_dict = self.get_pending_raw_materials(backflush_based_on)
@@ -1065,14 +1316,20 @@ class StockEntry(StockController):
item["to_warehouse"] = self.pro_doc.wip_warehouse
self.add_to_stock_entry_detail(item_dict)
- elif (self.work_order and (self.purpose == "Manufacture"
- or self.purpose == "Material Consumption for Manufacture") and not self.pro_doc.skip_transfer
- and self.flags.backflush_based_on == "Material Transferred for Manufacture"):
+ elif (
+ self.work_order
+ and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture")
+ and not self.pro_doc.skip_transfer
+ and self.flags.backflush_based_on == "Material Transferred for Manufacture"
+ ):
self.get_transfered_raw_materials()
- elif (self.work_order and (self.purpose == "Manufacture" or
- self.purpose == "Material Consumption for Manufacture") and self.flags.backflush_based_on== "BOM"
- and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1):
+ elif (
+ self.work_order
+ and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture")
+ and self.flags.backflush_based_on == "BOM"
+ and frappe.db.get_single_value("Manufacturing Settings", "material_consumption") == 1
+ ):
self.get_unconsumed_raw_materials()
else:
@@ -1081,31 +1338,36 @@ class StockEntry(StockController):
item_dict = self.get_bom_raw_materials(self.fg_completed_qty)
- #Get PO Supplied Items Details
+ # Get PO Supplied Items Details
if self.purchase_order and self.purpose == "Send to Subcontractor":
- #Get PO Supplied Items Details
- item_wh = frappe._dict(frappe.db.sql("""
+ # Get PO Supplied Items Details
+ item_wh = frappe._dict(
+ frappe.db.sql(
+ """
SELECT
rm_item_code, reserve_warehouse
FROM
`tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup
WHERE
- po.name = poitemsup.parent and po.name = %s """,self.purchase_order))
+ po.name = poitemsup.parent and po.name = %s """,
+ self.purchase_order,
+ )
+ )
for item in itervalues(item_dict):
if self.pro_doc and cint(self.pro_doc.from_wip_warehouse):
item["from_warehouse"] = self.pro_doc.wip_warehouse
- #Get Reserve Warehouse from PO
- if self.purchase_order and self.purpose=="Send to Subcontractor":
+ # Get Reserve Warehouse from PO
+ if self.purchase_order and self.purpose == "Send to Subcontractor":
item["from_warehouse"] = item_wh.get(item.item_code)
- item["to_warehouse"] = self.to_warehouse if self.purpose=="Send to Subcontractor" else ""
+ item["to_warehouse"] = self.to_warehouse if self.purpose == "Send to Subcontractor" else ""
self.add_to_stock_entry_detail(item_dict)
# fetch the serial_no of the first stock entry for the second stock entry
if self.work_order and self.purpose == "Manufacture":
self.set_serial_nos(self.work_order)
- work_order = frappe.get_doc('Work Order', self.work_order)
+ work_order = frappe.get_doc("Work Order", self.work_order)
add_additional_cost(self, work_order)
# add finished goods item
@@ -1121,8 +1383,8 @@ class StockEntry(StockController):
def set_scrap_items(self):
if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]:
scrap_item_dict = self.get_bom_scrap_material(self.fg_completed_qty)
- for item in itervalues(scrap_item_dict):
- item.idx = ''
+
+ for item in scrap_item_dict.values():
if self.pro_doc and self.pro_doc.scrap_warehouse:
item["to_warehouse"] = self.pro_doc.scrap_warehouse
@@ -1135,7 +1397,7 @@ class StockEntry(StockController):
if self.work_order:
# common validations
if not self.pro_doc:
- self.pro_doc = frappe.get_doc('Work Order', self.work_order)
+ self.pro_doc = frappe.get_doc("Work Order", self.work_order)
if self.pro_doc:
self.bom_no = self.pro_doc.bom_no
@@ -1166,11 +1428,18 @@ class StockEntry(StockController):
"stock_uom": item.stock_uom,
"expense_account": item.get("expense_account"),
"cost_center": item.get("buying_cost_center"),
- "is_finished_item": 1
+ "is_finished_item": 1,
}
- if self.work_order and self.pro_doc.has_batch_no and cint(frappe.db.get_single_value('Manufacturing Settings',
- 'make_serial_no_batch_from_work_order', cache=True)):
+ if (
+ self.work_order
+ and self.pro_doc.has_batch_no
+ and cint(
+ frappe.db.get_single_value(
+ "Manufacturing Settings", "make_serial_no_batch_from_work_order", cache=True
+ )
+ )
+ ):
self.set_batchwise_finished_goods(args, item)
else:
self.add_finished_goods(args, item)
@@ -1179,12 +1448,12 @@ class StockEntry(StockController):
filters = {
"reference_name": self.pro_doc.name,
"reference_doctype": self.pro_doc.doctype,
- "qty_to_produce": (">", 0)
+ "qty_to_produce": (">", 0),
}
fields = ["qty_to_produce as qty", "produced_qty", "name"]
- data = frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc")
+ data = frappe.get_all("Batch", filters=filters, fields=fields, order_by="creation asc")
if not data:
self.add_finished_goods(args, item)
@@ -1199,7 +1468,7 @@ class StockEntry(StockController):
if not batch_qty:
continue
- if qty <=0:
+ if qty <= 0:
break
fg_qty = batch_qty
@@ -1213,23 +1482,27 @@ class StockEntry(StockController):
self.add_finished_goods(args, item)
def add_finished_goods(self, args, item):
- self.add_to_stock_entry_detail({
- item.name: args
- }, bom_no = self.bom_no)
+ self.add_to_stock_entry_detail({item.name: args}, bom_no=self.bom_no)
def get_bom_raw_materials(self, qty):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
# item dict = { item_code: {qty, description, stock_uom} }
- item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty,
- fetch_exploded = self.use_multi_level_bom, fetch_qty_in_stock_uom=False)
+ item_dict = get_bom_items_as_dict(
+ self.bom_no,
+ self.company,
+ qty=qty,
+ fetch_exploded=self.use_multi_level_bom,
+ fetch_qty_in_stock_uom=False,
+ )
- used_alternative_items = get_used_alternative_items(work_order = self.work_order)
+ used_alternative_items = get_used_alternative_items(work_order=self.work_order)
for item in itervalues(item_dict):
# if source warehouse presents in BOM set from_warehouse as bom source_warehouse
if item["allow_alternative_item"]:
- item["allow_alternative_item"] = frappe.db.get_value('Work Order',
- self.work_order, "allow_alternative_item")
+ item["allow_alternative_item"] = frappe.db.get_value(
+ "Work Order", self.work_order, "allow_alternative_item"
+ )
item.from_warehouse = self.from_warehouse or item.source_warehouse or item.default_warehouse
if item.item_code in used_alternative_items:
@@ -1247,8 +1520,10 @@ class StockEntry(StockController):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
# item dict = { item_code: {qty, description, stock_uom} }
- item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty,
- fetch_exploded = 0, fetch_scrap_items = 1) or {}
+ item_dict = (
+ get_bom_items_as_dict(self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1)
+ or {}
+ )
for item in itervalues(item_dict):
item.from_warehouse = ""
@@ -1262,16 +1537,18 @@ class StockEntry(StockController):
if not item_row:
item_row = frappe._dict({})
- item_row.update({
- 'uom': row.stock_uom,
- 'from_warehouse': '',
- 'qty': row.stock_qty + flt(item_row.stock_qty),
- 'converison_factor': 1,
- 'is_scrap_item': 1,
- 'item_name': row.item_name,
- 'description': row.description,
- 'allow_zero_valuation_rate': 1
- })
+ item_row.update(
+ {
+ "uom": row.stock_uom,
+ "from_warehouse": "",
+ "qty": row.stock_qty + flt(item_row.stock_qty),
+ "converison_factor": 1,
+ "is_scrap_item": 1,
+ "item_name": row.item_name,
+ "description": row.description,
+ "allow_zero_valuation_rate": 1,
+ }
+ )
item_dict[row.item_code] = item_row
@@ -1284,21 +1561,25 @@ class StockEntry(StockController):
if not self.pro_doc.operations:
return []
- job_card = frappe.qb.DocType('Job Card')
- job_card_scrap_item = frappe.qb.DocType('Job Card Scrap Item')
+ job_card = frappe.qb.DocType("Job Card")
+ job_card_scrap_item = frappe.qb.DocType("Job Card Scrap Item")
scrap_items = (
frappe.qb.from_(job_card)
.select(
- Sum(job_card_scrap_item.stock_qty).as_('stock_qty'),
- job_card_scrap_item.item_code, job_card_scrap_item.item_name,
- job_card_scrap_item.description, job_card_scrap_item.stock_uom)
+ Sum(job_card_scrap_item.stock_qty).as_("stock_qty"),
+ job_card_scrap_item.item_code,
+ job_card_scrap_item.item_name,
+ job_card_scrap_item.description,
+ job_card_scrap_item.stock_uom,
+ )
.join(job_card_scrap_item)
.on(job_card_scrap_item.parent == job_card.name)
.where(
(job_card_scrap_item.item_code.isnotnull())
& (job_card.work_order == self.work_order)
- & (job_card.docstatus == 1))
+ & (job_card.docstatus == 1)
+ )
.groupby(job_card_scrap_item.item_code)
).run(as_dict=1)
@@ -1312,7 +1593,7 @@ class StockEntry(StockController):
if used_scrap_items.get(row.item_code):
used_scrap_items[row.item_code] -= row.stock_qty
- if cint(frappe.get_cached_value('UOM', row.stock_uom, 'must_be_whole_number')):
+ if cint(frappe.get_cached_value("UOM", row.stock_uom, "must_be_whole_number")):
row.stock_qty = frappe.utils.ceil(row.stock_qty)
return scrap_items
@@ -1323,16 +1604,14 @@ class StockEntry(StockController):
def get_used_scrap_items(self):
used_scrap_items = defaultdict(float)
data = frappe.get_all(
- 'Stock Entry',
- fields = [
- '`tabStock Entry Detail`.`item_code`', '`tabStock Entry Detail`.`qty`'
+ "Stock Entry",
+ fields=["`tabStock Entry Detail`.`item_code`", "`tabStock Entry Detail`.`qty`"],
+ filters=[
+ ["Stock Entry", "work_order", "=", self.work_order],
+ ["Stock Entry Detail", "is_scrap_item", "=", 1],
+ ["Stock Entry", "docstatus", "=", 1],
+ ["Stock Entry", "purpose", "in", ["Repack", "Manufacture"]],
],
- filters = [
- ['Stock Entry', 'work_order', '=', self.work_order],
- ['Stock Entry Detail', 'is_scrap_item', '=', 1],
- ['Stock Entry', 'docstatus', '=', 1],
- ['Stock Entry', 'purpose', 'in', ['Repack', 'Manufacture']]
- ]
)
for row in data:
@@ -1342,10 +1621,11 @@ class StockEntry(StockController):
def get_unconsumed_raw_materials(self):
wo = frappe.get_doc("Work Order", self.work_order)
- wo_items = frappe.get_all('Work Order Item',
- filters={'parent': self.work_order},
- fields=["item_code", "source_warehouse", "required_qty", "consumed_qty", "transferred_qty"]
- )
+ wo_items = frappe.get_all(
+ "Work Order Item",
+ filters={"parent": self.work_order},
+ fields=["item_code", "source_warehouse", "required_qty", "consumed_qty", "transferred_qty"],
+ )
work_order_qty = wo.material_transferred_for_manufacturing or wo.qty
for item in wo_items:
@@ -1362,21 +1642,24 @@ class StockEntry(StockController):
qty = req_qty_each * flt(self.fg_completed_qty)
if qty > 0:
- self.add_to_stock_entry_detail({
- item.item_code: {
- "from_warehouse": wo.wip_warehouse or item.source_warehouse,
- "to_warehouse": "",
- "qty": qty,
- "item_name": item.item_name,
- "description": item.description,
- "stock_uom": item_account_details.stock_uom,
- "expense_account": item_account_details.get("expense_account"),
- "cost_center": item_account_details.get("buying_cost_center"),
+ self.add_to_stock_entry_detail(
+ {
+ item.item_code: {
+ "from_warehouse": wo.wip_warehouse or item.source_warehouse,
+ "to_warehouse": "",
+ "qty": qty,
+ "item_name": item.item_name,
+ "description": item.description,
+ "stock_uom": item_account_details.stock_uom,
+ "expense_account": item_account_details.get("expense_account"),
+ "cost_center": item_account_details.get("buying_cost_center"),
+ }
}
- })
+ )
def get_transfered_raw_materials(self):
- transferred_materials = frappe.db.sql("""
+ transferred_materials = frappe.db.sql(
+ """
select
item_name, original_item, item_code, sum(qty) as qty, sed.t_warehouse as warehouse,
description, stock_uom, expense_account, cost_center
@@ -1385,9 +1668,13 @@ class StockEntry(StockController):
se.name = sed.parent and se.docstatus=1 and se.purpose='Material Transfer for Manufacture'
and se.work_order= %s and ifnull(sed.t_warehouse, '') != ''
group by sed.item_code, sed.t_warehouse
- """, self.work_order, as_dict=1)
+ """,
+ self.work_order,
+ as_dict=1,
+ )
- materials_already_backflushed = frappe.db.sql("""
+ materials_already_backflushed = frappe.db.sql(
+ """
select
item_code, sed.s_warehouse as warehouse, sum(qty) as qty
from
@@ -1397,26 +1684,34 @@ class StockEntry(StockController):
and (se.purpose='Manufacture' or se.purpose='Material Consumption for Manufacture')
and se.work_order= %s and ifnull(sed.s_warehouse, '') != ''
group by sed.item_code, sed.s_warehouse
- """, self.work_order, as_dict=1)
+ """,
+ self.work_order,
+ as_dict=1,
+ )
- backflushed_materials= {}
+ backflushed_materials = {}
for d in materials_already_backflushed:
- backflushed_materials.setdefault(d.item_code,[]).append({d.warehouse: d.qty})
+ backflushed_materials.setdefault(d.item_code, []).append({d.warehouse: d.qty})
- po_qty = frappe.db.sql("""select qty, produced_qty, material_transferred_for_manufacturing from
- `tabWork Order` where name=%s""", self.work_order, as_dict=1)[0]
+ po_qty = frappe.db.sql(
+ """select qty, produced_qty, material_transferred_for_manufacturing from
+ `tabWork Order` where name=%s""",
+ self.work_order,
+ as_dict=1,
+ )[0]
manufacturing_qty = flt(po_qty.qty) or 1
produced_qty = flt(po_qty.produced_qty)
trans_qty = flt(po_qty.material_transferred_for_manufacturing) or 1
for item in transferred_materials:
- qty= item.qty
+ qty = item.qty
item_code = item.original_item or item.item_code
- req_items = frappe.get_all('Work Order Item',
- filters={'parent': self.work_order, 'item_code': item_code},
- fields=["required_qty", "consumed_qty"]
- )
+ req_items = frappe.get_all(
+ "Work Order Item",
+ filters={"parent": self.work_order, "item_code": item_code},
+ fields=["required_qty", "consumed_qty"],
+ )
req_qty = flt(req_items[0].required_qty) if req_items else flt(4)
req_qty_each = flt(req_qty / manufacturing_qty)
@@ -1424,23 +1719,23 @@ class StockEntry(StockController):
if trans_qty and manufacturing_qty > (produced_qty + flt(self.fg_completed_qty)):
if qty >= req_qty:
- qty = (req_qty/trans_qty) * flt(self.fg_completed_qty)
+ qty = (req_qty / trans_qty) * flt(self.fg_completed_qty)
else:
qty = qty - consumed_qty
- if self.purpose == 'Manufacture':
+ if self.purpose == "Manufacture":
# If Material Consumption is booked, must pull only remaining components to finish product
if consumed_qty != 0:
remaining_qty = consumed_qty - (produced_qty * req_qty_each)
exhaust_qty = req_qty_each * produced_qty
- if remaining_qty > exhaust_qty :
- if (remaining_qty/(req_qty_each * flt(self.fg_completed_qty))) >= 1:
- qty =0
+ if remaining_qty > exhaust_qty:
+ if (remaining_qty / (req_qty_each * flt(self.fg_completed_qty))) >= 1:
+ qty = 0
else:
qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty
else:
if self.flags.backflush_based_on == "Material Transferred for Manufacture":
- qty = (item.qty/trans_qty) * flt(self.fg_completed_qty)
+ qty = (item.qty / trans_qty) * flt(self.fg_completed_qty)
else:
qty = req_qty_each * flt(self.fg_completed_qty)
@@ -1448,45 +1743,51 @@ class StockEntry(StockController):
precision = frappe.get_precision("Stock Entry Detail", "qty")
for d in backflushed_materials.get(item.item_code):
if d.get(item.warehouse) > 0:
- if (qty > req_qty):
- qty = ((flt(qty, precision) - flt(d.get(item.warehouse), precision))
+ if qty > req_qty:
+ qty = (
+ (flt(qty, precision) - flt(d.get(item.warehouse), precision))
/ (flt(trans_qty, precision) - flt(produced_qty, precision))
) * flt(self.fg_completed_qty)
d[item.warehouse] -= qty
- if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')):
+ if cint(frappe.get_cached_value("UOM", item.stock_uom, "must_be_whole_number")):
qty = frappe.utils.ceil(qty)
if qty > 0:
- self.add_to_stock_entry_detail({
- item.item_code: {
- "from_warehouse": item.warehouse,
- "to_warehouse": "",
- "qty": qty,
- "item_name": item.item_name,
- "description": item.description,
- "stock_uom": item.stock_uom,
- "expense_account": item.expense_account,
- "cost_center": item.buying_cost_center,
- "original_item": item.original_item
+ self.add_to_stock_entry_detail(
+ {
+ item.item_code: {
+ "from_warehouse": item.warehouse,
+ "to_warehouse": "",
+ "qty": qty,
+ "item_name": item.item_name,
+ "description": item.description,
+ "stock_uom": item.stock_uom,
+ "expense_account": item.expense_account,
+ "cost_center": item.buying_cost_center,
+ "original_item": item.original_item,
+ }
}
- })
+ )
def get_pending_raw_materials(self, backflush_based_on=None):
"""
- issue (item quantity) that is pending to issue or desire to transfer,
- whichever is less
+ issue (item quantity) that is pending to issue or desire to transfer,
+ whichever is less
"""
item_dict = self.get_pro_order_required_items(backflush_based_on)
max_qty = flt(self.pro_doc.qty)
allow_overproduction = False
- overproduction_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
- "overproduction_percentage_for_work_order"))
+ overproduction_percentage = flt(
+ frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order")
+ )
- to_transfer_qty = flt(self.pro_doc.material_transferred_for_manufacturing) + flt(self.fg_completed_qty)
+ to_transfer_qty = flt(self.pro_doc.material_transferred_for_manufacturing) + flt(
+ self.fg_completed_qty
+ )
transfer_limit_qty = max_qty + ((max_qty * overproduction_percentage) / 100)
if transfer_limit_qty >= to_transfer_qty:
@@ -1496,10 +1797,14 @@ class StockEntry(StockController):
pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty)
desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty
- if (desire_to_transfer <= pending_to_issue
+ if (
+ desire_to_transfer <= pending_to_issue
or (desire_to_transfer > 0 and backflush_based_on == "Material Transferred for Manufacture")
- or allow_overproduction):
- item_dict[item]["qty"] = desire_to_transfer
+ or allow_overproduction
+ ):
+ # "No need for transfer but qty still pending to transfer" case can occur
+ # when transferring multiple RM in different Stock Entries
+ item_dict[item]["qty"] = desire_to_transfer if (desire_to_transfer > 0) else pending_to_issue
elif pending_to_issue > 0:
item_dict[item]["qty"] = pending_to_issue
else:
@@ -1519,7 +1824,7 @@ class StockEntry(StockController):
def get_pro_order_required_items(self, backflush_based_on=None):
"""
- Gets Work Order Required Items only if Stock Entry purpose is **Material Transferred for Manufacture**.
+ Gets Work Order Required Items only if Stock Entry purpose is **Material Transferred for Manufacture**.
"""
item_dict, job_card_items = frappe._dict(), []
work_order = frappe.get_doc("Work Order", self.work_order)
@@ -1538,7 +1843,9 @@ class StockEntry(StockController):
continue
transfer_pending = flt(d.required_qty) > flt(d.transferred_qty)
- can_transfer = transfer_pending or (backflush_based_on == "Material Transferred for Manufacture")
+ can_transfer = transfer_pending or (
+ backflush_based_on == "Material Transferred for Manufacture"
+ )
if not can_transfer:
continue
@@ -1549,11 +1856,7 @@ class StockEntry(StockController):
if consider_job_card:
job_card_item = frappe.db.get_value(
- "Job Card Item",
- {
- "item_code": d.item_code,
- "parent": self.get("job_card")
- }
+ "Job Card Item", {"item_code": d.item_code, "parent": self.get("job_card")}
)
item_row["job_card_item"] = job_card_item or None
@@ -1573,12 +1876,7 @@ class StockEntry(StockController):
return []
job_card_items = frappe.get_all(
- "Job Card Item",
- filters={
- "parent": job_card
- },
- fields=["item_code"],
- distinct=True
+ "Job Card Item", filters={"parent": job_card}, fields=["item_code"], distinct=True
)
return [d.item_code for d in job_card_items]
@@ -1587,60 +1885,86 @@ class StockEntry(StockController):
item_row = item_dict[d]
stock_uom = item_row.get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom")
- se_child = self.append('items')
+ se_child = self.append("items")
se_child.s_warehouse = item_row.get("from_warehouse")
se_child.t_warehouse = item_row.get("to_warehouse")
- se_child.item_code = item_row.get('item_code') or cstr(d)
+ se_child.item_code = item_row.get("item_code") or cstr(d)
se_child.uom = item_row["uom"] if item_row.get("uom") else stock_uom
se_child.stock_uom = stock_uom
se_child.qty = flt(item_row["qty"], se_child.precision("qty"))
se_child.allow_alternative_item = item_row.get("allow_alternative_item", 0)
se_child.subcontracted_item = item_row.get("main_item_code")
- se_child.cost_center = (item_row.get("cost_center") or
- get_default_cost_center(item_row, company = self.company))
+ se_child.cost_center = item_row.get("cost_center") or get_default_cost_center(
+ item_row, company=self.company
+ )
se_child.is_finished_item = item_row.get("is_finished_item", 0)
se_child.is_scrap_item = item_row.get("is_scrap_item", 0)
se_child.is_process_loss = item_row.get("is_process_loss", 0)
- for field in ["idx", "po_detail", "original_item", "expense_account",
- "description", "item_name", "serial_no", "batch_no", "allow_zero_valuation_rate"]:
+ for field in [
+ "po_detail",
+ "original_item",
+ "expense_account",
+ "description",
+ "item_name",
+ "serial_no",
+ "batch_no",
+ "allow_zero_valuation_rate",
+ ]:
if item_row.get(field):
se_child.set(field, item_row.get(field))
- if se_child.s_warehouse==None:
+ if se_child.s_warehouse == None:
se_child.s_warehouse = self.from_warehouse
- if se_child.t_warehouse==None:
+ if se_child.t_warehouse == None:
se_child.t_warehouse = self.to_warehouse
# in stock uom
se_child.conversion_factor = flt(item_row.get("conversion_factor")) or 1
- se_child.transfer_qty = flt(item_row["qty"]*se_child.conversion_factor, se_child.precision("qty"))
+ se_child.transfer_qty = flt(
+ item_row["qty"] * se_child.conversion_factor, se_child.precision("qty")
+ )
- se_child.bom_no = bom_no # to be assigned for finished item
+ se_child.bom_no = bom_no # to be assigned for finished item
se_child.job_card_item = item_row.get("job_card_item") if self.get("job_card") else None
def validate_with_material_request(self):
for item in self.get("items"):
material_request = item.material_request or None
material_request_item = item.material_request_item or None
- if self.purpose == 'Material Transfer' and self.outgoing_stock_entry:
- parent_se = frappe.get_value("Stock Entry Detail", item.ste_detail, ['material_request','material_request_item'],as_dict=True)
+ if self.purpose == "Material Transfer" and self.outgoing_stock_entry:
+ parent_se = frappe.get_value(
+ "Stock Entry Detail",
+ item.ste_detail,
+ ["material_request", "material_request_item"],
+ as_dict=True,
+ )
if parent_se:
material_request = parent_se.material_request
material_request_item = parent_se.material_request_item
if material_request:
- mreq_item = frappe.db.get_value("Material Request Item",
+ mreq_item = frappe.db.get_value(
+ "Material Request Item",
{"name": material_request_item, "parent": material_request},
- ["item_code", "warehouse", "idx"], as_dict=True)
+ ["item_code", "warehouse", "idx"],
+ as_dict=True,
+ )
if mreq_item.item_code != item.item_code:
- frappe.throw(_("Item for row {0} does not match Material Request").format(item.idx),
- frappe.MappingMismatchError)
+ frappe.throw(
+ _("Item for row {0} does not match Material Request").format(item.idx),
+ frappe.MappingMismatchError,
+ )
elif self.purpose == "Material Transfer" and self.add_to_transit:
continue
def validate_batch(self):
- if self.purpose in ["Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor"]:
+ if self.purpose in [
+ "Material Transfer for Manufacture",
+ "Manufacture",
+ "Repack",
+ "Send to Subcontractor",
+ ]:
for item in self.get("items"):
if item.batch_no:
disabled = frappe.db.get_value("Batch", item.batch_no, "disabled")
@@ -1648,30 +1972,34 @@ class StockEntry(StockController):
expiry_date = frappe.db.get_value("Batch", item.batch_no, "expiry_date")
if expiry_date:
if getdate(self.posting_date) > getdate(expiry_date):
- frappe.throw(_("Batch {0} of Item {1} has expired.")
- .format(item.batch_no, item.item_code))
+ frappe.throw(_("Batch {0} of Item {1} has expired.").format(item.batch_no, item.item_code))
else:
- frappe.throw(_("Batch {0} of Item {1} is disabled.")
- .format(item.batch_no, item.item_code))
+ frappe.throw(_("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code))
def update_purchase_order_supplied_items(self):
- if (self.purchase_order and
- (self.purpose in ['Send to Subcontractor', 'Material Transfer'] or self.is_return)):
+ if self.purchase_order and (
+ self.purpose in ["Send to Subcontractor", "Material Transfer"] or self.is_return
+ ):
- #Get PO Supplied Items Details
- item_wh = frappe._dict(frappe.db.sql("""
+ # Get PO Supplied Items Details
+ item_wh = frappe._dict(
+ frappe.db.sql(
+ """
select rm_item_code, reserve_warehouse
from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup
where po.name = poitemsup.parent
- and po.name = %s""", self.purchase_order))
+ and po.name = %s""",
+ self.purchase_order,
+ )
+ )
supplied_items = get_supplied_items(self.purchase_order)
for name, item in supplied_items.items():
- frappe.db.set_value('Purchase Order Item Supplied', name, item)
+ frappe.db.set_value("Purchase Order Item Supplied", name, item)
- #Update reserved sub contracted quantity in bin based on Supplied Item Details and
+ # Update reserved sub contracted quantity in bin based on Supplied Item Details and
for d in self.get("items"):
- item_code = d.get('original_item') or d.get('item_code')
+ item_code = d.get("original_item") or d.get("item_code")
reserve_warehouse = item_wh.get(item_code)
if not (reserve_warehouse and item_code):
continue
@@ -1679,12 +2007,17 @@ class StockEntry(StockController):
stock_bin.update_reserved_qty_for_sub_contracting()
def update_so_in_serial_number(self):
- so_name, item_code = frappe.db.get_value("Work Order", self.work_order, ["sales_order", "production_item"])
+ so_name, item_code = frappe.db.get_value(
+ "Work Order", self.work_order, ["sales_order", "production_item"]
+ )
if so_name and item_code:
qty_to_reserve = get_reserved_qty_for_so(so_name, item_code)
if qty_to_reserve:
- reserved_qty = frappe.db.sql("""select count(name) from `tabSerial No` where item_code=%s and
- sales_order=%s""", (item_code, so_name))
+ reserved_qty = frappe.db.sql(
+ """select count(name) from `tabSerial No` where item_code=%s and
+ sales_order=%s""",
+ (item_code, so_name),
+ )
if reserved_qty and reserved_qty[0][0]:
qty_to_reserve -= reserved_qty[0][0]
if qty_to_reserve > 0:
@@ -1695,7 +2028,7 @@ class StockEntry(StockController):
for serial_no in serial_nos:
if qty_to_reserve > 0:
frappe.db.set_value("Serial No", serial_no, "sales_order", so_name)
- qty_to_reserve -=1
+ qty_to_reserve -= 1
def validate_reserved_serial_no_consumption(self):
for item in self.items:
@@ -1703,13 +2036,14 @@ class StockEntry(StockController):
for sr in get_serial_nos(item.serial_no):
sales_order = frappe.db.get_value("Serial No", sr, "sales_order")
if sales_order:
- msg = (_("(Serial No: {0}) cannot be consumed as it's reserverd to fullfill Sales Order {1}.")
- .format(sr, sales_order))
+ msg = _(
+ "(Serial No: {0}) cannot be consumed as it's reserverd to fullfill Sales Order {1}."
+ ).format(sr, sales_order)
frappe.throw(_("Item {0} {1}").format(item.item_code, msg))
def update_transferred_qty(self):
- if self.purpose == 'Material Transfer' and self.outgoing_stock_entry:
+ if self.purpose == "Material Transfer" and self.outgoing_stock_entry:
stock_entries = {}
stock_entries_child_list = []
for d in self.items:
@@ -1717,70 +2051,87 @@ class StockEntry(StockController):
continue
stock_entries_child_list.append(d.ste_detail)
- transferred_qty = frappe.get_all("Stock Entry Detail", fields = ["sum(qty) as qty"],
- filters = { 'against_stock_entry': d.against_stock_entry,
- 'ste_detail': d.ste_detail,'docstatus': 1})
+ transferred_qty = frappe.get_all(
+ "Stock Entry Detail",
+ fields=["sum(qty) as qty"],
+ filters={
+ "against_stock_entry": d.against_stock_entry,
+ "ste_detail": d.ste_detail,
+ "docstatus": 1,
+ },
+ )
- stock_entries[(d.against_stock_entry, d.ste_detail)] = (transferred_qty[0].qty
- if transferred_qty and transferred_qty[0] else 0.0) or 0.0
+ stock_entries[(d.against_stock_entry, d.ste_detail)] = (
+ transferred_qty[0].qty if transferred_qty and transferred_qty[0] else 0.0
+ ) or 0.0
- if not stock_entries: return None
+ if not stock_entries:
+ return None
- cond = ''
+ cond = ""
for data, transferred_qty in stock_entries.items():
cond += """ WHEN (parent = %s and name = %s) THEN %s
- """ %(frappe.db.escape(data[0]), frappe.db.escape(data[1]), transferred_qty)
+ """ % (
+ frappe.db.escape(data[0]),
+ frappe.db.escape(data[1]),
+ transferred_qty,
+ )
if stock_entries_child_list:
- frappe.db.sql(""" UPDATE `tabStock Entry Detail`
+ frappe.db.sql(
+ """ UPDATE `tabStock Entry Detail`
SET
transferred_qty = CASE {cond} END
WHERE
- name in ({ste_details}) """.format(cond=cond,
- ste_details = ','.join(['%s'] * len(stock_entries_child_list))),
- tuple(stock_entries_child_list))
+ name in ({ste_details}) """.format(
+ cond=cond, ste_details=",".join(["%s"] * len(stock_entries_child_list))
+ ),
+ tuple(stock_entries_child_list),
+ )
args = {
- 'source_dt': 'Stock Entry Detail',
- 'target_field': 'transferred_qty',
- 'target_ref_field': 'qty',
- 'target_dt': 'Stock Entry Detail',
- 'join_field': 'ste_detail',
- 'target_parent_dt': 'Stock Entry',
- 'target_parent_field': 'per_transferred',
- 'source_field': 'qty',
- 'percent_join_field': 'against_stock_entry'
+ "source_dt": "Stock Entry Detail",
+ "target_field": "transferred_qty",
+ "target_ref_field": "qty",
+ "target_dt": "Stock Entry Detail",
+ "join_field": "ste_detail",
+ "target_parent_dt": "Stock Entry",
+ "target_parent_field": "per_transferred",
+ "source_field": "qty",
+ "percent_join_field": "against_stock_entry",
}
self._update_percent_field_in_targets(args, update_modified=True)
def update_quality_inspection(self):
if self.inspection_required:
- reference_type = reference_name = ''
+ reference_type = reference_name = ""
if self.docstatus == 1:
reference_name = self.name
- reference_type = 'Stock Entry'
+ reference_type = "Stock Entry"
for d in self.items:
if d.quality_inspection:
- frappe.db.set_value("Quality Inspection", d.quality_inspection, {
- 'reference_type': reference_type,
- 'reference_name': reference_name
- })
+ frappe.db.set_value(
+ "Quality Inspection",
+ d.quality_inspection,
+ {"reference_type": reference_type, "reference_name": reference_name},
+ )
+
def set_material_request_transfer_status(self, status):
material_requests = []
if self.outgoing_stock_entry:
- parent_se = frappe.get_value("Stock Entry", self.outgoing_stock_entry, 'add_to_transit')
+ parent_se = frappe.get_value("Stock Entry", self.outgoing_stock_entry, "add_to_transit")
for item in self.items:
material_request = item.material_request or None
if self.purpose == "Material Transfer" and material_request not in material_requests:
if self.outgoing_stock_entry and parent_se:
- material_request = frappe.get_value("Stock Entry Detail", item.ste_detail, 'material_request')
+ material_request = frappe.get_value("Stock Entry Detail", item.ste_detail, "material_request")
if material_request and material_request not in material_requests:
material_requests.append(material_request)
- frappe.db.set_value('Material Request', material_request, 'transfer_status', status)
+ frappe.db.set_value("Material Request", material_request, "transfer_status", status)
def update_items_for_process_loss(self):
process_loss_dict = {}
@@ -1788,7 +2139,9 @@ class StockEntry(StockController):
if not d.is_process_loss:
continue
- scrap_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_scrap_warehouse")
+ scrap_warehouse = frappe.db.get_single_value(
+ "Manufacturing Settings", "default_scrap_warehouse"
+ )
if scrap_warehouse is not None:
d.t_warehouse = scrap_warehouse
d.is_scrap_item = 0
@@ -1805,7 +2158,6 @@ class StockEntry(StockController):
d.transfer_qty -= process_loss_dict[d.item_code][0]
d.qty -= process_loss_dict[d.item_code][1]
-
def set_serial_no_batch_for_finished_good(self):
args = {}
if self.pro_doc.serial_no:
@@ -1814,14 +2166,22 @@ class StockEntry(StockController):
for row in self.items:
if row.is_finished_item and row.item_code == self.pro_doc.production_item:
if args.get("serial_no"):
- row.serial_no = '\n'.join(args["serial_no"][0: cint(row.qty)])
+ row.serial_no = "\n".join(args["serial_no"][0 : cint(row.qty)])
def get_serial_nos_for_fg(self, args):
- fields = ["`tabStock Entry`.`name`", "`tabStock Entry Detail`.`qty`",
- "`tabStock Entry Detail`.`serial_no`", "`tabStock Entry Detail`.`batch_no`"]
+ fields = [
+ "`tabStock Entry`.`name`",
+ "`tabStock Entry Detail`.`qty`",
+ "`tabStock Entry Detail`.`serial_no`",
+ "`tabStock Entry Detail`.`batch_no`",
+ ]
- filters = [["Stock Entry","work_order","=",self.work_order], ["Stock Entry","purpose","=","Manufacture"],
- ["Stock Entry","docstatus","=",1], ["Stock Entry Detail","item_code","=",self.pro_doc.production_item]]
+ filters = [
+ ["Stock Entry", "work_order", "=", self.work_order],
+ ["Stock Entry", "purpose", "=", "Manufacture"],
+ ["Stock Entry", "docstatus", "=", 1],
+ ["Stock Entry Detail", "item_code", "=", self.pro_doc.production_item],
+ ]
stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters)
@@ -1836,85 +2196,98 @@ class StockEntry(StockController):
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
+
@frappe.whitelist()
def move_sample_to_retention_warehouse(company, items):
if isinstance(items, string_types):
items = json.loads(items)
- retention_warehouse = frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse')
+ retention_warehouse = frappe.db.get_single_value("Stock Settings", "sample_retention_warehouse")
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.company = company
stock_entry.purpose = "Material Transfer"
stock_entry.set_stock_entry_type()
for item in items:
- if item.get('sample_quantity') and item.get('batch_no'):
- sample_quantity = validate_sample_quantity(item.get('item_code'), item.get('sample_quantity'),
- item.get('transfer_qty') or item.get('qty'), item.get('batch_no'))
+ if item.get("sample_quantity") and item.get("batch_no"):
+ sample_quantity = validate_sample_quantity(
+ item.get("item_code"),
+ item.get("sample_quantity"),
+ item.get("transfer_qty") or item.get("qty"),
+ item.get("batch_no"),
+ )
if sample_quantity:
- sample_serial_nos = ''
- if item.get('serial_no'):
- serial_nos = (item.get('serial_no')).split()
- if serial_nos and len(serial_nos) > item.get('sample_quantity'):
- serial_no_list = serial_nos[:-(len(serial_nos)-item.get('sample_quantity'))]
- sample_serial_nos = '\n'.join(serial_no_list)
+ sample_serial_nos = ""
+ if item.get("serial_no"):
+ serial_nos = (item.get("serial_no")).split()
+ if serial_nos and len(serial_nos) > item.get("sample_quantity"):
+ serial_no_list = serial_nos[: -(len(serial_nos) - item.get("sample_quantity"))]
+ sample_serial_nos = "\n".join(serial_no_list)
- stock_entry.append("items", {
- "item_code": item.get('item_code'),
- "s_warehouse": item.get('t_warehouse'),
- "t_warehouse": retention_warehouse,
- "qty": item.get('sample_quantity'),
- "basic_rate": item.get('valuation_rate'),
- 'uom': item.get('uom'),
- 'stock_uom': item.get('stock_uom'),
- "conversion_factor": 1.0,
- "serial_no": sample_serial_nos,
- 'batch_no': item.get('batch_no')
- })
- if stock_entry.get('items'):
+ stock_entry.append(
+ "items",
+ {
+ "item_code": item.get("item_code"),
+ "s_warehouse": item.get("t_warehouse"),
+ "t_warehouse": retention_warehouse,
+ "qty": item.get("sample_quantity"),
+ "basic_rate": item.get("valuation_rate"),
+ "uom": item.get("uom"),
+ "stock_uom": item.get("stock_uom"),
+ "conversion_factor": 1.0,
+ "serial_no": sample_serial_nos,
+ "batch_no": item.get("batch_no"),
+ },
+ )
+ if stock_entry.get("items"):
return stock_entry.as_dict()
+
@frappe.whitelist()
def make_stock_in_entry(source_name, target_doc=None):
-
def set_missing_values(source, target):
target.set_stock_entry_type()
def update_item(source_doc, target_doc, source_parent):
- target_doc.t_warehouse = ''
+ target_doc.t_warehouse = ""
- if source_doc.material_request_item and source_doc.material_request :
- add_to_transit = frappe.db.get_value('Stock Entry', source_name, 'add_to_transit')
+ if source_doc.material_request_item and source_doc.material_request:
+ add_to_transit = frappe.db.get_value("Stock Entry", source_name, "add_to_transit")
if add_to_transit:
- warehouse = frappe.get_value('Material Request Item', source_doc.material_request_item, 'warehouse')
+ warehouse = frappe.get_value(
+ "Material Request Item", source_doc.material_request_item, "warehouse"
+ )
target_doc.t_warehouse = warehouse
target_doc.s_warehouse = source_doc.t_warehouse
target_doc.qty = source_doc.qty - source_doc.transferred_qty
- doclist = get_mapped_doc("Stock Entry", source_name, {
- "Stock Entry": {
- "doctype": "Stock Entry",
- "field_map": {
- "name": "outgoing_stock_entry"
+ doclist = get_mapped_doc(
+ "Stock Entry",
+ source_name,
+ {
+ "Stock Entry": {
+ "doctype": "Stock Entry",
+ "field_map": {"name": "outgoing_stock_entry"},
+ "validation": {"docstatus": ["=", 1]},
},
- "validation": {
- "docstatus": ["=", 1]
- }
- },
- "Stock Entry Detail": {
- "doctype": "Stock Entry Detail",
- "field_map": {
- "name": "ste_detail",
- "parent": "against_stock_entry",
- "serial_no": "serial_no",
- "batch_no": "batch_no"
+ "Stock Entry Detail": {
+ "doctype": "Stock Entry Detail",
+ "field_map": {
+ "name": "ste_detail",
+ "parent": "against_stock_entry",
+ "serial_no": "serial_no",
+ "batch_no": "batch_no",
+ },
+ "postprocess": update_item,
+ "condition": lambda doc: flt(doc.qty) - flt(doc.transferred_qty) > 0.01,
},
- "postprocess": update_item,
- "condition": lambda doc: flt(doc.qty) - flt(doc.transferred_qty) > 0.01
},
- }, target_doc, set_missing_values)
+ target_doc,
+ set_missing_values,
+ )
return doclist
+
@frappe.whitelist()
def get_work_order_details(work_order, company):
work_order = frappe.get_doc("Work Order", work_order)
@@ -1926,9 +2299,10 @@ def get_work_order_details(work_order, company):
"use_multi_level_bom": work_order.use_multi_level_bom,
"wip_warehouse": work_order.wip_warehouse,
"fg_warehouse": work_order.fg_warehouse,
- "fg_completed_qty": pending_qty_to_produce
+ "fg_completed_qty": pending_qty_to_produce,
}
+
def get_operating_cost_per_unit(work_order=None, bom_no=None):
operating_cost_per_unit = 0
if work_order:
@@ -1947,54 +2321,78 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
if bom.quantity:
operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity)
- if work_order and work_order.produced_qty and cint(frappe.db.get_single_value('Manufacturing Settings',
- 'add_corrective_operation_cost_in_finished_good_valuation')):
- operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty)
+ if (
+ work_order
+ and work_order.produced_qty
+ and cint(
+ frappe.db.get_single_value(
+ "Manufacturing Settings", "add_corrective_operation_cost_in_finished_good_valuation"
+ )
+ )
+ ):
+ operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(
+ work_order.produced_qty
+ )
return operating_cost_per_unit
+
def get_used_alternative_items(purchase_order=None, work_order=None):
cond = ""
if purchase_order:
- cond = "and ste.purpose = 'Send to Subcontractor' and ste.purchase_order = '{0}'".format(purchase_order)
+ cond = "and ste.purpose = 'Send to Subcontractor' and ste.purchase_order = '{0}'".format(
+ purchase_order
+ )
elif work_order:
- cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format(work_order)
+ cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format(
+ work_order
+ )
- if not cond: return {}
+ if not cond:
+ return {}
used_alternative_items = {}
- data = frappe.db.sql(""" select sted.original_item, sted.uom, sted.conversion_factor,
+ data = frappe.db.sql(
+ """ select sted.original_item, sted.uom, sted.conversion_factor,
sted.item_code, sted.item_name, sted.conversion_factor,sted.stock_uom, sted.description
from
`tabStock Entry` ste, `tabStock Entry Detail` sted
where
sted.parent = ste.name and ste.docstatus = 1 and sted.original_item != sted.item_code
- {0} """.format(cond), as_dict=1)
+ {0} """.format(
+ cond
+ ),
+ as_dict=1,
+ )
for d in data:
used_alternative_items[d.original_item] = d
return used_alternative_items
+
def get_valuation_rate_for_finished_good_entry(work_order):
- work_order_qty = flt(frappe.get_cached_value("Work Order",
- work_order, 'material_transferred_for_manufacturing'))
+ work_order_qty = flt(
+ frappe.get_cached_value("Work Order", work_order, "material_transferred_for_manufacturing")
+ )
field = "(SUM(total_outgoing_value) / %s) as valuation_rate" % (work_order_qty)
- stock_data = frappe.get_all("Stock Entry",
- fields = field,
- filters = {
+ stock_data = frappe.get_all(
+ "Stock Entry",
+ fields=field,
+ filters={
"docstatus": 1,
"purpose": "Material Transfer for Manufacture",
- "work_order": work_order
- }
+ "work_order": work_order,
+ },
)
if stock_data:
return stock_data[0].valuation_rate
+
@frappe.whitelist()
def get_uom_details(item_code, uom, qty):
"""Returns dict `{"conversion_factor": [value], "transfer_qty": qty * [value]}`
@@ -2003,24 +2401,31 @@ def get_uom_details(item_code, uom, qty):
conversion_factor = get_conversion_factor(item_code, uom).get("conversion_factor")
if not conversion_factor:
- frappe.msgprint(_("UOM coversion factor required for UOM: {0} in Item: {1}")
- .format(uom, item_code))
- ret = {'uom' : ''}
+ frappe.msgprint(
+ _("UOM coversion factor required for UOM: {0} in Item: {1}").format(uom, item_code)
+ )
+ ret = {"uom": ""}
else:
ret = {
- 'conversion_factor' : flt(conversion_factor),
- 'transfer_qty' : flt(qty) * flt(conversion_factor)
+ "conversion_factor": flt(conversion_factor),
+ "transfer_qty": flt(qty) * flt(conversion_factor),
}
return ret
+
@frappe.whitelist()
def get_expired_batch_items():
- return frappe.db.sql("""select b.item, sum(sle.actual_qty) as qty, sle.batch_no, sle.warehouse, sle.stock_uom\
+ return frappe.db.sql(
+ """select b.item, sum(sle.actual_qty) as qty, sle.batch_no, sle.warehouse, sle.stock_uom\
from `tabBatch` b, `tabStock Ledger Entry` sle
where b.expiry_date <= %s
and b.expiry_date is not NULL
and b.batch_id = sle.batch_no and sle.is_cancelled = 0
- group by sle.warehouse, sle.item_code, sle.batch_no""",(nowdate()), as_dict=1)
+ group by sle.warehouse, sle.item_code, sle.batch_no""",
+ (nowdate()),
+ as_dict=1,
+ )
+
@frappe.whitelist()
def get_warehouse_details(args):
@@ -2031,51 +2436,73 @@ def get_warehouse_details(args):
ret = {}
if args.warehouse and args.item_code:
- args.update({
- "posting_date": args.posting_date,
- "posting_time": args.posting_time,
- })
+ args.update(
+ {
+ "posting_date": args.posting_date,
+ "posting_time": args.posting_time,
+ }
+ )
ret = {
- "actual_qty" : get_previous_sle(args).get("qty_after_transaction") or 0,
- "basic_rate" : get_incoming_rate(args)
+ "actual_qty": get_previous_sle(args).get("qty_after_transaction") or 0,
+ "basic_rate": get_incoming_rate(args),
}
return ret
+
@frappe.whitelist()
-def validate_sample_quantity(item_code, sample_quantity, qty, batch_no = None):
+def validate_sample_quantity(item_code, sample_quantity, qty, batch_no=None):
if cint(qty) < cint(sample_quantity):
- frappe.throw(_("Sample quantity {0} cannot be more than received quantity {1}").format(sample_quantity, qty))
- retention_warehouse = frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse')
+ frappe.throw(
+ _("Sample quantity {0} cannot be more than received quantity {1}").format(sample_quantity, qty)
+ )
+ retention_warehouse = frappe.db.get_single_value("Stock Settings", "sample_retention_warehouse")
retainted_qty = 0
if batch_no:
retainted_qty = get_batch_qty(batch_no, retention_warehouse, item_code)
- max_retain_qty = frappe.get_value('Item', item_code, 'sample_quantity')
+ max_retain_qty = frappe.get_value("Item", item_code, "sample_quantity")
if retainted_qty >= max_retain_qty:
- frappe.msgprint(_("Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}.").
- format(retainted_qty, batch_no, item_code, batch_no), alert=True)
+ frappe.msgprint(
+ _(
+ "Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}."
+ ).format(retainted_qty, batch_no, item_code, batch_no),
+ alert=True,
+ )
sample_quantity = 0
- qty_diff = max_retain_qty-retainted_qty
+ qty_diff = max_retain_qty - retainted_qty
if cint(sample_quantity) > cint(qty_diff):
- frappe.msgprint(_("Maximum Samples - {0} can be retained for Batch {1} and Item {2}.").
- format(max_retain_qty, batch_no, item_code), alert=True)
+ frappe.msgprint(
+ _("Maximum Samples - {0} can be retained for Batch {1} and Item {2}.").format(
+ max_retain_qty, batch_no, item_code
+ ),
+ alert=True,
+ )
sample_quantity = qty_diff
return sample_quantity
-def get_supplied_items(purchase_order):
- fields = ['`tabStock Entry Detail`.`transfer_qty`', '`tabStock Entry`.`is_return`',
- '`tabStock Entry Detail`.`po_detail`', '`tabStock Entry Detail`.`item_code`']
- filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purchase_order', '=', purchase_order]]
+def get_supplied_items(purchase_order):
+ fields = [
+ "`tabStock Entry Detail`.`transfer_qty`",
+ "`tabStock Entry`.`is_return`",
+ "`tabStock Entry Detail`.`po_detail`",
+ "`tabStock Entry Detail`.`item_code`",
+ ]
+
+ filters = [
+ ["Stock Entry", "docstatus", "=", 1],
+ ["Stock Entry", "purchase_order", "=", purchase_order],
+ ]
supplied_item_details = {}
- for row in frappe.get_all('Stock Entry', fields = fields, filters = filters):
+ for row in frappe.get_all("Stock Entry", fields=fields, filters=filters):
if not row.po_detail:
continue
key = row.po_detail
if key not in supplied_item_details:
- supplied_item_details.setdefault(key,
- frappe._dict({'supplied_qty': 0, 'returned_qty':0, 'total_supplied_qty':0}))
+ supplied_item_details.setdefault(
+ key, frappe._dict({"supplied_qty": 0, "returned_qty": 0, "total_supplied_qty": 0})
+ )
supplied_item = supplied_item_details[key]
@@ -2084,6 +2511,8 @@ def get_supplied_items(purchase_order):
else:
supplied_item.supplied_qty += row.transfer_qty
- supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(supplied_item.returned_qty)
+ supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(
+ supplied_item.returned_qty
+ )
return supplied_item_details
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
index 5832fe49b2d..85ccc5b13fc 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
@@ -11,7 +11,7 @@ import erpnext
@frappe.whitelist()
def make_stock_entry(**args):
- '''Helper function to make a Stock Entry
+ """Helper function to make a Stock Entry
:item_code: Item to be moved
:qty: Qty to be moved
@@ -26,16 +26,16 @@ def make_stock_entry(**args):
:purpose: Optional
:do_not_save: Optional flag
:do_not_submit: Optional flag
- '''
+ """
def process_serial_numbers(serial_nos_list):
serial_nos_list = [
- '\n'.join(serial_num['serial_no'] for serial_num in serial_nos_list if serial_num.serial_no)
+ "\n".join(serial_num["serial_no"] for serial_num in serial_nos_list if serial_num.serial_no)
]
- uniques = list(set(serial_nos_list[0].split('\n')))
+ uniques = list(set(serial_nos_list[0].split("\n")))
- return '\n'.join(uniques)
+ return "\n".join(uniques)
s = frappe.new_doc("Stock Entry")
args = frappe._dict(args)
@@ -61,7 +61,7 @@ def make_stock_entry(**args):
s.apply_putaway_rule = args.apply_putaway_rule
if isinstance(args.qty, string_types):
- if '.' in args.qty:
+ if "." in args.qty:
args.qty = flt(args.qty)
else:
args.qty = cint(args.qty)
@@ -80,16 +80,16 @@ def make_stock_entry(**args):
# company
if not args.company:
if args.source:
- args.company = frappe.db.get_value('Warehouse', args.source, 'company')
+ args.company = frappe.db.get_value("Warehouse", args.source, "company")
elif args.target:
- args.company = frappe.db.get_value('Warehouse', args.target, 'company')
+ args.company = frappe.db.get_value("Warehouse", args.target, "company")
# set vales from test
if frappe.flags.in_test:
if not args.company:
- args.company = '_Test Company'
+ args.company = "_Test Company"
if not args.item:
- args.item = '_Test Item'
+ args.item = "_Test Item"
s.company = args.company or erpnext.get_default_company()
s.purchase_receipt_no = args.purchase_receipt_no
@@ -97,40 +97,40 @@ def make_stock_entry(**args):
s.sales_invoice_no = args.sales_invoice_no
s.is_opening = args.is_opening or "No"
if not args.cost_center:
- args.cost_center = frappe.get_value('Company', s.company, 'cost_center')
+ args.cost_center = frappe.get_value("Company", s.company, "cost_center")
if not args.expense_account and s.is_opening == "No":
- args.expense_account = frappe.get_value('Company', s.company, 'stock_adjustment_account')
+ args.expense_account = frappe.get_value("Company", s.company, "stock_adjustment_account")
# We can find out the serial number using the batch source document
serial_number = args.serial_no
if not args.serial_no and args.qty and args.batch_no:
serial_number_list = frappe.get_list(
- doctype='Stock Ledger Entry',
- fields=['serial_no'],
- filters={
- 'batch_no': args.batch_no,
- 'warehouse': args.from_warehouse
- }
+ doctype="Stock Ledger Entry",
+ fields=["serial_no"],
+ filters={"batch_no": args.batch_no, "warehouse": args.from_warehouse},
)
serial_number = process_serial_numbers(serial_number_list)
args.serial_no = serial_number
- s.append("items", {
- "item_code": args.item,
- "s_warehouse": args.source,
- "t_warehouse": args.target,
- "qty": args.qty,
- "basic_rate": args.rate or args.basic_rate,
- "conversion_factor": args.conversion_factor or 1.0,
- "transfer_qty": flt(args.qty) * (flt(args.conversion_factor) or 1.0),
- "serial_no": args.serial_no,
- 'batch_no': args.batch_no,
- 'cost_center': args.cost_center,
- 'expense_account': args.expense_account
- })
+ s.append(
+ "items",
+ {
+ "item_code": args.item,
+ "s_warehouse": args.source,
+ "t_warehouse": args.target,
+ "qty": args.qty,
+ "basic_rate": args.rate or args.basic_rate,
+ "conversion_factor": args.conversion_factor or 1.0,
+ "transfer_qty": flt(args.qty) * (flt(args.conversion_factor) or 1.0),
+ "serial_no": args.serial_no,
+ "batch_no": args.batch_no,
+ "cost_center": args.cost_center,
+ "expense_account": args.expense_account,
+ },
+ )
s.set_stock_entry_type()
if not args.do_not_save:
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 3c34d4795cb..323ab6af819 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -6,6 +6,7 @@ import unittest
import frappe
from frappe.permissions import add_user_permission, remove_user_permission
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import flt, nowdate, nowtime
from six import iteritems
@@ -29,7 +30,6 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
create_stock_reconciliation,
)
from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle
-from erpnext.tests.utils import ERPNextTestCase, change_settings
def get_sle(**args):
@@ -39,51 +39,56 @@ def get_sle(**args):
condition += "`{0}`=%s".format(key)
values.append(value)
- return frappe.db.sql("""select * from `tabStock Ledger Entry` %s
- order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition,
- values, as_dict=1)
+ return frappe.db.sql(
+ """select * from `tabStock Ledger Entry` %s
+ order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""
+ % condition,
+ values,
+ as_dict=1,
+ )
-class TestStockEntry(ERPNextTestCase):
+
+class TestStockEntry(FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
frappe.set_user("Administrator")
- frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
def test_fifo(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
item_code = "_Test Item 2"
warehouse = "_Test Warehouse - _TC"
- create_stock_reconciliation(item_code="_Test Item 2", warehouse="_Test Warehouse - _TC",
- qty=0, rate=100)
+ create_stock_reconciliation(
+ item_code="_Test Item 2", warehouse="_Test Warehouse - _TC", qty=0, rate=100
+ )
make_stock_entry(item_code=item_code, target=warehouse, qty=1, basic_rate=10)
- sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
+ sle = get_sle(item_code=item_code, warehouse=warehouse)[0]
self.assertEqual([[1, 10]], frappe.safe_eval(sle.stock_queue))
# negative qty
make_stock_entry(item_code=item_code, source=warehouse, qty=2, basic_rate=10)
- sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
+ sle = get_sle(item_code=item_code, warehouse=warehouse)[0]
self.assertEqual([[-1, 10]], frappe.safe_eval(sle.stock_queue))
# further negative
make_stock_entry(item_code=item_code, source=warehouse, qty=1)
- sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
+ sle = get_sle(item_code=item_code, warehouse=warehouse)[0]
self.assertEqual([[-2, 10]], frappe.safe_eval(sle.stock_queue))
# move stock to positive
make_stock_entry(item_code=item_code, target=warehouse, qty=3, basic_rate=20)
- sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
+ sle = get_sle(item_code=item_code, warehouse=warehouse)[0]
self.assertEqual([[1, 20]], frappe.safe_eval(sle.stock_queue))
# incoming entry with diff rate
make_stock_entry(item_code=item_code, target=warehouse, qty=1, basic_rate=30)
- sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
+ sle = get_sle(item_code=item_code, warehouse=warehouse)[0]
- self.assertEqual([[1, 20],[1, 30]], frappe.safe_eval(sle.stock_queue))
+ self.assertEqual([[1, 20], [1, 30]], frappe.safe_eval(sle.stock_queue))
frappe.db.set_default("allow_negative_stock", 0)
@@ -93,37 +98,48 @@ class TestStockEntry(ERPNextTestCase):
self._test_auto_material_request("_Test Item", material_request_type="Transfer")
def test_auto_material_request_for_variant(self):
- fields = [{'field_name': 'reorder_levels'}]
+ fields = [{"field_name": "reorder_levels"}]
set_item_variant_settings(fields)
make_item_variant()
template = frappe.get_doc("Item", "_Test Variant Item")
if not template.reorder_levels:
- template.append('reorder_levels', {
- "material_request_type": "Purchase",
- "warehouse": "_Test Warehouse - _TC",
- "warehouse_reorder_level": 20,
- "warehouse_reorder_qty": 20
- })
+ template.append(
+ "reorder_levels",
+ {
+ "material_request_type": "Purchase",
+ "warehouse": "_Test Warehouse - _TC",
+ "warehouse_reorder_level": 20,
+ "warehouse_reorder_qty": 20,
+ },
+ )
template.save()
self._test_auto_material_request("_Test Variant Item-S")
def test_auto_material_request_for_warehouse_group(self):
- self._test_auto_material_request("_Test Item Warehouse Group Wise Reorder", warehouse="_Test Warehouse Group-C1 - _TC")
+ self._test_auto_material_request(
+ "_Test Item Warehouse Group Wise Reorder", warehouse="_Test Warehouse Group-C1 - _TC"
+ )
- def _test_auto_material_request(self, item_code, material_request_type="Purchase", warehouse="_Test Warehouse - _TC"):
+ def _test_auto_material_request(
+ self, item_code, material_request_type="Purchase", warehouse="_Test Warehouse - _TC"
+ ):
variant = frappe.get_doc("Item", item_code)
- projected_qty, actual_qty = frappe.db.get_value("Bin", {"item_code": item_code,
- "warehouse": warehouse}, ["projected_qty", "actual_qty"]) or [0, 0]
+ projected_qty, actual_qty = frappe.db.get_value(
+ "Bin", {"item_code": item_code, "warehouse": warehouse}, ["projected_qty", "actual_qty"]
+ ) or [0, 0]
# stock entry reqd for auto-reorder
- create_stock_reconciliation(item_code=item_code, warehouse=warehouse,
- qty = actual_qty + abs(projected_qty) + 10, rate=100)
+ create_stock_reconciliation(
+ item_code=item_code, warehouse=warehouse, qty=actual_qty + abs(projected_qty) + 10, rate=100
+ )
- projected_qty = frappe.db.get_value("Bin", {"item_code": item_code,
- "warehouse": warehouse}, "projected_qty") or 0
+ projected_qty = (
+ frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "projected_qty")
+ or 0
+ )
frappe.db.set_value("Stock Settings", None, "auto_indent", 1)
@@ -134,6 +150,7 @@ class TestStockEntry(ERPNextTestCase):
variant.save()
from erpnext.stock.reorder_item import reorder_item
+
mr_list = reorder_item()
frappe.db.set_value("Stock Settings", None, "auto_indent", 0)
@@ -146,65 +163,113 @@ class TestStockEntry(ERPNextTestCase):
self.assertTrue(item_code in items)
def test_material_receipt_gl_entry(self):
- company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
+ company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
- mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company= company,
- qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1")
+ mr = make_stock_entry(
+ item_code="_Test Item",
+ target="Stores - TCP1",
+ company=company,
+ qty=50,
+ basic_rate=100,
+ expense_account="Stock Adjustment - TCP1",
+ )
stock_in_hand_account = get_inventory_account(mr.company, mr.get("items")[0].t_warehouse)
- self.check_stock_ledger_entries("Stock Entry", mr.name,
- [["_Test Item", "Stores - TCP1", 50.0]])
+ self.check_stock_ledger_entries("Stock Entry", mr.name, [["_Test Item", "Stores - TCP1", 50.0]])
- self.check_gl_entries("Stock Entry", mr.name,
- sorted([
- [stock_in_hand_account, 5000.0, 0.0],
- ["Stock Adjustment - TCP1", 0.0, 5000.0]
- ])
+ self.check_gl_entries(
+ "Stock Entry",
+ mr.name,
+ sorted([[stock_in_hand_account, 5000.0, 0.0], ["Stock Adjustment - TCP1", 0.0, 5000.0]]),
)
mr.cancel()
- self.assertTrue(frappe.db.sql("""select * from `tabStock Ledger Entry`
- where voucher_type='Stock Entry' and voucher_no=%s""", mr.name))
+ self.assertTrue(
+ frappe.db.sql(
+ """select * from `tabStock Ledger Entry`
+ where voucher_type='Stock Entry' and voucher_no=%s""",
+ mr.name,
+ )
+ )
- self.assertTrue(frappe.db.sql("""select * from `tabGL Entry`
- where voucher_type='Stock Entry' and voucher_no=%s""", mr.name))
+ self.assertTrue(
+ frappe.db.sql(
+ """select * from `tabGL Entry`
+ where voucher_type='Stock Entry' and voucher_no=%s""",
+ mr.name,
+ )
+ )
def test_material_issue_gl_entry(self):
- company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
- make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company= company,
- qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1")
+ company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
+ make_stock_entry(
+ item_code="_Test Item",
+ target="Stores - TCP1",
+ company=company,
+ qty=50,
+ basic_rate=100,
+ expense_account="Stock Adjustment - TCP1",
+ )
- mi = make_stock_entry(item_code="_Test Item", source="Stores - TCP1", company=company,
- qty=40, expense_account="Stock Adjustment - TCP1")
+ mi = make_stock_entry(
+ item_code="_Test Item",
+ source="Stores - TCP1",
+ company=company,
+ qty=40,
+ expense_account="Stock Adjustment - TCP1",
+ )
- self.check_stock_ledger_entries("Stock Entry", mi.name,
- [["_Test Item", "Stores - TCP1", -40.0]])
+ self.check_stock_ledger_entries("Stock Entry", mi.name, [["_Test Item", "Stores - TCP1", -40.0]])
stock_in_hand_account = get_inventory_account(mi.company, "Stores - TCP1")
- stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry",
- "voucher_no": mi.name}, "stock_value_difference"))
+ stock_value_diff = abs(
+ frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Stock Entry", "voucher_no": mi.name},
+ "stock_value_difference",
+ )
+ )
- self.check_gl_entries("Stock Entry", mi.name,
- sorted([
- [stock_in_hand_account, 0.0, stock_value_diff],
- ["Stock Adjustment - TCP1", stock_value_diff, 0.0]
- ])
+ self.check_gl_entries(
+ "Stock Entry",
+ mi.name,
+ sorted(
+ [
+ [stock_in_hand_account, 0.0, stock_value_diff],
+ ["Stock Adjustment - TCP1", stock_value_diff, 0.0],
+ ]
+ ),
)
mi.cancel()
def test_material_transfer_gl_entry(self):
- company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
+ company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
- item_code = 'Hand Sanitizer - 001'
- create_item(item_code =item_code, is_stock_item = 1,
- is_purchase_item=1, opening_stock=1000, valuation_rate=10, company=company, warehouse="Stores - TCP1")
+ item_code = "Hand Sanitizer - 001"
+ create_item(
+ item_code=item_code,
+ is_stock_item=1,
+ is_purchase_item=1,
+ opening_stock=1000,
+ valuation_rate=10,
+ company=company,
+ warehouse="Stores - TCP1",
+ )
- mtn = make_stock_entry(item_code=item_code, source="Stores - TCP1",
- target="Finished Goods - TCP1", qty=45, company=company)
+ mtn = make_stock_entry(
+ item_code=item_code,
+ source="Stores - TCP1",
+ target="Finished Goods - TCP1",
+ qty=45,
+ company=company,
+ )
- self.check_stock_ledger_entries("Stock Entry", mtn.name,
- [[item_code, "Stores - TCP1", -45.0], [item_code, "Finished Goods - TCP1", 45.0]])
+ self.check_stock_ledger_entries(
+ "Stock Entry",
+ mtn.name,
+ [[item_code, "Stores - TCP1", -45.0], [item_code, "Finished Goods - TCP1", 45.0]],
+ )
source_warehouse_account = get_inventory_account(mtn.company, mtn.get("items")[0].s_warehouse)
@@ -212,18 +277,33 @@ class TestStockEntry(ERPNextTestCase):
if source_warehouse_account == target_warehouse_account:
# no gl entry as both source and target warehouse has linked to same account.
- self.assertFalse(frappe.db.sql("""select * from `tabGL Entry`
- where voucher_type='Stock Entry' and voucher_no=%s""", mtn.name, as_dict=1))
+ self.assertFalse(
+ frappe.db.sql(
+ """select * from `tabGL Entry`
+ where voucher_type='Stock Entry' and voucher_no=%s""",
+ mtn.name,
+ as_dict=1,
+ )
+ )
else:
- stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry",
- "voucher_no": mtn.name, "warehouse": "Stores - TCP1"}, "stock_value_difference"))
+ stock_value_diff = abs(
+ frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Stock Entry", "voucher_no": mtn.name, "warehouse": "Stores - TCP1"},
+ "stock_value_difference",
+ )
+ )
- self.check_gl_entries("Stock Entry", mtn.name,
- sorted([
- [source_warehouse_account, 0.0, stock_value_diff],
- [target_warehouse_account, stock_value_diff, 0.0],
- ])
+ self.check_gl_entries(
+ "Stock Entry",
+ mtn.name,
+ sorted(
+ [
+ [source_warehouse_account, 0.0, stock_value_diff],
+ [target_warehouse_account, stock_value_diff, 0.0],
+ ]
+ ),
)
mtn.cancel()
@@ -240,20 +320,23 @@ class TestStockEntry(ERPNextTestCase):
repack.items[0].transfer_qty = 100.0
repack.items[1].qty = 50.0
- repack.append("items", {
- "conversion_factor": 1.0,
- "cost_center": "_Test Cost Center - _TC",
- "doctype": "Stock Entry Detail",
- "expense_account": "Stock Adjustment - _TC",
- "basic_rate": 150,
- "item_code": "_Test Item 2",
- "parentfield": "items",
- "qty": 50.0,
- "stock_uom": "_Test UOM",
- "t_warehouse": "_Test Warehouse - _TC",
- "transfer_qty": 50.0,
- "uom": "_Test UOM"
- })
+ repack.append(
+ "items",
+ {
+ "conversion_factor": 1.0,
+ "cost_center": "_Test Cost Center - _TC",
+ "doctype": "Stock Entry Detail",
+ "expense_account": "Stock Adjustment - _TC",
+ "basic_rate": 150,
+ "item_code": "_Test Item 2",
+ "parentfield": "items",
+ "qty": 50.0,
+ "stock_uom": "_Test UOM",
+ "t_warehouse": "_Test Warehouse - _TC",
+ "transfer_qty": 50.0,
+ "uom": "_Test UOM",
+ },
+ )
repack.set_stock_entry_type()
repack.insert()
@@ -266,12 +349,13 @@ class TestStockEntry(ERPNextTestCase):
# must raise error if 0 fg in repack entry
self.assertRaises(FinishedGoodError, repack.validate_finished_goods)
- repack.delete() # teardown
+ repack.delete() # teardown
def test_repack_no_change_in_valuation(self):
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
- make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC",
- qty=50, basic_rate=100)
+ make_stock_entry(
+ item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=50, basic_rate=100
+ )
repack = frappe.copy_doc(test_records[3])
repack.posting_date = nowdate()
@@ -280,76 +364,113 @@ class TestStockEntry(ERPNextTestCase):
repack.insert()
repack.submit()
- self.check_stock_ledger_entries("Stock Entry", repack.name,
- [["_Test Item", "_Test Warehouse - _TC", -50.0],
- ["_Test Item Home Desktop 100", "_Test Warehouse - _TC", 1]])
+ self.check_stock_ledger_entries(
+ "Stock Entry",
+ repack.name,
+ [
+ ["_Test Item", "_Test Warehouse - _TC", -50.0],
+ ["_Test Item Home Desktop 100", "_Test Warehouse - _TC", 1],
+ ],
+ )
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit
from `tabGL Entry` where voucher_type='Stock Entry' and voucher_no=%s
- order by account desc""", repack.name, as_dict=1)
+ order by account desc""",
+ repack.name,
+ as_dict=1,
+ )
self.assertFalse(gl_entries)
def test_repack_with_additional_costs(self):
- company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
+ company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
- make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company= company,
- qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1")
+ make_stock_entry(
+ item_code="_Test Item",
+ target="Stores - TCP1",
+ company=company,
+ qty=50,
+ basic_rate=100,
+ expense_account="Stock Adjustment - TCP1",
+ )
-
- repack = make_stock_entry(company = company, purpose="Repack", do_not_save=True)
+ repack = make_stock_entry(company=company, purpose="Repack", do_not_save=True)
repack.posting_date = nowdate()
repack.posting_time = nowtime()
- expenses_included_in_valuation = frappe.get_value("Company", company, "expenses_included_in_valuation")
+ expenses_included_in_valuation = frappe.get_value(
+ "Company", company, "expenses_included_in_valuation"
+ )
items = get_multiple_items()
repack.items = []
for item in items:
repack.append("items", item)
- repack.set("additional_costs", [
- {
- "expense_account": expenses_included_in_valuation,
- "description": "Actual Operating Cost",
- "amount": 1000
- },
- {
- "expense_account": expenses_included_in_valuation,
- "description": "Additional Operating Cost",
- "amount": 200
- },
- ])
+ repack.set(
+ "additional_costs",
+ [
+ {
+ "expense_account": expenses_included_in_valuation,
+ "description": "Actual Operating Cost",
+ "amount": 1000,
+ },
+ {
+ "expense_account": expenses_included_in_valuation,
+ "description": "Additional Operating Cost",
+ "amount": 200,
+ },
+ ],
+ )
repack.set_stock_entry_type()
repack.insert()
repack.submit()
stock_in_hand_account = get_inventory_account(repack.company, repack.get("items")[1].t_warehouse)
- rm_stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry",
- "voucher_no": repack.name, "item_code": "_Test Item"}, "stock_value_difference"))
+ rm_stock_value_diff = abs(
+ frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Stock Entry", "voucher_no": repack.name, "item_code": "_Test Item"},
+ "stock_value_difference",
+ )
+ )
- fg_stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry",
- "voucher_no": repack.name, "item_code": "_Test Item Home Desktop 100"}, "stock_value_difference"))
+ fg_stock_value_diff = abs(
+ frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Stock Entry",
+ "voucher_no": repack.name,
+ "item_code": "_Test Item Home Desktop 100",
+ },
+ "stock_value_difference",
+ )
+ )
stock_value_diff = flt(fg_stock_value_diff - rm_stock_value_diff, 2)
self.assertEqual(stock_value_diff, 1200)
- self.check_gl_entries("Stock Entry", repack.name,
- sorted([
- [stock_in_hand_account, 1200, 0.0],
- ["Expenses Included In Valuation - TCP1", 0.0, 1200.0]
- ])
+ self.check_gl_entries(
+ "Stock Entry",
+ repack.name,
+ sorted(
+ [[stock_in_hand_account, 1200, 0.0], ["Expenses Included In Valuation - TCP1", 0.0, 1200.0]]
+ ),
)
def check_stock_ledger_entries(self, voucher_type, voucher_no, expected_sle):
expected_sle.sort(key=lambda x: x[1])
# check stock ledger entries
- sle = frappe.db.sql("""select item_code, warehouse, actual_qty
+ sle = frappe.db.sql(
+ """select item_code, warehouse, actual_qty
from `tabStock Ledger Entry` where voucher_type = %s
and voucher_no = %s order by item_code, warehouse, actual_qty""",
- (voucher_type, voucher_no), as_list=1)
+ (voucher_type, voucher_no),
+ as_list=1,
+ )
self.assertTrue(sle)
sle.sort(key=lambda x: x[1])
@@ -361,9 +482,13 @@ class TestStockEntry(ERPNextTestCase):
def check_gl_entries(self, voucher_type, voucher_no, expected_gl_entries):
expected_gl_entries.sort(key=lambda x: x[0])
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit
from `tabGL Entry` where voucher_type=%s and voucher_no=%s
- order by account asc, debit asc""", (voucher_type, voucher_no), as_list=1)
+ order by account asc, debit asc""",
+ (voucher_type, voucher_no),
+ as_list=1,
+ )
self.assertTrue(gl_entries)
gl_entries.sort(key=lambda x: x[0])
@@ -464,7 +589,7 @@ class TestStockEntry(ERPNextTestCase):
def test_serial_item_error(self):
se, serial_nos = self.test_serial_by_series()
- if not frappe.db.exists('Serial No', 'ABCD'):
+ if not frappe.db.exists("Serial No", "ABCD"):
make_serialized_item(item_code="_Test Serialized Item", serial_no="ABCD\nEFGH")
se = frappe.copy_doc(test_records[0])
@@ -494,10 +619,14 @@ class TestStockEntry(ERPNextTestCase):
se.set_stock_entry_type()
se.insert()
se.submit()
- self.assertTrue(frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse 1 - _TC")
+ self.assertTrue(
+ frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse 1 - _TC"
+ )
se.cancel()
- self.assertTrue(frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC")
+ self.assertTrue(
+ frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC"
+ )
def test_serial_warehouse_error(self):
make_serialized_item(target_warehouse="_Test Warehouse 1 - _TC")
@@ -525,14 +654,16 @@ class TestStockEntry(ERPNextTestCase):
self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse"))
def test_warehouse_company_validation(self):
- company = frappe.db.get_value('Warehouse', '_Test Warehouse 2 - _TC1', 'company')
- frappe.get_doc("User", "test2@example.com")\
- .add_roles("Sales User", "Sales Manager", "Stock User", "Stock Manager")
+ company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company")
+ frappe.get_doc("User", "test2@example.com").add_roles(
+ "Sales User", "Sales Manager", "Stock User", "Stock Manager"
+ )
frappe.set_user("test2@example.com")
from erpnext.stock.utils import InvalidWarehouseCompany
+
st1 = frappe.copy_doc(test_records[0])
- st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1"
+ st1.get("items")[0].t_warehouse = "_Test Warehouse 2 - _TC1"
st1.set_stock_entry_type()
st1.insert()
self.assertRaises(InvalidWarehouseCompany, st1.submit)
@@ -546,14 +677,15 @@ class TestStockEntry(ERPNextTestCase):
test_user.add_roles("Sales User", "Sales Manager", "Stock User")
test_user.remove_roles("Stock Manager", "System Manager")
- frappe.get_doc("User", "test2@example.com")\
- .add_roles("Sales User", "Sales Manager", "Stock User", "Stock Manager")
+ frappe.get_doc("User", "test2@example.com").add_roles(
+ "Sales User", "Sales Manager", "Stock User", "Stock Manager"
+ )
st1 = frappe.copy_doc(test_records[0])
st1.company = "_Test Company 1"
frappe.set_user("test@example.com")
- st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1"
+ st1.get("items")[0].t_warehouse = "_Test Warehouse 2 - _TC1"
self.assertRaises(frappe.PermissionError, st1.insert)
test_user.add_roles("System Manager")
@@ -561,7 +693,7 @@ class TestStockEntry(ERPNextTestCase):
frappe.set_user("test2@example.com")
st1 = frappe.copy_doc(test_records[0])
st1.company = "_Test Company 1"
- st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1"
+ st1.get("items")[0].t_warehouse = "_Test Warehouse 2 - _TC1"
st1.get("items")[0].expense_account = "Stock Adjustment - _TC1"
st1.get("items")[0].cost_center = "Main - _TC1"
st1.set_stock_entry_type()
@@ -575,14 +707,14 @@ class TestStockEntry(ERPNextTestCase):
remove_user_permission("Company", "_Test Company 1", "test2@example.com")
def test_freeze_stocks(self):
- frappe.db.set_value('Stock Settings', None,'stock_auth_role', '')
+ frappe.db.set_value("Stock Settings", None, "stock_auth_role", "")
# test freeze_stocks_upto
frappe.db.set_value("Stock Settings", None, "stock_frozen_upto", add_days(nowdate(), 5))
se = frappe.copy_doc(test_records[0]).insert()
self.assertRaises(StockFreezeError, se.submit)
- frappe.db.set_value("Stock Settings", None, "stock_frozen_upto", '')
+ frappe.db.set_value("Stock Settings", None, "stock_frozen_upto", "")
# test freeze_stocks_upto_days
frappe.db.set_value("Stock Settings", None, "stock_frozen_upto_days", -1)
@@ -598,20 +730,24 @@ class TestStockEntry(ERPNextTestCase):
from erpnext.manufacturing.doctype.work_order.work_order import (
make_stock_entry as _make_stock_entry,
)
- bom_no, bom_operation_cost = frappe.db.get_value("BOM", {"item": "_Test FG Item 2",
- "is_default": 1, "docstatus": 1}, ["name", "operating_cost"])
+
+ bom_no, bom_operation_cost = frappe.db.get_value(
+ "BOM", {"item": "_Test FG Item 2", "is_default": 1, "docstatus": 1}, ["name", "operating_cost"]
+ )
work_order = frappe.new_doc("Work Order")
- work_order.update({
- "company": "_Test Company",
- "fg_warehouse": "_Test Warehouse 1 - _TC",
- "production_item": "_Test FG Item 2",
- "bom_no": bom_no,
- "qty": 1.0,
- "stock_uom": "_Test UOM",
- "wip_warehouse": "_Test Warehouse - _TC",
- "additional_operating_cost": 1000
- })
+ work_order.update(
+ {
+ "company": "_Test Company",
+ "fg_warehouse": "_Test Warehouse 1 - _TC",
+ "production_item": "_Test FG Item 2",
+ "bom_no": bom_no,
+ "qty": 1.0,
+ "stock_uom": "_Test UOM",
+ "wip_warehouse": "_Test Warehouse - _TC",
+ "additional_operating_cost": 1000,
+ }
+ )
work_order.insert()
work_order.submit()
@@ -624,37 +760,40 @@ class TestStockEntry(ERPNextTestCase):
for d in stock_entry.get("items"):
if d.item_code != "_Test FG Item 2":
rm_cost += flt(d.amount)
- fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item 2", stock_entry.get("items")))[0].amount
- self.assertEqual(fg_cost,
- flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2))
+ fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item 2", stock_entry.get("items")))[
+ 0
+ ].amount
+ self.assertEqual(
+ fg_cost, flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2)
+ )
+ @change_settings("Manufacturing Settings", {"material_consumption": 1})
def test_work_order_manufacture_with_material_consumption(self):
from erpnext.manufacturing.doctype.work_order.work_order import (
make_stock_entry as _make_stock_entry,
)
- frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "1")
- bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item",
- "is_default": 1, "docstatus": 1})
+ bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item", "is_default": 1, "docstatus": 1})
work_order = frappe.new_doc("Work Order")
- work_order.update({
- "company": "_Test Company",
- "fg_warehouse": "_Test Warehouse 1 - _TC",
- "production_item": "_Test FG Item",
- "bom_no": bom_no,
- "qty": 1.0,
- "stock_uom": "_Test UOM",
- "wip_warehouse": "_Test Warehouse - _TC"
- })
+ work_order.update(
+ {
+ "company": "_Test Company",
+ "fg_warehouse": "_Test Warehouse 1 - _TC",
+ "production_item": "_Test FG Item",
+ "bom_no": bom_no,
+ "qty": 1.0,
+ "stock_uom": "_Test UOM",
+ "wip_warehouse": "_Test Warehouse - _TC",
+ }
+ )
work_order.insert()
work_order.submit()
- make_stock_entry(item_code="_Test Item",
- target="Stores - _TC", qty=10, basic_rate=5000.0)
- make_stock_entry(item_code="_Test Item Home Desktop 100",
- target="Stores - _TC", qty=10, basic_rate=1000.0)
-
+ make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=5000.0)
+ make_stock_entry(
+ item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=1000.0
+ )
s = frappe.get_doc(_make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1))
for d in s.get("items"):
@@ -666,13 +805,12 @@ class TestStockEntry(ERPNextTestCase):
s = frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 1))
s.save()
rm_cost = 0
- for d in s.get('items'):
+ for d in s.get("items"):
if d.s_warehouse:
rm_cost += d.amount
- fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount
+ fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item", s.get("items")))[0].amount
scrap_cost = list(filter(lambda x: x.is_scrap_item, s.get("items")))[0].amount
- self.assertEqual(fg_cost,
- flt(rm_cost - scrap_cost, 2))
+ self.assertEqual(fg_cost, flt(rm_cost - scrap_cost, 2))
# When Stock Entry has only FG + Scrap
s.items.pop(0)
@@ -680,31 +818,34 @@ class TestStockEntry(ERPNextTestCase):
s.submit()
rm_cost = 0
- for d in s.get('items'):
+ for d in s.get("items"):
if d.s_warehouse:
rm_cost += d.amount
self.assertEqual(rm_cost, 0)
expected_fg_cost = s.get_basic_rate_for_manufactured_item(1)
- fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount
+ fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item", s.get("items")))[0].amount
self.assertEqual(flt(fg_cost, 2), flt(expected_fg_cost, 2))
def test_variant_work_order(self):
- bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item",
- "is_default": 1, "docstatus": 1})
+ bom_no = frappe.db.get_value(
+ "BOM", {"item": "_Test Variant Item", "is_default": 1, "docstatus": 1}
+ )
- make_item_variant() # make variant of _Test Variant Item if absent
+ make_item_variant() # make variant of _Test Variant Item if absent
work_order = frappe.new_doc("Work Order")
- work_order.update({
- "company": "_Test Company",
- "fg_warehouse": "_Test Warehouse 1 - _TC",
- "production_item": "_Test Variant Item-S",
- "bom_no": bom_no,
- "qty": 1.0,
- "stock_uom": "_Test UOM",
- "wip_warehouse": "_Test Warehouse - _TC",
- "skip_transfer": 1
- })
+ work_order.update(
+ {
+ "company": "_Test Company",
+ "fg_warehouse": "_Test Warehouse 1 - _TC",
+ "production_item": "_Test Variant Item-S",
+ "bom_no": bom_no,
+ "qty": 1.0,
+ "stock_uom": "_Test UOM",
+ "wip_warehouse": "_Test Warehouse - _TC",
+ "skip_transfer": 1,
+ }
+ )
work_order.insert()
work_order.submit()
@@ -718,19 +859,29 @@ class TestStockEntry(ERPNextTestCase):
s1 = make_serialized_item(target_warehouse="_Test Warehouse - _TC")
serial_nos = s1.get("items")[0].serial_no
- s2 = make_stock_entry(item_code="_Test Serialized Item With Series", source="_Test Warehouse - _TC",
- qty=2, basic_rate=100, purpose="Repack", serial_no=serial_nos, do_not_save=True)
+ s2 = make_stock_entry(
+ item_code="_Test Serialized Item With Series",
+ source="_Test Warehouse - _TC",
+ qty=2,
+ basic_rate=100,
+ purpose="Repack",
+ serial_no=serial_nos,
+ do_not_save=True,
+ )
- s2.append("items", {
- "item_code": "_Test Serialized Item",
- "t_warehouse": "_Test Warehouse - _TC",
- "qty": 2,
- "basic_rate": 120,
- "expense_account": "Stock Adjustment - _TC",
- "conversion_factor": 1.0,
- "cost_center": "_Test Cost Center - _TC",
- "serial_no": serial_nos
- })
+ s2.append(
+ "items",
+ {
+ "item_code": "_Test Serialized Item",
+ "t_warehouse": "_Test Warehouse - _TC",
+ "qty": 2,
+ "basic_rate": 120,
+ "expense_account": "Stock Adjustment - _TC",
+ "conversion_factor": 1.0,
+ "cost_center": "_Test Cost Center - _TC",
+ "serial_no": serial_nos,
+ },
+ )
s2.submit()
s2.cancel()
@@ -740,10 +891,15 @@ class TestStockEntry(ERPNextTestCase):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
create_warehouse("Test Warehouse for Sample Retention")
- frappe.db.set_value("Stock Settings", None, "sample_retention_warehouse", "Test Warehouse for Sample Retention - _TC")
+ frappe.db.set_value(
+ "Stock Settings",
+ None,
+ "sample_retention_warehouse",
+ "Test Warehouse for Sample Retention - _TC",
+ )
test_item_code = "Retain Sample Item"
- if not frappe.db.exists('Item', test_item_code):
+ if not frappe.db.exists("Item", test_item_code):
item = frappe.new_doc("Item")
item.item_code = test_item_code
item.item_name = "Retain Sample Item"
@@ -759,44 +915,58 @@ class TestStockEntry(ERPNextTestCase):
receipt_entry = frappe.new_doc("Stock Entry")
receipt_entry.company = "_Test Company"
receipt_entry.purpose = "Material Receipt"
- receipt_entry.append("items", {
- "item_code": test_item_code,
- "t_warehouse": "_Test Warehouse - _TC",
- "qty": 40,
- "basic_rate": 12,
- "cost_center": "_Test Cost Center - _TC",
- "sample_quantity": 4
- })
+ receipt_entry.append(
+ "items",
+ {
+ "item_code": test_item_code,
+ "t_warehouse": "_Test Warehouse - _TC",
+ "qty": 40,
+ "basic_rate": 12,
+ "cost_center": "_Test Cost Center - _TC",
+ "sample_quantity": 4,
+ },
+ )
receipt_entry.set_stock_entry_type()
receipt_entry.insert()
receipt_entry.submit()
- retention_data = move_sample_to_retention_warehouse(receipt_entry.company, receipt_entry.get("items"))
+ retention_data = move_sample_to_retention_warehouse(
+ receipt_entry.company, receipt_entry.get("items")
+ )
retention_entry = frappe.new_doc("Stock Entry")
retention_entry.company = retention_data.company
retention_entry.purpose = retention_data.purpose
- retention_entry.append("items", {
- "item_code": test_item_code,
- "t_warehouse": "Test Warehouse for Sample Retention - _TC",
- "s_warehouse": "_Test Warehouse - _TC",
- "qty": 4,
- "basic_rate": 12,
- "cost_center": "_Test Cost Center - _TC",
- "batch_no": receipt_entry.get("items")[0].batch_no
- })
+ retention_entry.append(
+ "items",
+ {
+ "item_code": test_item_code,
+ "t_warehouse": "Test Warehouse for Sample Retention - _TC",
+ "s_warehouse": "_Test Warehouse - _TC",
+ "qty": 4,
+ "basic_rate": 12,
+ "cost_center": "_Test Cost Center - _TC",
+ "batch_no": receipt_entry.get("items")[0].batch_no,
+ },
+ )
retention_entry.set_stock_entry_type()
retention_entry.insert()
retention_entry.submit()
- qty_in_usable_warehouse = get_batch_qty(receipt_entry.get("items")[0].batch_no, "_Test Warehouse - _TC", "_Test Item")
- qty_in_retention_warehouse = get_batch_qty(receipt_entry.get("items")[0].batch_no, "Test Warehouse for Sample Retention - _TC", "_Test Item")
+ qty_in_usable_warehouse = get_batch_qty(
+ receipt_entry.get("items")[0].batch_no, "_Test Warehouse - _TC", "_Test Item"
+ )
+ qty_in_retention_warehouse = get_batch_qty(
+ receipt_entry.get("items")[0].batch_no,
+ "Test Warehouse for Sample Retention - _TC",
+ "_Test Item",
+ )
self.assertEqual(qty_in_usable_warehouse, 36)
self.assertEqual(qty_in_retention_warehouse, 4)
def test_quality_check(self):
item_code = "_Test Item For QC"
- if not frappe.db.exists('Item', item_code):
+ if not frappe.db.exists("Item", item_code):
create_item(item_code)
repack = frappe.copy_doc(test_records[3])
@@ -812,82 +982,64 @@ class TestStockEntry(ERPNextTestCase):
repack.insert()
self.assertRaises(frappe.ValidationError, repack.submit)
- # def test_material_consumption(self):
- # frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM")
- # frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
-
- # from erpnext.manufacturing.doctype.work_order.work_order \
- # import make_stock_entry as _make_stock_entry
- # bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2",
- # "is_default": 1, "docstatus": 1})
-
- # work_order = frappe.new_doc("Work Order")
- # work_order.update({
- # "company": "_Test Company",
- # "fg_warehouse": "_Test Warehouse 1 - _TC",
- # "production_item": "_Test FG Item 2",
- # "bom_no": bom_no,
- # "qty": 4.0,
- # "stock_uom": "_Test UOM",
- # "wip_warehouse": "_Test Warehouse - _TC",
- # "additional_operating_cost": 1000,
- # "use_multi_level_bom": 1
- # })
- # work_order.insert()
- # work_order.submit()
-
- # make_stock_entry(item_code="_Test Serialized Item With Series", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
- # make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20)
-
- # item_quantity = {
- # '_Test Item': 2.0,
- # '_Test Item 2': 12.0,
- # '_Test Serialized Item With Series': 6.0
- # }
-
- # stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 2))
- # for d in stock_entry.get('items'):
- # self.assertEqual(item_quantity.get(d.item_code), d.qty)
-
def test_customer_provided_parts_se(self):
- create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
- se = make_stock_entry(item_code='CUST-0987', purpose = 'Material Receipt',
- qty=4, to_warehouse = "_Test Warehouse - _TC")
+ create_item(
+ "CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0
+ )
+ se = make_stock_entry(
+ item_code="CUST-0987", purpose="Material Receipt", qty=4, to_warehouse="_Test Warehouse - _TC"
+ )
self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1)
self.assertEqual(se.get("items")[0].amount, 0)
def test_zero_incoming_rate(self):
- """ Make sure incoming rate of 0 is allowed while consuming.
+ """Make sure incoming rate of 0 is allowed while consuming.
- qty | rate | valuation rate
- 1 | 100 | 100
- 1 | 0 | 50
- -1 | 100 | 0
- -1 | 0 <--- assert this
+ qty | rate | valuation rate
+ 1 | 100 | 100
+ 1 | 0 | 50
+ -1 | 100 | 0
+ -1 | 0 <--- assert this
"""
item_code = "_TestZeroVal"
warehouse = "_Test Warehouse - _TC"
- create_item('_TestZeroVal')
+ create_item("_TestZeroVal")
_receipt = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=100)
- receipt2 = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=0, do_not_save=True)
+ receipt2 = make_stock_entry(
+ item_code=item_code, qty=1, to_warehouse=warehouse, rate=0, do_not_save=True
+ )
receipt2.items[0].allow_zero_valuation_rate = 1
receipt2.save()
receipt2.submit()
issue = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse)
- value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue.name, "voucher_type": "Stock Entry"}, "stock_value_difference")
+ value_diff = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_no": issue.name, "voucher_type": "Stock Entry"},
+ "stock_value_difference",
+ )
self.assertEqual(value_diff, -100)
issue2 = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse)
- value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue2.name, "voucher_type": "Stock Entry"}, "stock_value_difference")
+ value_diff = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_no": issue2.name, "voucher_type": "Stock Entry"},
+ "stock_value_difference",
+ )
self.assertEqual(value_diff, 0)
-
def test_gle_for_opening_stock_entry(self):
- mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1",
- company="_Test Company with perpetual inventory", qty=50, basic_rate=100,
- expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True)
+ mr = make_stock_entry(
+ item_code="_Test Item",
+ target="Stores - TCP1",
+ company="_Test Company with perpetual inventory",
+ qty=50,
+ basic_rate=100,
+ expense_account="Stock Adjustment - TCP1",
+ is_opening="Yes",
+ do_not_save=True,
+ )
self.assertRaises(OpeningEntryAccountError, mr.save)
@@ -896,52 +1048,61 @@ class TestStockEntry(ERPNextTestCase):
mr.save()
mr.submit()
- is_opening = frappe.db.get_value("GL Entry",
- filters={"voucher_type": "Stock Entry", "voucher_no": mr.name}, fieldname="is_opening")
+ is_opening = frappe.db.get_value(
+ "GL Entry",
+ filters={"voucher_type": "Stock Entry", "voucher_no": mr.name},
+ fieldname="is_opening",
+ )
self.assertEqual(is_opening, "Yes")
def test_total_basic_amount_zero(self):
- se = frappe.get_doc({"doctype":"Stock Entry",
- "purpose":"Material Receipt",
- "stock_entry_type":"Material Receipt",
- "posting_date": nowdate(),
- "company":"_Test Company with perpetual inventory",
- "items":[
- {
- "item_code":"_Test Item",
- "description":"_Test Item",
- "qty": 1,
- "basic_rate": 0,
- "uom":"Nos",
- "t_warehouse": "Stores - TCP1",
- "allow_zero_valuation_rate": 1,
- "cost_center": "Main - TCP1"
- },
- {
- "item_code":"_Test Item",
- "description":"_Test Item",
- "qty": 2,
- "basic_rate": 0,
- "uom":"Nos",
- "t_warehouse": "Stores - TCP1",
- "allow_zero_valuation_rate": 1,
- "cost_center": "Main - TCP1"
- },
- ],
- "additional_costs":[
- {"expense_account":"Miscellaneous Expenses - TCP1",
- "amount":100,
- "description": "miscellanous"
- }]
- })
+ se = frappe.get_doc(
+ {
+ "doctype": "Stock Entry",
+ "purpose": "Material Receipt",
+ "stock_entry_type": "Material Receipt",
+ "posting_date": nowdate(),
+ "company": "_Test Company with perpetual inventory",
+ "items": [
+ {
+ "item_code": "_Test Item",
+ "description": "_Test Item",
+ "qty": 1,
+ "basic_rate": 0,
+ "uom": "Nos",
+ "t_warehouse": "Stores - TCP1",
+ "allow_zero_valuation_rate": 1,
+ "cost_center": "Main - TCP1",
+ },
+ {
+ "item_code": "_Test Item",
+ "description": "_Test Item",
+ "qty": 2,
+ "basic_rate": 0,
+ "uom": "Nos",
+ "t_warehouse": "Stores - TCP1",
+ "allow_zero_valuation_rate": 1,
+ "cost_center": "Main - TCP1",
+ },
+ ],
+ "additional_costs": [
+ {
+ "expense_account": "Miscellaneous Expenses - TCP1",
+ "amount": 100,
+ "description": "miscellanous",
+ }
+ ],
+ }
+ )
se.insert()
se.submit()
- self.check_gl_entries("Stock Entry", se.name,
- sorted([
- ["Stock Adjustment - TCP1", 100.0, 0.0],
- ["Miscellaneous Expenses - TCP1", 0.0, 100.0]
- ])
+ self.check_gl_entries(
+ "Stock Entry",
+ se.name,
+ sorted(
+ [["Stock Adjustment - TCP1", 100.0, 0.0], ["Miscellaneous Expenses - TCP1", 0.0, 100.0]]
+ ),
)
def test_conversion_factor_change(self):
@@ -972,15 +1133,15 @@ class TestStockEntry(ERPNextTestCase):
def test_additional_cost_distribution_manufacture(self):
se = frappe.get_doc(
- doctype="Stock Entry",
- purpose="Manufacture",
- additional_costs=[frappe._dict(base_amount=100)],
- items=[
- frappe._dict(item_code="RM", basic_amount=10),
- frappe._dict(item_code="FG", basic_amount=20, t_warehouse="X", is_finished_item=1),
- frappe._dict(item_code="scrap", basic_amount=30, t_warehouse="X")
- ],
- )
+ doctype="Stock Entry",
+ purpose="Manufacture",
+ additional_costs=[frappe._dict(base_amount=100)],
+ items=[
+ frappe._dict(item_code="RM", basic_amount=10),
+ frappe._dict(item_code="FG", basic_amount=20, t_warehouse="X", is_finished_item=1),
+ frappe._dict(item_code="scrap", basic_amount=30, t_warehouse="X"),
+ ],
+ )
se.distribute_additional_costs()
@@ -989,14 +1150,14 @@ class TestStockEntry(ERPNextTestCase):
def test_additional_cost_distribution_non_manufacture(self):
se = frappe.get_doc(
- doctype="Stock Entry",
- purpose="Material Receipt",
- additional_costs=[frappe._dict(base_amount=100)],
- items=[
- frappe._dict(item_code="RECEIVED_1", basic_amount=20, t_warehouse="X"),
- frappe._dict(item_code="RECEIVED_2", basic_amount=30, t_warehouse="X")
- ],
- )
+ doctype="Stock Entry",
+ purpose="Material Receipt",
+ additional_costs=[frappe._dict(base_amount=100)],
+ items=[
+ frappe._dict(item_code="RECEIVED_1", basic_amount=20, t_warehouse="X"),
+ frappe._dict(item_code="RECEIVED_2", basic_amount=30, t_warehouse="X"),
+ ],
+ )
se.distribute_additional_costs()
@@ -1006,40 +1167,42 @@ class TestStockEntry(ERPNextTestCase):
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_future_negative_sle(self):
# Initialize item, batch, warehouse, opening qty
- item_code = '_Test Future Neg Item'
- batch_no = '_Test Future Neg Batch'
- warehouses = [
- '_Test Future Neg Warehouse Source',
- '_Test Future Neg Warehouse Destination'
- ]
+ item_code = "_Test Future Neg Item"
+ batch_no = "_Test Future Neg Batch"
+ warehouses = ["_Test Future Neg Warehouse Source", "_Test Future Neg Warehouse Destination"]
warehouse_names = initialize_records_for_future_negative_sle_test(
- item_code, batch_no, warehouses,
- opening_qty=2, posting_date='2021-07-01'
+ item_code, batch_no, warehouses, opening_qty=2, posting_date="2021-07-01"
)
# Executing an illegal sequence should raise an error
sequence_of_entries = [
- dict(item_code=item_code,
+ dict(
+ item_code=item_code,
qty=2,
from_warehouse=warehouse_names[0],
to_warehouse=warehouse_names[1],
batch_no=batch_no,
- posting_date='2021-07-03',
- purpose='Material Transfer'),
- dict(item_code=item_code,
+ posting_date="2021-07-03",
+ purpose="Material Transfer",
+ ),
+ dict(
+ item_code=item_code,
qty=2,
from_warehouse=warehouse_names[1],
to_warehouse=warehouse_names[0],
batch_no=batch_no,
- posting_date='2021-07-04',
- purpose='Material Transfer'),
- dict(item_code=item_code,
+ posting_date="2021-07-04",
+ purpose="Material Transfer",
+ ),
+ dict(
+ item_code=item_code,
qty=2,
from_warehouse=warehouse_names[0],
to_warehouse=warehouse_names[1],
batch_no=batch_no,
- posting_date='2021-07-02', # Illegal SE
- purpose='Material Transfer')
+ posting_date="2021-07-02", # Illegal SE
+ purpose="Material Transfer",
+ ),
]
self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries)
@@ -1049,36 +1212,38 @@ class TestStockEntry(ERPNextTestCase):
from erpnext.stock.doctype.batch.test_batch import TestBatch
# Initialize item, batch, warehouse, opening qty
- item_code = '_Test MultiBatch Item'
+ item_code = "_Test MultiBatch Item"
TestBatch.make_batch_item(item_code)
- batch_nos = [] # store generate batches
- warehouse = '_Test Warehouse - _TC'
+ batch_nos = [] # store generate batches
+ warehouse = "_Test Warehouse - _TC"
se1 = make_stock_entry(
- item_code=item_code,
- qty=2,
- to_warehouse=warehouse,
- posting_date='2021-09-01',
- purpose='Material Receipt'
- )
+ item_code=item_code,
+ qty=2,
+ to_warehouse=warehouse,
+ posting_date="2021-09-01",
+ purpose="Material Receipt",
+ )
batch_nos.append(se1.items[0].batch_no)
se2 = make_stock_entry(
- item_code=item_code,
- qty=2,
- to_warehouse=warehouse,
- posting_date='2021-09-03',
- purpose='Material Receipt'
- )
+ item_code=item_code,
+ qty=2,
+ to_warehouse=warehouse,
+ posting_date="2021-09-03",
+ purpose="Material Receipt",
+ )
batch_nos.append(se2.items[0].batch_no)
with self.assertRaises(NegativeStockError) as nse:
- make_stock_entry(item_code=item_code,
+ make_stock_entry(
+ item_code=item_code,
qty=1,
from_warehouse=warehouse,
batch_no=batch_nos[1],
- posting_date='2021-09-02', # backdated consumption of 2nd batch
- purpose='Material Issue')
+ posting_date="2021-09-02", # backdated consumption of 2nd batch
+ purpose="Material Issue",
+ )
def test_independent_manufacture_entry(self):
"Test FG items and incoming rate calculation in Maniufacture Entry without WO or BOM linked."
@@ -1088,9 +1253,11 @@ class TestStockEntry(ERPNextTestCase):
stock_entry_type="Manufacture",
company="_Test Company",
items=[
- frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"),
- frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC")
- ]
+ frappe._dict(
+ item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"
+ ),
+ frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC"),
+ ],
)
# SE must have atleast one FG
self.assertRaises(FinishedGoodError, se.save)
@@ -1105,10 +1272,18 @@ class TestStockEntry(ERPNextTestCase):
# Check if FG cost is calculated based on RM total cost
# RM total cost = 200, FG rate = 200/4(FG qty) = 50
- self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate/4))
+ self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate / 4))
self.assertEqual(se.value_difference, 0.0)
self.assertEqual(se.total_incoming_value, se.total_outgoing_value)
+ def test_transfer_qty_validation(self):
+ se = make_stock_entry(item_code="_Test Item", do_not_save=True, qty=0.001, rate=100)
+ se.items[0].uom = "Kg"
+ se.items[0].conversion_factor = 0.002
+
+ self.assertRaises(frappe.ValidationError, se.save)
+
+
def make_serialized_item(**args):
args = frappe._dict(args)
se = frappe.copy_doc(test_records[0])
@@ -1138,50 +1313,57 @@ def make_serialized_item(**args):
se.submit()
return se
+
def get_qty_after_transaction(**args):
args = frappe._dict(args)
- last_sle = get_previous_sle({
- "item_code": args.item_code or "_Test Item",
- "warehouse": args.warehouse or "_Test Warehouse - _TC",
- "posting_date": args.posting_date or nowdate(),
- "posting_time": args.posting_time or nowtime()
- })
+ last_sle = get_previous_sle(
+ {
+ "item_code": args.item_code or "_Test Item",
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "posting_date": args.posting_date or nowdate(),
+ "posting_time": args.posting_time or nowtime(),
+ }
+ )
return flt(last_sle.get("qty_after_transaction"))
+
def get_multiple_items():
return [
- {
- "conversion_factor": 1.0,
- "cost_center": "Main - TCP1",
- "doctype": "Stock Entry Detail",
- "expense_account": "Stock Adjustment - TCP1",
- "basic_rate": 100,
- "item_code": "_Test Item",
- "qty": 50.0,
- "s_warehouse": "Stores - TCP1",
- "stock_uom": "_Test UOM",
- "transfer_qty": 50.0,
- "uom": "_Test UOM"
- },
- {
- "conversion_factor": 1.0,
- "cost_center": "Main - TCP1",
- "doctype": "Stock Entry Detail",
- "expense_account": "Stock Adjustment - TCP1",
- "basic_rate": 5000,
- "item_code": "_Test Item Home Desktop 100",
- "qty": 1,
- "stock_uom": "_Test UOM",
- "t_warehouse": "Stores - TCP1",
- "transfer_qty": 1,
- "uom": "_Test UOM"
- }
- ]
+ {
+ "conversion_factor": 1.0,
+ "cost_center": "Main - TCP1",
+ "doctype": "Stock Entry Detail",
+ "expense_account": "Stock Adjustment - TCP1",
+ "basic_rate": 100,
+ "item_code": "_Test Item",
+ "qty": 50.0,
+ "s_warehouse": "Stores - TCP1",
+ "stock_uom": "_Test UOM",
+ "transfer_qty": 50.0,
+ "uom": "_Test UOM",
+ },
+ {
+ "conversion_factor": 1.0,
+ "cost_center": "Main - TCP1",
+ "doctype": "Stock Entry Detail",
+ "expense_account": "Stock Adjustment - TCP1",
+ "basic_rate": 5000,
+ "item_code": "_Test Item Home Desktop 100",
+ "qty": 1,
+ "stock_uom": "_Test UOM",
+ "t_warehouse": "Stores - TCP1",
+ "transfer_qty": 1,
+ "uom": "_Test UOM",
+ },
+ ]
+
+
+test_records = frappe.get_test_records("Stock Entry")
-test_records = frappe.get_test_records('Stock Entry')
def initialize_records_for_future_negative_sle_test(
- item_code, batch_no, warehouses, opening_qty, posting_date):
+ item_code, batch_no, warehouses, opening_qty, posting_date
+):
from erpnext.stock.doctype.batch.test_batch import TestBatch, make_new_batch
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
@@ -1192,9 +1374,9 @@ def initialize_records_for_future_negative_sle_test(
make_new_batch(item_code=item_code, batch_id=batch_no)
warehouse_names = [create_warehouse(w) for w in warehouses]
create_stock_reconciliation(
- purpose='Opening Stock',
+ purpose="Opening Stock",
posting_date=posting_date,
- posting_time='20:00:20',
+ posting_time="20:00:20",
item_code=item_code,
warehouse=warehouse_names[0],
valuation_rate=100,
diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py
index efd97c04ac6..7258cfbe2c9 100644
--- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py
+++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py
@@ -8,5 +8,5 @@ from frappe.model.document import Document
class StockEntryType(Document):
def validate(self):
- if self.add_to_transit and self.purpose != 'Material Transfer':
+ if self.add_to_transit and self.purpose != "Material Transfer":
self.add_to_transit = 0
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
index 2651407d16f..6de40984cd7 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
@@ -317,7 +317,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-10-08 13:42:51.857631",
+ "modified": "2021-10-08 13:44:51.857631",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 39ca97a47bc..5c1da420e24 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -1,4 +1,3 @@
-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
@@ -15,11 +14,17 @@ from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
-class StockFreezeError(frappe.ValidationError): pass
-class BackDatedStockTransaction(frappe.ValidationError): pass
+class StockFreezeError(frappe.ValidationError):
+ pass
+
+
+class BackDatedStockTransaction(frappe.ValidationError):
+ pass
+
exclude_from_linked_with = True
+
class StockLedgerEntry(Document):
def autoname(self):
"""
@@ -27,10 +32,13 @@ class StockLedgerEntry(Document):
name will be changed using autoname options (in a scheduled job)
"""
self.name = frappe.generate_hash(txt="", length=10)
+ if self.meta.autoname == "hash":
+ self.to_rename = 0
def validate(self):
self.flags.ignore_submit_comment = True
from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
+
self.validate_mandatory()
self.validate_item()
self.validate_batch()
@@ -41,24 +49,29 @@ class StockLedgerEntry(Document):
self.block_transactions_against_group_warehouse()
self.validate_with_last_transaction_posting_time()
-
def on_submit(self):
self.check_stock_frozen_date()
self.calculate_batch_qty()
if not self.get("via_landed_cost_voucher"):
from erpnext.stock.doctype.serial_no.serial_no import process_serial_no
+
process_serial_no(self)
def calculate_batch_qty(self):
if self.batch_no:
- batch_qty = frappe.db.get_value("Stock Ledger Entry",
- {"docstatus": 1, "batch_no": self.batch_no, "is_cancelled": 0},
- "sum(actual_qty)") or 0
+ batch_qty = (
+ frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"docstatus": 1, "batch_no": self.batch_no, "is_cancelled": 0},
+ "sum(actual_qty)",
+ )
+ or 0
+ )
frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty)
def validate_mandatory(self):
- mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company']
+ mandatory = ["warehouse", "posting_date", "voucher_type", "voucher_no", "company"]
for k in mandatory:
if not self.get(k):
frappe.throw(_("{0} is required").format(self.meta.get_label(k)))
@@ -67,9 +80,13 @@ class StockLedgerEntry(Document):
frappe.throw(_("Actual Qty is mandatory"))
def validate_item(self):
- item_det = frappe.db.sql("""select name, item_name, has_batch_no, docstatus,
+ item_det = frappe.db.sql(
+ """select name, item_name, has_batch_no, docstatus,
is_stock_item, has_variants, stock_uom, create_new_batch
- from tabItem where name=%s""", self.item_code, as_dict=True)
+ from tabItem where name=%s""",
+ self.item_code,
+ as_dict=True,
+ )
if not item_det:
frappe.throw(_("Item {0} not found").format(self.item_code))
@@ -81,39 +98,58 @@ class StockLedgerEntry(Document):
# check if batch number is valid
if item_det.has_batch_no == 1:
- batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name
+ batch_item = (
+ self.item_code
+ if self.item_code == item_det.item_name
+ else self.item_code + ":" + item_det.item_name
+ )
if not self.batch_no:
frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item))
- elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}):
- frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item))
+ elif not frappe.db.get_value("Batch", {"item": self.item_code, "name": self.batch_no}):
+ frappe.throw(
+ _("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)
+ )
elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0:
frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code))
if item_det.has_variants:
- frappe.throw(_("Stock cannot exist for Item {0} since has variants").format(self.item_code),
- ItemTemplateCannotHaveStock)
+ frappe.throw(
+ _("Stock cannot exist for Item {0} since has variants").format(self.item_code),
+ ItemTemplateCannotHaveStock,
+ )
self.stock_uom = item_det.stock_uom
def check_stock_frozen_date(self):
- stock_settings = frappe.get_cached_doc('Stock Settings')
+ stock_settings = frappe.get_cached_doc("Stock Settings")
if stock_settings.stock_frozen_upto:
- if (getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto)
- and stock_settings.stock_auth_role not in frappe.get_roles()):
- frappe.throw(_("Stock transactions before {0} are frozen")
- .format(formatdate(stock_settings.stock_frozen_upto)), StockFreezeError)
+ if (
+ getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto)
+ and stock_settings.stock_auth_role not in frappe.get_roles()
+ ):
+ frappe.throw(
+ _("Stock transactions before {0} are frozen").format(
+ formatdate(stock_settings.stock_frozen_upto)
+ ),
+ StockFreezeError,
+ )
stock_frozen_upto_days = cint(stock_settings.stock_frozen_upto_days)
if stock_frozen_upto_days:
- older_than_x_days_ago = (add_days(getdate(self.posting_date), stock_frozen_upto_days) <= date.today())
+ older_than_x_days_ago = (
+ add_days(getdate(self.posting_date), stock_frozen_upto_days) <= date.today()
+ )
if older_than_x_days_ago and stock_settings.stock_auth_role not in frappe.get_roles():
- frappe.throw(_("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days), StockFreezeError)
+ frappe.throw(
+ _("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days),
+ StockFreezeError,
+ )
def scrub_posting_time(self):
- if not self.posting_time or self.posting_time == '00:0':
- self.posting_time = '00:00'
+ if not self.posting_time or self.posting_time == "00:0":
+ self.posting_time = "00:00"
def validate_batch(self):
if self.batch_no and self.voucher_type != "Stock Entry":
@@ -127,43 +163,61 @@ class StockLedgerEntry(Document):
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
else:
from erpnext.accounts.utils import validate_fiscal_year
- validate_fiscal_year(self.posting_date, self.fiscal_year, self.company,
- self.meta.get_label("posting_date"), self)
+
+ validate_fiscal_year(
+ self.posting_date, self.fiscal_year, self.company, self.meta.get_label("posting_date"), self
+ )
def block_transactions_against_group_warehouse(self):
from erpnext.stock.utils import is_group_warehouse
+
is_group_warehouse(self.warehouse)
def validate_with_last_transaction_posting_time(self):
- authorized_role = frappe.db.get_single_value("Stock Settings", "role_allowed_to_create_edit_back_dated_transactions")
+ authorized_role = frappe.db.get_single_value(
+ "Stock Settings", "role_allowed_to_create_edit_back_dated_transactions"
+ )
if authorized_role:
authorized_users = get_users(authorized_role)
if authorized_users and frappe.session.user not in authorized_users:
- last_transaction_time = frappe.db.sql("""
+ last_transaction_time = frappe.db.sql(
+ """
select MAX(timestamp(posting_date, posting_time)) as posting_time
from `tabStock Ledger Entry`
where docstatus = 1 and is_cancelled = 0 and item_code = %s
- and warehouse = %s""", (self.item_code, self.warehouse))[0][0]
+ and warehouse = %s""",
+ (self.item_code, self.warehouse),
+ )[0][0]
- cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00")
+ cur_doc_posting_datetime = "%s %s" % (
+ self.posting_date,
+ self.get("posting_time") or "00:00:00",
+ )
- if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime(last_transaction_time):
- msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format(frappe.bold(self.item_code),
- frappe.bold(self.warehouse), frappe.bold(last_transaction_time))
+ if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime(
+ last_transaction_time
+ ):
+ msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format(
+ frappe.bold(self.item_code), frappe.bold(self.warehouse), frappe.bold(last_transaction_time)
+ )
- msg += "
" + _("You are not authorized to make/edit Stock Transactions for Item {0} under warehouse {1} before this time.").format(
- frappe.bold(self.item_code), frappe.bold(self.warehouse))
+ msg += "
" + _(
+ "You are not authorized to make/edit Stock Transactions for Item {0} under warehouse {1} before this time."
+ ).format(frappe.bold(self.item_code), frappe.bold(self.warehouse))
msg += "
" + _("Please contact any of the following users to {} this transaction.")
msg += " " + " ".join(authorized_users)
frappe.throw(msg, BackDatedStockTransaction, title=_("Backdated Stock Entry"))
+
def on_doctype_update():
- if not frappe.db.has_index('tabStock Ledger Entry', 'posting_sort_index'):
+ if not frappe.db.has_index("tabStock Ledger Entry", "posting_sort_index"):
frappe.db.commit()
- frappe.db.add_index("Stock Ledger Entry",
+ frappe.db.add_index(
+ "Stock Ledger Entry",
fields=["posting_date", "posting_time", "name"],
- index_name="posting_sort_index")
+ index_name="posting_sort_index",
+ )
frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"])
frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"])
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index 6d113ba4eb6..74775b98e39 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -5,12 +5,13 @@ import json
import frappe
from frappe.core.page.permission_manager.permission_manager import reset
+from frappe.custom.doctype.property_setter.property_setter import make_property_setter
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, today
+from frappe.utils.data import add_to_date
-from erpnext.stock.doctype.delivery_note.test_delivery_note import (
- create_delivery_note,
- create_return_delivery_note,
-)
+from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs
+from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
create_landed_cost_voucher,
@@ -22,27 +23,35 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
create_stock_reconciliation,
)
from erpnext.stock.stock_ledger import get_previous_sle
-from erpnext.tests.utils import ERPNextTestCase
-class TestStockLedgerEntry(ERPNextTestCase):
+class TestStockLedgerEntry(FrappeTestCase):
def setUp(self):
items = create_items()
- reset('Stock Entry')
+ reset("Stock Entry")
# delete SLE and BINs for all items
- frappe.db.sql("delete from `tabStock Ledger Entry` where item_code in (%s)" % (', '.join(['%s']*len(items))), items)
- frappe.db.sql("delete from `tabBin` where item_code in (%s)" % (', '.join(['%s']*len(items))), items)
-
+ frappe.db.sql(
+ "delete from `tabStock Ledger Entry` where item_code in (%s)"
+ % (", ".join(["%s"] * len(items))),
+ items,
+ )
+ frappe.db.sql(
+ "delete from `tabBin` where item_code in (%s)" % (", ".join(["%s"] * len(items))), items
+ )
def assertSLEs(self, doc, expected_sles, sle_filters=None):
- """ Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
+ """Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
if sle_filters:
filters.update(sle_filters)
- sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters,
- order_by="timestamp(posting_date, posting_time), creation")
+ sles = frappe.get_all(
+ "Stock Ledger Entry",
+ fields=["*"],
+ filters=filters,
+ order_by="timestamp(posting_date, posting_time), creation",
+ )
for exp_sle, act_sle in zip(expected_sles, sles):
for k, v in exp_sle.items():
@@ -65,9 +74,11 @@ class TestStockLedgerEntry(ERPNextTestCase):
qty=50,
rate=100,
company=company,
- expense_account = "Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC",
- posting_date='2020-04-10',
- posting_time='14:00'
+ expense_account="Stock Adjustment - _TC"
+ if frappe.get_all("Stock Ledger Entry")
+ else "Temporary Opening - _TC",
+ posting_date="2020-04-10",
+ posting_time="14:00",
)
# _Test Item for Reposting at FG warehouse on 20-04-2020: Qty = 10, Rate = 200
@@ -77,9 +88,11 @@ class TestStockLedgerEntry(ERPNextTestCase):
qty=10,
rate=200,
company=company,
- expense_account="Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC",
- posting_date='2020-04-20',
- posting_time='14:00'
+ expense_account="Stock Adjustment - _TC"
+ if frappe.get_all("Stock Ledger Entry")
+ else "Temporary Opening - _TC",
+ posting_date="2020-04-20",
+ posting_time="14:00",
)
# _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020
@@ -89,28 +102,40 @@ class TestStockLedgerEntry(ERPNextTestCase):
target="Finished Goods - _TC",
company=company,
qty=10,
- expense_account="Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC",
- posting_date='2020-04-30',
- posting_time='14:00'
+ expense_account="Stock Adjustment - _TC"
+ if frappe.get_all("Stock Ledger Entry")
+ else "Temporary Opening - _TC",
+ posting_date="2020-04-30",
+ posting_time="14:00",
+ )
+ target_wh_sle = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "item_code": "_Test Item for Reposting",
+ "warehouse": "Finished Goods - _TC",
+ "voucher_type": "Stock Entry",
+ "voucher_no": se.name,
+ },
+ ["valuation_rate"],
+ as_dict=1,
)
- target_wh_sle = frappe.db.get_value('Stock Ledger Entry', {
- "item_code": "_Test Item for Reposting",
- "warehouse": "Finished Goods - _TC",
- "voucher_type": "Stock Entry",
- "voucher_no": se.name
- }, ["valuation_rate"], as_dict=1)
self.assertEqual(target_wh_sle.get("valuation_rate"), 150)
# Repack entry on 5-5-2020
- repack = create_repack_entry(company=company, posting_date='2020-05-05', posting_time='14:00')
+ repack = create_repack_entry(company=company, posting_date="2020-05-05", posting_time="14:00")
- finished_item_sle = frappe.db.get_value('Stock Ledger Entry', {
- "item_code": "_Test Finished Item for Reposting",
- "warehouse": "Finished Goods - _TC",
- "voucher_type": "Stock Entry",
- "voucher_no": repack.name
- }, ["incoming_rate", "valuation_rate"], as_dict=1)
+ finished_item_sle = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "item_code": "_Test Finished Item for Reposting",
+ "warehouse": "Finished Goods - _TC",
+ "voucher_type": "Stock Entry",
+ "voucher_no": repack.name,
+ },
+ ["incoming_rate", "valuation_rate"],
+ as_dict=1,
+ )
self.assertEqual(finished_item_sle.get("incoming_rate"), 540)
self.assertEqual(finished_item_sle.get("valuation_rate"), 540)
@@ -121,29 +146,37 @@ class TestStockLedgerEntry(ERPNextTestCase):
qty=50,
rate=150,
company=company,
- expense_account ="Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC",
- posting_date='2020-04-12',
- posting_time='14:00'
+ expense_account="Stock Adjustment - _TC"
+ if frappe.get_all("Stock Ledger Entry")
+ else "Temporary Opening - _TC",
+ posting_date="2020-04-12",
+ posting_time="14:00",
)
-
# Check valuation rate of finished goods warehouse after back-dated entry at Stores
- target_wh_sle = get_previous_sle({
- "item_code": "_Test Item for Reposting",
- "warehouse": "Finished Goods - _TC",
- "posting_date": '2020-04-30',
- "posting_time": '14:00'
- })
+ target_wh_sle = get_previous_sle(
+ {
+ "item_code": "_Test Item for Reposting",
+ "warehouse": "Finished Goods - _TC",
+ "posting_date": "2020-04-30",
+ "posting_time": "14:00",
+ }
+ )
self.assertEqual(target_wh_sle.get("incoming_rate"), 150)
self.assertEqual(target_wh_sle.get("valuation_rate"), 175)
# Check valuation rate of repacked item after back-dated entry at Stores
- finished_item_sle = frappe.db.get_value('Stock Ledger Entry', {
- "item_code": "_Test Finished Item for Reposting",
- "warehouse": "Finished Goods - _TC",
- "voucher_type": "Stock Entry",
- "voucher_no": repack.name
- }, ["incoming_rate", "valuation_rate"], as_dict=1)
+ finished_item_sle = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "item_code": "_Test Finished Item for Reposting",
+ "warehouse": "Finished Goods - _TC",
+ "voucher_type": "Stock Entry",
+ "voucher_no": repack.name,
+ },
+ ["incoming_rate", "valuation_rate"],
+ as_dict=1,
+ )
self.assertEqual(finished_item_sle.get("incoming_rate"), 790)
self.assertEqual(finished_item_sle.get("valuation_rate"), 790)
@@ -153,76 +186,133 @@ class TestStockLedgerEntry(ERPNextTestCase):
self.assertEqual(repack.items[1].get("basic_rate"), 750)
def test_purchase_return_valuation_reposting(self):
- pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-10',
- warehouse="Stores - _TC", item_code="_Test Item for Reposting", qty=5, rate=100)
+ pr = make_purchase_receipt(
+ company="_Test Company",
+ posting_date="2020-04-10",
+ warehouse="Stores - _TC",
+ item_code="_Test Item for Reposting",
+ qty=5,
+ rate=100,
+ )
- return_pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-15',
- warehouse="Stores - _TC", item_code="_Test Item for Reposting", is_return=1, return_against=pr.name, qty=-2)
+ return_pr = make_purchase_receipt(
+ company="_Test Company",
+ posting_date="2020-04-15",
+ warehouse="Stores - _TC",
+ item_code="_Test Item for Reposting",
+ is_return=1,
+ return_against=pr.name,
+ qty=-2,
+ )
# check sle
- outgoing_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
- "voucher_no": return_pr.name}, ["outgoing_rate", "stock_value_difference"])
+ outgoing_rate, stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": return_pr.name},
+ ["outgoing_rate", "stock_value_difference"],
+ )
self.assertEqual(outgoing_rate, 100)
self.assertEqual(stock_value_difference, -200)
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
- outgoing_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
- "voucher_no": return_pr.name}, ["outgoing_rate", "stock_value_difference"])
+ outgoing_rate, stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": return_pr.name},
+ ["outgoing_rate", "stock_value_difference"],
+ )
self.assertEqual(outgoing_rate, 110)
self.assertEqual(stock_value_difference, -220)
def test_sales_return_valuation_reposting(self):
company = "_Test Company"
- item_code="_Test Item for Reposting"
+ item_code = "_Test Item for Reposting"
# Purchase Return: Qty = 5, Rate = 100
- pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
- warehouse="Stores - _TC", item_code=item_code, qty=5, rate=100)
+ pr = make_purchase_receipt(
+ company=company,
+ posting_date="2020-04-10",
+ warehouse="Stores - _TC",
+ item_code=item_code,
+ qty=5,
+ rate=100,
+ )
- #Delivery Note: Qty = 5, Rate = 150
- dn = create_delivery_note(item_code=item_code, qty=5, rate=150, warehouse="Stores - _TC",
- company=company, expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
+ # Delivery Note: Qty = 5, Rate = 150
+ dn = create_delivery_note(
+ item_code=item_code,
+ qty=5,
+ rate=150,
+ warehouse="Stores - _TC",
+ company=company,
+ expense_account="Cost of Goods Sold - _TC",
+ cost_center="Main - _TC",
+ )
# check outgoing_rate for DN
- outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
- "voucher_no": dn.name}, "stock_value_difference") / 5)
+ outgoing_rate = abs(
+ frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": dn.name},
+ "stock_value_difference",
+ )
+ / 5
+ )
self.assertEqual(dn.items[0].incoming_rate, 100)
self.assertEqual(outgoing_rate, 100)
# Return Entry: Qty = -2, Rate = 150
- return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=item_code, qty=-2, rate=150,
- company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
+ return_dn = create_delivery_note(
+ is_return=1,
+ return_against=dn.name,
+ item_code=item_code,
+ qty=-2,
+ rate=150,
+ company=company,
+ warehouse="Stores - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ cost_center="Main - _TC",
+ )
# check incoming rate for Return entry
- incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ incoming_rate, stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
{"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
- ["incoming_rate", "stock_value_difference"])
+ ["incoming_rate", "stock_value_difference"],
+ )
self.assertEqual(return_dn.items[0].incoming_rate, 100)
self.assertEqual(incoming_rate, 100)
self.assertEqual(stock_value_difference, 200)
- #-------------------------------
+ # -------------------------------
# Landed Cost Voucher to update the rate of incoming Purchase Return: Additional cost = 50
lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
# check outgoing_rate for DN after reposting
- outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
- "voucher_no": dn.name}, "stock_value_difference") / 5)
+ outgoing_rate = abs(
+ frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": dn.name},
+ "stock_value_difference",
+ )
+ / 5
+ )
self.assertEqual(outgoing_rate, 110)
dn.reload()
self.assertEqual(dn.items[0].incoming_rate, 110)
# check incoming rate for Return entry after reposting
- incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ incoming_rate, stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
{"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
- ["incoming_rate", "stock_value_difference"])
+ ["incoming_rate", "stock_value_difference"],
+ )
self.assertEqual(incoming_rate, 110)
self.assertEqual(stock_value_difference, 220)
@@ -238,54 +328,93 @@ class TestStockLedgerEntry(ERPNextTestCase):
def test_reposting_of_sales_return_for_packed_item(self):
company = "_Test Company"
- packed_item_code="_Test Item for Reposting"
+ packed_item_code = "_Test Item for Reposting"
bundled_item = "_Test Bundled Item for Reposting"
create_product_bundle_item(bundled_item, [[packed_item_code, 4]])
# Purchase Return: Qty = 50, Rate = 100
- pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
- warehouse="Stores - _TC", item_code=packed_item_code, qty=50, rate=100)
+ pr = make_purchase_receipt(
+ company=company,
+ posting_date="2020-04-10",
+ warehouse="Stores - _TC",
+ item_code=packed_item_code,
+ qty=50,
+ rate=100,
+ )
- #Delivery Note: Qty = 5, Rate = 150
- dn = create_delivery_note(item_code=bundled_item, qty=5, rate=150, warehouse="Stores - _TC",
- company=company, expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
+ # Delivery Note: Qty = 5, Rate = 150
+ dn = create_delivery_note(
+ item_code=bundled_item,
+ qty=5,
+ rate=150,
+ warehouse="Stores - _TC",
+ company=company,
+ expense_account="Cost of Goods Sold - _TC",
+ cost_center="Main - _TC",
+ )
# check outgoing_rate for DN
- outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
- "voucher_no": dn.name}, "stock_value_difference") / 20)
+ outgoing_rate = abs(
+ frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": dn.name},
+ "stock_value_difference",
+ )
+ / 20
+ )
self.assertEqual(dn.packed_items[0].incoming_rate, 100)
self.assertEqual(outgoing_rate, 100)
# Return Entry: Qty = -2, Rate = 150
- return_dn = create_return_delivery_note(source_name=dn.name, rate=150, qty=-2)
+ return_dn = create_delivery_note(
+ is_return=1,
+ return_against=dn.name,
+ item_code=bundled_item,
+ qty=-2,
+ rate=150,
+ company=company,
+ warehouse="Stores - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ cost_center="Main - _TC",
+ )
# check incoming rate for Return entry
- incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ incoming_rate, stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
{"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
- ["incoming_rate", "stock_value_difference"])
+ ["incoming_rate", "stock_value_difference"],
+ )
self.assertEqual(return_dn.packed_items[0].incoming_rate, 100)
self.assertEqual(incoming_rate, 100)
self.assertEqual(stock_value_difference, 800)
- #-------------------------------
+ # -------------------------------
# Landed Cost Voucher to update the rate of incoming Purchase Return: Additional cost = 50
lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
# check outgoing_rate for DN after reposting
- outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
- "voucher_no": dn.name}, "stock_value_difference") / 20)
+ outgoing_rate = abs(
+ frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": dn.name},
+ "stock_value_difference",
+ )
+ / 20
+ )
self.assertEqual(outgoing_rate, 101)
dn.reload()
self.assertEqual(dn.packed_items[0].incoming_rate, 101)
# check incoming rate for Return entry after reposting
- incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ incoming_rate, stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
{"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
- ["incoming_rate", "stock_value_difference"])
+ ["incoming_rate", "stock_value_difference"],
+ )
self.assertEqual(incoming_rate, 101)
self.assertEqual(stock_value_difference, 808)
@@ -303,20 +432,35 @@ class TestStockLedgerEntry(ERPNextTestCase):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
company = "_Test Company"
- rm_item_code="_Test Item for Reposting"
+ rm_item_code = "_Test Item for Reposting"
subcontracted_item = "_Test Subcontracted Item for Reposting"
- frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
- make_bom(item = subcontracted_item, raw_materials =[rm_item_code], currency="INR")
+ frappe.db.set_value(
+ "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM"
+ )
+ make_bom(item=subcontracted_item, raw_materials=[rm_item_code], currency="INR")
# Purchase raw materials on supplier warehouse: Qty = 50, Rate = 100
- pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
- warehouse="Stores - _TC", item_code=rm_item_code, qty=10, rate=100)
+ pr = make_purchase_receipt(
+ company=company,
+ posting_date="2020-04-10",
+ warehouse="Stores - _TC",
+ item_code=rm_item_code,
+ qty=10,
+ rate=100,
+ )
# Purchase Receipt for subcontracted item
- pr1 = make_purchase_receipt(company=company, posting_date='2020-04-20',
- warehouse="Finished Goods - _TC", supplier_warehouse="Stores - _TC",
- item_code=subcontracted_item, qty=10, rate=20, is_subcontracted="Yes")
+ pr1 = make_purchase_receipt(
+ company=company,
+ posting_date="2020-04-20",
+ warehouse="Finished Goods - _TC",
+ supplier_warehouse="Stores - _TC",
+ item_code=subcontracted_item,
+ qty=10,
+ rate=20,
+ is_subcontracted="Yes",
+ )
self.assertEqual(pr1.items[0].valuation_rate, 120)
@@ -327,8 +471,11 @@ class TestStockLedgerEntry(ERPNextTestCase):
self.assertEqual(pr1.items[0].valuation_rate, 125)
# check outgoing_rate for DN after reposting
- incoming_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
- "voucher_no": pr1.name, "item_code": subcontracted_item}, "incoming_rate")
+ incoming_rate = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "item_code": subcontracted_item},
+ "incoming_rate",
+ )
self.assertEqual(incoming_rate, 125)
# cleanup data
@@ -338,8 +485,9 @@ class TestStockLedgerEntry(ERPNextTestCase):
def test_back_dated_entry_not_allowed(self):
# Back dated stock transactions are only allowed to stock managers
- frappe.db.set_value("Stock Settings", None,
- "role_allowed_to_create_edit_back_dated_transactions", "Stock Manager")
+ frappe.db.set_value(
+ "Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", "Stock Manager"
+ )
# Set User with Stock User role but not Stock Manager
try:
@@ -350,8 +498,13 @@ class TestStockLedgerEntry(ERPNextTestCase):
frappe.set_user(user.name)
stock_entry_on_today = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
- back_dated_se_1 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
- posting_date=add_days(today(), -1), do_not_submit=True)
+ back_dated_se_1 = make_stock_entry(
+ target="_Test Warehouse - _TC",
+ qty=10,
+ basic_rate=100,
+ posting_date=add_days(today(), -1),
+ do_not_submit=True,
+ )
# Block back-dated entry
self.assertRaises(BackDatedStockTransaction, back_dated_se_1.submit)
@@ -361,14 +514,17 @@ class TestStockLedgerEntry(ERPNextTestCase):
frappe.set_user(user.name)
# Back dated entry allowed to Stock Manager
- back_dated_se_2 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
- posting_date=add_days(today(), -1))
+ back_dated_se_2 = make_stock_entry(
+ target="_Test Warehouse - _TC", qty=10, basic_rate=100, posting_date=add_days(today(), -1)
+ )
back_dated_se_2.cancel()
stock_entry_on_today.cancel()
finally:
- frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None)
+ frappe.db.set_value(
+ "Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None
+ )
frappe.set_user("Administrator")
user.remove_roles("Stock Manager")
@@ -390,12 +546,12 @@ class TestStockLedgerEntry(ERPNextTestCase):
expected_queues = []
for idx, rate in enumerate(rates, start=1):
- expected_queues.append(
- {"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]}
- )
+ expected_queues.append({"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]})
self.assertSLEs(receipt, expected_queues)
- transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10)
+ transfer = make_stock_entry(
+ item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10
+ )
for rate in rates[1:]:
row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False)
transfer.append("items", row)
@@ -413,7 +569,9 @@ class TestStockLedgerEntry(ERPNextTestCase):
rates = [10 * i for i in range(1, 5)]
- receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10)
+ receipt = make_stock_entry(
+ item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10
+ )
for rate in rates[1:]:
row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
row.basic_rate = rate
@@ -422,26 +580,108 @@ class TestStockLedgerEntry(ERPNextTestCase):
receipt.save()
receipt.submit()
- repack = make_stock_entry(item_code=rm.name, source=warehouse, qty=10,
- do_not_save=True, rate=10, purpose="Repack")
+ repack = make_stock_entry(
+ item_code=rm.name, source=warehouse, qty=10, do_not_save=True, rate=10, purpose="Repack"
+ )
for rate in rates[1:]:
row = frappe.copy_doc(repack.items[0], ignore_no_copy=False)
repack.append("items", row)
- repack.append("items", {
- "item_code": packed.name,
- "t_warehouse": warehouse,
- "qty": 1,
- "transfer_qty": 1,
- })
+ repack.append(
+ "items",
+ {
+ "item_code": packed.name,
+ "t_warehouse": warehouse,
+ "qty": 1,
+ "transfer_qty": 1,
+ },
+ )
repack.save()
repack.submit()
# same exact queue should be transferred
- self.assertSLEs(repack, [
- {"incoming_rate": sum(rates) * 10}
- ], sle_filters={"item_code": packed.name})
+ self.assertSLEs(
+ repack, [{"incoming_rate": sum(rates) * 10}], sle_filters={"item_code": packed.name}
+ )
+
+ @change_settings("Stock Settings", {"allow_negative_stock": 1})
+ def test_negative_fifo_valuation(self):
+ """
+ When stock goes negative discard FIFO queue.
+ Only pervailing valuation rate should be used for making transactions in such cases.
+ """
+ item = make_item(properties={"allow_negative_stock": 1}).name
+ warehouse = "_Test Warehouse - _TC"
+
+ receipt = make_stock_entry(item_code=item, target=warehouse, qty=10, rate=10)
+ consume1 = make_stock_entry(item_code=item, source=warehouse, qty=15)
+
+ self.assertSLEs(consume1, [{"stock_value": -5 * 10, "stock_queue": [[-5, 10]]}])
+
+ consume2 = make_stock_entry(item_code=item, source=warehouse, qty=5)
+ self.assertSLEs(consume2, [{"stock_value": -10 * 10, "stock_queue": [[-10, 10]]}])
+
+ receipt2 = make_stock_entry(item_code=item, target=warehouse, qty=15, rate=15)
+ self.assertSLEs(receipt2, [{"stock_queue": [[5, 15]], "stock_value_difference": 175}])
+
+ def test_dependent_gl_entry_reposting(self):
+ def _get_stock_credit(doc):
+ return frappe.db.get_value(
+ "GL Entry",
+ {
+ "voucher_no": doc.name,
+ "voucher_type": doc.doctype,
+ "is_cancelled": 0,
+ "account": "Stock In Hand - TCP1",
+ },
+ "sum(credit)",
+ )
+
+ def _day(days):
+ return add_to_date(date=today(), days=days)
+
+ item = make_item().name
+ A = "Stores - TCP1"
+ B = "Work In Progress - TCP1"
+ C = "Finished Goods - TCP1"
+
+ make_stock_entry(item_code=item, to_warehouse=A, qty=5, rate=10, posting_date=_day(0))
+ make_stock_entry(item_code=item, from_warehouse=A, to_warehouse=B, qty=5, posting_date=_day(1))
+ depdendent_consumption = make_stock_entry(
+ item_code=item, from_warehouse=B, qty=5, posting_date=_day(2)
+ )
+ self.assertEqual(50, _get_stock_credit(depdendent_consumption))
+
+ # backdated receipt - should trigger GL repost of all previous stock entries
+ bd_receipt = make_stock_entry(
+ item_code=item, to_warehouse=A, qty=5, rate=20, posting_date=_day(-1)
+ )
+ self.assertEqual(100, _get_stock_credit(depdendent_consumption))
+
+ # cancelling receipt should reset it back
+ bd_receipt.cancel()
+ self.assertEqual(50, _get_stock_credit(depdendent_consumption))
+
+ bd_receipt2 = make_stock_entry(
+ item_code=item, to_warehouse=A, qty=2, rate=20, posting_date=_day(-2)
+ )
+ # total as per FIFO -> 2 * 20 + 3 * 10 = 70
+ self.assertEqual(70, _get_stock_credit(depdendent_consumption))
+
+ # transfer WIP material to final destination and consume it all
+ depdendent_consumption.cancel()
+ make_stock_entry(item_code=item, from_warehouse=B, to_warehouse=C, qty=5, posting_date=_day(3))
+ final_consumption = make_stock_entry(
+ item_code=item, from_warehouse=C, qty=5, posting_date=_day(4)
+ )
+ # exact amount gets consumed
+ self.assertEqual(70, _get_stock_credit(final_consumption))
+
+ # cancel original backdated receipt - should repost A -> B -> C
+ bd_receipt2.cancel()
+ # original amount
+ self.assertEqual(50, _get_stock_credit(final_consumption))
def create_repack_entry(**args):
@@ -451,51 +691,63 @@ def create_repack_entry(**args):
repack.company = args.company or "_Test Company"
repack.posting_date = args.posting_date
repack.set_posting_time = 1
- repack.append("items", {
- "item_code": "_Test Item for Reposting",
- "s_warehouse": "Stores - _TC",
- "qty": 5,
- "conversion_factor": 1,
- "expense_account": "Stock Adjustment - _TC",
- "cost_center": "Main - _TC"
- })
+ repack.append(
+ "items",
+ {
+ "item_code": "_Test Item for Reposting",
+ "s_warehouse": "Stores - _TC",
+ "qty": 5,
+ "conversion_factor": 1,
+ "expense_account": "Stock Adjustment - _TC",
+ "cost_center": "Main - _TC",
+ },
+ )
- repack.append("items", {
- "item_code": "_Test Finished Item for Reposting",
- "t_warehouse": "Finished Goods - _TC",
- "qty": 1,
- "conversion_factor": 1,
- "expense_account": "Stock Adjustment - _TC",
- "cost_center": "Main - _TC"
- })
+ repack.append(
+ "items",
+ {
+ "item_code": "_Test Finished Item for Reposting",
+ "t_warehouse": "Finished Goods - _TC",
+ "qty": 1,
+ "conversion_factor": 1,
+ "expense_account": "Stock Adjustment - _TC",
+ "cost_center": "Main - _TC",
+ },
+ )
- repack.append("additional_costs", {
- "expense_account": "Freight and Forwarding Charges - _TC",
- "description": "transport cost",
- "amount": 40
- })
+ repack.append(
+ "additional_costs",
+ {
+ "expense_account": "Freight and Forwarding Charges - _TC",
+ "description": "transport cost",
+ "amount": 40,
+ },
+ )
repack.save()
repack.submit()
return repack
+
def create_product_bundle_item(new_item_code, packed_items):
if not frappe.db.exists("Product Bundle", new_item_code):
item = frappe.new_doc("Product Bundle")
item.new_item_code = new_item_code
for d in packed_items:
- item.append("items", {
- "item_code": d[0],
- "qty": d[1]
- })
+ item.append("items", {"item_code": d[0], "qty": d[1]})
item.save()
+
def create_items():
- items = ["_Test Item for Reposting", "_Test Finished Item for Reposting",
- "_Test Subcontracted Item for Reposting", "_Test Bundled Item for Reposting"]
+ items = [
+ "_Test Item for Reposting",
+ "_Test Finished Item for Reposting",
+ "_Test Subcontracted Item for Reposting",
+ "_Test Bundled Item for Reposting",
+ ]
for d in items:
properties = {"valuation_method": "FIFO"}
if d == "_Test Bundled Item for Reposting":
@@ -506,3 +758,81 @@ def create_items():
make_item(d, properties=properties)
return items
+
+
+class TestDeferredNaming(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls) -> None:
+ super().setUpClass()
+ cls.gle_autoname = frappe.get_meta("GL Entry").autoname
+ cls.sle_autoname = frappe.get_meta("Stock Ledger Entry").autoname
+
+ def setUp(self) -> None:
+ self.item = make_item().name
+ self.warehouse = "Stores - TCP1"
+ self.company = "_Test Company with perpetual inventory"
+
+ def tearDown(self) -> None:
+ make_property_setter(
+ doctype="GL Entry",
+ for_doctype=True,
+ property="autoname",
+ value=self.gle_autoname,
+ property_type="Data",
+ fieldname=None,
+ )
+ make_property_setter(
+ doctype="Stock Ledger Entry",
+ for_doctype=True,
+ property="autoname",
+ value=self.sle_autoname,
+ property_type="Data",
+ fieldname=None,
+ )
+
+ # since deferred naming autocommits, commit all changes to avoid flake
+ frappe.db.commit() # nosemgrep
+
+ @staticmethod
+ def get_gle_sles(se):
+ filters = {"voucher_type": se.doctype, "voucher_no": se.name}
+ gle = set(frappe.get_list("GL Entry", filters, pluck="name"))
+ sle = set(frappe.get_list("Stock Ledger Entry", filters, pluck="name"))
+ return gle, sle
+
+ def test_deferred_naming(self):
+ se = make_stock_entry(
+ item_code=self.item, to_warehouse=self.warehouse, qty=10, rate=100, company=self.company
+ )
+
+ gle, sle = self.get_gle_sles(se)
+ rename_gle_sle_docs()
+ renamed_gle, renamed_sle = self.get_gle_sles(se)
+
+ self.assertFalse(gle & renamed_gle, msg="GLEs not renamed")
+ self.assertFalse(sle & renamed_sle, msg="SLEs not renamed")
+ se.cancel()
+
+ def test_hash_naming(self):
+ # disable naming series
+ for doctype in ("GL Entry", "Stock Ledger Entry"):
+ make_property_setter(
+ doctype=doctype,
+ for_doctype=True,
+ property="autoname",
+ value="hash",
+ property_type="Data",
+ fieldname=None,
+ )
+
+ se = make_stock_entry(
+ item_code=self.item, to_warehouse=self.warehouse, qty=10, rate=100, company=self.company
+ )
+
+ gle, sle = self.get_gle_sles(se)
+ rename_gle_sle_docs()
+ renamed_gle, renamed_sle = self.get_gle_sles(se)
+
+ self.assertEqual(gle, renamed_gle, msg="GLEs are renamed while using hash naming")
+ self.assertEqual(sle, renamed_sle, msg="SLEs are renamed while using hash naming")
+ se.cancel()
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 8f65287c4e8..16ca88bd810 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -14,8 +14,13 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.utils import get_stock_balance
-class OpeningEntryAccountError(frappe.ValidationError): pass
-class EmptyStockReconciliationItemsError(frappe.ValidationError): pass
+class OpeningEntryAccountError(frappe.ValidationError):
+ pass
+
+
+class EmptyStockReconciliationItemsError(frappe.ValidationError):
+ pass
+
class StockReconciliation(StockController):
def __init__(self, *args, **kwargs):
@@ -24,9 +29,11 @@ class StockReconciliation(StockController):
def validate(self):
if not self.expense_account:
- self.expense_account = frappe.get_cached_value('Company', self.company, "stock_adjustment_account")
+ self.expense_account = frappe.get_cached_value(
+ "Company", self.company, "stock_adjustment_account"
+ )
if not self.cost_center:
- self.cost_center = frappe.get_cached_value('Company', self.company, "cost_center")
+ self.cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
self.validate_posting_time()
self.remove_items_with_no_change()
self.validate_data()
@@ -37,8 +44,8 @@ class StockReconciliation(StockController):
self.set_total_qty_and_amount()
self.validate_putaway_capacity()
- if self._action=="submit":
- self.make_batches('warehouse')
+ if self._action == "submit":
+ self.make_batches("warehouse")
def on_submit(self):
self.update_stock_ledger()
@@ -46,10 +53,11 @@ class StockReconciliation(StockController):
self.repost_future_sle_and_gle()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
+
update_serial_nos_after_submit(self, "items")
def on_cancel(self):
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.make_sle_on_cancel()
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
@@ -57,13 +65,17 @@ class StockReconciliation(StockController):
def remove_items_with_no_change(self):
"""Remove items if qty or rate is not changed"""
self.difference_amount = 0.0
- def _changed(item):
- item_dict = get_stock_balance_for(item.item_code, item.warehouse,
- self.posting_date, self.posting_time, batch_no=item.batch_no)
- if ((item.qty is None or item.qty==item_dict.get("qty")) and
- (item.valuation_rate is None or item.valuation_rate==item_dict.get("rate")) and
- (not item.serial_no or (item.serial_no == item_dict.get("serial_nos")) )):
+ def _changed(item):
+ item_dict = get_stock_balance_for(
+ item.item_code, item.warehouse, self.posting_date, self.posting_time, batch_no=item.batch_no
+ )
+
+ if (
+ (item.qty is None or item.qty == item_dict.get("qty"))
+ and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate"))
+ and (not item.serial_no or (item.serial_no == item_dict.get("serial_nos")))
+ ):
return False
else:
# set default as current rates
@@ -80,16 +92,20 @@ class StockReconciliation(StockController):
item.current_qty = item_dict.get("qty")
item.current_valuation_rate = item_dict.get("rate")
- self.difference_amount += (flt(item.qty, item.precision("qty")) * \
- flt(item.valuation_rate or item_dict.get("rate"), item.precision("valuation_rate")) \
- - flt(item_dict.get("qty"), item.precision("qty")) * flt(item_dict.get("rate"), item.precision("valuation_rate")))
+ self.difference_amount += flt(item.qty, item.precision("qty")) * flt(
+ item.valuation_rate or item_dict.get("rate"), item.precision("valuation_rate")
+ ) - flt(item_dict.get("qty"), item.precision("qty")) * flt(
+ item_dict.get("rate"), item.precision("valuation_rate")
+ )
return True
items = list(filter(lambda d: _changed(d), self.items))
if not items:
- frappe.throw(_("None of the items have any change in quantity or value."),
- EmptyStockReconciliationItemsError)
+ frappe.throw(
+ _("None of the items have any change in quantity or value."),
+ EmptyStockReconciliationItemsError,
+ )
elif len(items) != len(self.items):
self.items = items
@@ -99,7 +115,7 @@ class StockReconciliation(StockController):
def validate_data(self):
def _get_msg(row_num, msg):
- return _("Row # {0}:").format(row_num+1) + " " + msg
+ return _("Row # {0}:").format(row_num + 1) + " " + msg
self.validation_messages = []
item_warehouse_combinations = []
@@ -109,7 +125,7 @@ class StockReconciliation(StockController):
for row_num, row in enumerate(self.items):
# find duplicates
key = [row.item_code, row.warehouse]
- for field in ['serial_no', 'batch_no']:
+ for field in ["serial_no", "batch_no"]:
if row.get(field):
key.append(row.get(field))
@@ -126,32 +142,35 @@ class StockReconciliation(StockController):
# if both not specified
if row.qty in ["", None] and row.valuation_rate in ["", None]:
- self.validation_messages.append(_get_msg(row_num,
- _("Please specify either Quantity or Valuation Rate or both")))
+ self.validation_messages.append(
+ _get_msg(row_num, _("Please specify either Quantity or Valuation Rate or both"))
+ )
# do not allow negative quantity
if flt(row.qty) < 0:
- self.validation_messages.append(_get_msg(row_num,
- _("Negative Quantity is not allowed")))
+ self.validation_messages.append(_get_msg(row_num, _("Negative Quantity is not allowed")))
# do not allow negative valuation
if flt(row.valuation_rate) < 0:
- self.validation_messages.append(_get_msg(row_num,
- _("Negative Valuation Rate is not allowed")))
+ self.validation_messages.append(_get_msg(row_num, _("Negative Valuation Rate is not allowed")))
if row.qty and row.valuation_rate in ["", None]:
- row.valuation_rate = get_stock_balance(row.item_code, row.warehouse,
- self.posting_date, self.posting_time, with_valuation_rate=True)[1]
+ row.valuation_rate = get_stock_balance(
+ row.item_code, row.warehouse, self.posting_date, self.posting_time, with_valuation_rate=True
+ )[1]
if not row.valuation_rate:
# try if there is a buying price list in default currency
- buying_rate = frappe.db.get_value("Item Price", {"item_code": row.item_code,
- "buying": 1, "currency": default_currency}, "price_list_rate")
+ buying_rate = frappe.db.get_value(
+ "Item Price",
+ {"item_code": row.item_code, "buying": 1, "currency": default_currency},
+ "price_list_rate",
+ )
if buying_rate:
row.valuation_rate = buying_rate
else:
# get valuation rate from Item
- row.valuation_rate = frappe.get_value('Item', row.item_code, 'valuation_rate')
+ row.valuation_rate = frappe.get_value("Item", row.item_code, "valuation_rate")
# throw all validation messages
if self.validation_messages:
@@ -178,7 +197,9 @@ class StockReconciliation(StockController):
# item should not be serialized
if item.has_serial_no and not row.serial_no and not item.serial_no_series:
- raise frappe.ValidationError(_("Serial no(s) required for serialized item {0}").format(item_code))
+ raise frappe.ValidationError(
+ _("Serial no(s) required for serialized item {0}").format(item_code)
+ )
# item managed batch-wise not allowed
if item.has_batch_no and not row.batch_no and not item.create_new_batch:
@@ -191,8 +212,8 @@ class StockReconciliation(StockController):
self.validation_messages.append(_("Row #") + " " + ("%d: " % (row.idx)) + cstr(e))
def update_stock_ledger(self):
- """ find difference between current and expected entries
- and create stock ledger entries based on the difference"""
+ """find difference between current and expected entries
+ and create stock ledger entries based on the difference"""
from erpnext.stock.stock_ledger import get_previous_sle
sl_entries = []
@@ -208,15 +229,20 @@ class StockReconciliation(StockController):
self.get_sle_for_serialized_items(row, sl_entries)
else:
if row.serial_no or row.batch_no:
- frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \
- .format(row.idx, frappe.bold(row.item_code)))
+ frappe.throw(
+ _(
+ "Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it."
+ ).format(row.idx, frappe.bold(row.item_code))
+ )
- previous_sle = get_previous_sle({
- "item_code": row.item_code,
- "warehouse": row.warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time
- })
+ previous_sle = get_previous_sle(
+ {
+ "item_code": row.item_code,
+ "warehouse": row.warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ }
+ )
if previous_sle:
if row.qty in ("", None):
@@ -226,12 +252,16 @@ class StockReconciliation(StockController):
row.valuation_rate = previous_sle.get("valuation_rate", 0)
if row.qty and not row.valuation_rate and not row.allow_zero_valuation_rate:
- frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx))
+ frappe.throw(
+ _("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx)
+ )
- if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction")
- and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0))
- or (not previous_sle and not row.qty)):
- continue
+ if (
+ previous_sle
+ and row.qty == previous_sle.get("qty_after_transaction")
+ and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0)
+ ) or (not previous_sle and not row.qty):
+ continue
sl_entries.append(self.get_sle_for_items(row))
@@ -253,21 +283,24 @@ class StockReconciliation(StockController):
serial_nos = get_serial_nos(row.serial_no)
-
# To issue existing serial nos
if row.current_qty and (row.current_serial_no or row.batch_no):
args = self.get_sle_for_items(row)
- args.update({
- 'actual_qty': -1 * row.current_qty,
- 'serial_no': row.current_serial_no,
- 'batch_no': row.batch_no,
- 'valuation_rate': row.current_valuation_rate
- })
+ args.update(
+ {
+ "actual_qty": -1 * row.current_qty,
+ "serial_no": row.current_serial_no,
+ "batch_no": row.batch_no,
+ "valuation_rate": row.current_valuation_rate,
+ }
+ )
if row.current_serial_no:
- args.update({
- 'qty_after_transaction': 0,
- })
+ args.update(
+ {
+ "qty_after_transaction": 0,
+ }
+ )
sl_entries.append(args)
@@ -275,42 +308,49 @@ class StockReconciliation(StockController):
for serial_no in serial_nos:
args = self.get_sle_for_items(row, [serial_no])
- previous_sle = get_previous_sle({
- "item_code": row.item_code,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "serial_no": serial_no
- })
+ previous_sle = get_previous_sle(
+ {
+ "item_code": row.item_code,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "serial_no": serial_no,
+ }
+ )
if previous_sle and row.warehouse != previous_sle.get("warehouse"):
# If serial no exists in different warehouse
- warehouse = previous_sle.get("warehouse", '') or row.warehouse
+ warehouse = previous_sle.get("warehouse", "") or row.warehouse
if not qty_after_transaction:
- qty_after_transaction = get_stock_balance(row.item_code,
- warehouse, self.posting_date, self.posting_time)
+ qty_after_transaction = get_stock_balance(
+ row.item_code, warehouse, self.posting_date, self.posting_time
+ )
qty_after_transaction -= 1
new_args = args.copy()
- new_args.update({
- 'actual_qty': -1,
- 'qty_after_transaction': qty_after_transaction,
- 'warehouse': warehouse,
- 'valuation_rate': previous_sle.get("valuation_rate")
- })
+ new_args.update(
+ {
+ "actual_qty": -1,
+ "qty_after_transaction": qty_after_transaction,
+ "warehouse": warehouse,
+ "valuation_rate": previous_sle.get("valuation_rate"),
+ }
+ )
sl_entries.append(new_args)
if row.qty:
args = self.get_sle_for_items(row)
- args.update({
- 'actual_qty': row.qty,
- 'incoming_rate': row.valuation_rate,
- 'valuation_rate': row.valuation_rate
- })
+ args.update(
+ {
+ "actual_qty": row.qty,
+ "incoming_rate": row.valuation_rate,
+ "valuation_rate": row.valuation_rate,
+ }
+ )
sl_entries.append(args)
@@ -320,7 +360,8 @@ class StockReconciliation(StockController):
def update_valuation_rate_for_serial_no(self):
for d in self.items:
- if not d.serial_no: continue
+ if not d.serial_no:
+ continue
serial_nos = get_serial_nos(d.serial_no)
self.update_valuation_rate_for_serial_nos(d, serial_nos)
@@ -331,7 +372,7 @@ class StockReconciliation(StockController):
return
for d in serial_nos:
- frappe.db.set_value("Serial No", d, 'purchase_rate', valuation_rate)
+ frappe.db.set_value("Serial No", d, "purchase_rate", valuation_rate)
def get_sle_for_items(self, row, serial_nos=None):
"""Insert Stock Ledger Entries"""
@@ -339,22 +380,24 @@ class StockReconciliation(StockController):
if not serial_nos and row.serial_no:
serial_nos = get_serial_nos(row.serial_no)
- data = frappe._dict({
- "doctype": "Stock Ledger Entry",
- "item_code": row.item_code,
- "warehouse": row.warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "voucher_type": self.doctype,
- "voucher_no": self.name,
- "voucher_detail_no": row.name,
- "company": self.company,
- "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"),
- "is_cancelled": 1 if self.docstatus == 2 else 0,
- "serial_no": '\n'.join(serial_nos) if serial_nos else '',
- "batch_no": row.batch_no,
- "valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate"))
- })
+ data = frappe._dict(
+ {
+ "doctype": "Stock Ledger Entry",
+ "item_code": row.item_code,
+ "warehouse": row.warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "voucher_detail_no": row.name,
+ "company": self.company,
+ "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"),
+ "is_cancelled": 1 if self.docstatus == 2 else 0,
+ "serial_no": "\n".join(serial_nos) if serial_nos else "",
+ "batch_no": row.batch_no,
+ "valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate")),
+ }
+ )
if not row.batch_no:
data.qty_after_transaction = flt(row.qty, row.precision("qty"))
@@ -382,7 +425,7 @@ class StockReconciliation(StockController):
for row in self.items:
if row.serial_no or row.batch_no or row.current_serial_no:
has_serial_no = True
- serial_nos = ''
+ serial_nos = ""
if row.current_serial_no:
serial_nos = get_serial_nos(row.current_serial_no)
@@ -395,10 +438,11 @@ class StockReconciliation(StockController):
sl_entries = self.merge_similar_item_serial_nos(sl_entries)
sl_entries.reverse()
- allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
+ allow_negative_stock = cint(
+ frappe.db.get_single_value("Stock Settings", "allow_negative_stock")
+ )
self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock)
-
def merge_similar_item_serial_nos(self, sl_entries):
# If user has put the same item in multiple row with different serial no
new_sl_entries = []
@@ -411,16 +455,16 @@ class StockReconciliation(StockController):
key = (d.item_code, d.warehouse)
if key not in merge_similar_entries:
- d.total_amount = (d.actual_qty * d.valuation_rate)
+ d.total_amount = d.actual_qty * d.valuation_rate
merge_similar_entries[key] = d
elif d.serial_no:
data = merge_similar_entries[key]
data.actual_qty += d.actual_qty
data.qty_after_transaction += d.qty_after_transaction
- data.total_amount += (d.actual_qty * d.valuation_rate)
+ data.total_amount += d.actual_qty * d.valuation_rate
data.valuation_rate = (data.total_amount) / data.actual_qty
- data.serial_no += '\n' + d.serial_no
+ data.serial_no += "\n" + d.serial_no
data.incoming_rate = (data.total_amount) / data.actual_qty
@@ -433,8 +477,9 @@ class StockReconciliation(StockController):
if not self.cost_center:
msgprint(_("Please enter Cost Center"), raise_exception=1)
- return super(StockReconciliation, self).get_gl_entries(warehouse_account,
- self.expense_account, self.cost_center)
+ return super(StockReconciliation, self).get_gl_entries(
+ warehouse_account, self.expense_account, self.cost_center
+ )
def validate_expense_account(self):
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
@@ -442,29 +487,39 @@ class StockReconciliation(StockController):
if not self.expense_account:
frappe.throw(_("Please enter Expense Account"))
- elif self.purpose == "Opening Stock" or not frappe.db.sql("""select name from `tabStock Ledger Entry` limit 1"""):
+ elif self.purpose == "Opening Stock" or not frappe.db.sql(
+ """select name from `tabStock Ledger Entry` limit 1"""
+ ):
if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss":
- frappe.throw(_("Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry"), OpeningEntryAccountError)
+ frappe.throw(
+ _(
+ "Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry"
+ ),
+ OpeningEntryAccountError,
+ )
def set_zero_value_for_customer_provided_items(self):
changed_any_values = False
- for d in self.get('items'):
- is_customer_item = frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item')
+ for d in self.get("items"):
+ is_customer_item = frappe.db.get_value("Item", d.item_code, "is_customer_provided_item")
if is_customer_item and d.valuation_rate:
d.valuation_rate = 0.0
changed_any_values = True
if changed_any_values:
- msgprint(_("Valuation rate for customer provided items has been set to zero."),
- title=_("Note"), indicator="blue")
-
+ msgprint(
+ _("Valuation rate for customer provided items has been set to zero."),
+ title=_("Note"),
+ indicator="blue",
+ )
def set_total_qty_and_amount(self):
for d in self.get("items"):
d.amount = flt(d.qty, d.precision("qty")) * flt(d.valuation_rate, d.precision("valuation_rate"))
- d.current_amount = (flt(d.current_qty,
- d.precision("current_qty")) * flt(d.current_valuation_rate, d.precision("current_valuation_rate")))
+ d.current_amount = flt(d.current_qty, d.precision("current_qty")) * flt(
+ d.current_valuation_rate, d.precision("current_valuation_rate")
+ )
d.quantity_difference = flt(d.qty) - flt(d.current_qty)
d.amount_difference = flt(d.amount) - flt(d.current_amount)
@@ -476,25 +531,33 @@ class StockReconciliation(StockController):
def submit(self):
if len(self.items) > 100:
- msgprint(_("The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage"))
- self.queue_action('submit', timeout=2000)
+ msgprint(
+ _(
+ "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage"
+ )
+ )
+ self.queue_action("submit", timeout=2000)
else:
self._submit()
def cancel(self):
if len(self.items) > 100:
- msgprint(_("The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage"))
- self.queue_action('cancel', timeout=2000)
+ msgprint(
+ _(
+ "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage"
+ )
+ )
+ self.queue_action("cancel", timeout=2000)
else:
self._cancel()
+
@frappe.whitelist()
-def get_items(warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False):
+def get_items(
+ warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False
+):
ignore_empty_stock = cint(ignore_empty_stock)
- items = [frappe._dict({
- 'item_code': item_code,
- 'warehouse': warehouse
- })]
+ items = [frappe._dict({"item_code": item_code, "warehouse": warehouse})]
if not item_code:
items = get_items_for_stock_reco(warehouse, company)
@@ -504,8 +567,9 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig
for d in items:
if d.item_code in itemwise_batch_data:
- valuation_rate = get_stock_balance(d.item_code, d.warehouse,
- posting_date, posting_time, with_valuation_rate=True)[1]
+ valuation_rate = get_stock_balance(
+ d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True
+ )[1]
for row in itemwise_batch_data.get(d.item_code):
if ignore_empty_stock and not row.qty:
@@ -514,12 +578,22 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig
args = get_item_data(row, row.qty, valuation_rate)
res.append(args)
else:
- stock_bal = get_stock_balance(d.item_code, d.warehouse, posting_date, posting_time,
- with_valuation_rate=True , with_serial_no=cint(d.has_serial_no))
- qty, valuation_rate, serial_no = stock_bal[0], stock_bal[1], stock_bal[2] if cint(d.has_serial_no) else ''
+ stock_bal = get_stock_balance(
+ d.item_code,
+ d.warehouse,
+ posting_date,
+ posting_time,
+ with_valuation_rate=True,
+ with_serial_no=cint(d.has_serial_no),
+ )
+ qty, valuation_rate, serial_no = (
+ stock_bal[0],
+ stock_bal[1],
+ stock_bal[2] if cint(d.has_serial_no) else "",
+ )
if ignore_empty_stock and not stock_bal[0]:
- continue
+ continue
args = get_item_data(d, qty, valuation_rate, serial_no)
@@ -527,9 +601,11 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig
return res
+
def get_items_for_stock_reco(warehouse, company):
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
- items = frappe.db.sql(f"""
+ items = frappe.db.sql(
+ f"""
select
i.name as item_code, i.item_name, bin.warehouse as warehouse, i.has_serial_no, i.has_batch_no
from
@@ -542,9 +618,12 @@ def get_items_for_stock_reco(warehouse, company):
and exists(
select name from `tabWarehouse` where lft >= {lft} and rgt <= {rgt} and name = bin.warehouse
)
- """, as_dict=1)
+ """,
+ as_dict=1,
+ )
- items += frappe.db.sql("""
+ items += frappe.db.sql(
+ """
select
i.name as item_code, i.item_name, id.default_warehouse as warehouse, i.has_serial_no, i.has_batch_no
from
@@ -559,40 +638,50 @@ def get_items_for_stock_reco(warehouse, company):
and IFNULL(i.disabled, 0) = 0
and id.company = %s
group by i.name
- """, (lft, rgt, company), as_dict=1)
+ """,
+ (lft, rgt, company),
+ as_dict=1,
+ )
# remove duplicates
# check if item-warehouse key extracted from each entry exists in set iw_keys
# and update iw_keys
iw_keys = set()
- items = [item for item in items if [(item.item_code, item.warehouse) not in iw_keys, iw_keys.add((item.item_code, item.warehouse))][0]]
+ items = [
+ item
+ for item in items
+ if [
+ (item.item_code, item.warehouse) not in iw_keys,
+ iw_keys.add((item.item_code, item.warehouse)),
+ ][0]
+ ]
return items
+
def get_item_data(row, qty, valuation_rate, serial_no=None):
return {
- 'item_code': row.item_code,
- 'warehouse': row.warehouse,
- 'qty': qty,
- 'item_name': row.item_name,
- 'valuation_rate': valuation_rate,
- 'current_qty': qty,
- 'current_valuation_rate': valuation_rate,
- 'current_serial_no': serial_no,
- 'serial_no': serial_no,
- 'batch_no': row.get('batch_no')
+ "item_code": row.item_code,
+ "warehouse": row.warehouse,
+ "qty": qty,
+ "item_name": row.item_name,
+ "valuation_rate": valuation_rate,
+ "current_qty": qty,
+ "current_valuation_rate": valuation_rate,
+ "current_serial_no": serial_no,
+ "serial_no": serial_no,
+ "batch_no": row.get("batch_no"),
}
+
def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
from erpnext.stock.report.batch_wise_balance_history.batch_wise_balance_history import execute
+
itemwise_batch_data = {}
- filters = frappe._dict({
- 'warehouse': warehouse,
- 'from_date': posting_date,
- 'to_date': posting_date,
- 'company': company
- })
+ filters = frappe._dict(
+ {"warehouse": warehouse, "from_date": posting_date, "to_date": posting_date, "company": company}
+ )
if item_code:
filters.item_code = item_code
@@ -600,23 +689,28 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
columns, data = execute(filters)
for row in data:
- itemwise_batch_data.setdefault(row[0], []).append(frappe._dict({
- 'item_code': row[0],
- 'warehouse': warehouse,
- 'qty': row[8],
- 'item_name': row[1],
- 'batch_no': row[4]
- }))
+ itemwise_batch_data.setdefault(row[0], []).append(
+ frappe._dict(
+ {
+ "item_code": row[0],
+ "warehouse": warehouse,
+ "qty": row[8],
+ "item_name": row[1],
+ "batch_no": row[4],
+ }
+ )
+ )
return itemwise_batch_data
-@frappe.whitelist()
-def get_stock_balance_for(item_code, warehouse,
- posting_date, posting_time, batch_no=None, with_valuation_rate= True):
- frappe.has_permission("Stock Reconciliation", "write", throw = True)
- item_dict = frappe.db.get_value("Item", item_code,
- ["has_serial_no", "has_batch_no"], as_dict=1)
+@frappe.whitelist()
+def get_stock_balance_for(
+ item_code, warehouse, posting_date, posting_time, batch_no=None, with_valuation_rate=True
+):
+ frappe.has_permission("Stock Reconciliation", "write", throw=True)
+
+ item_dict = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
if not item_dict:
# In cases of data upload to Items table
@@ -625,8 +719,14 @@ def get_stock_balance_for(item_code, warehouse,
serial_nos = ""
with_serial_no = True if item_dict.get("has_serial_no") else False
- data = get_stock_balance(item_code, warehouse, posting_date, posting_time,
- with_valuation_rate=with_valuation_rate, with_serial_no=with_serial_no)
+ data = get_stock_balance(
+ item_code,
+ warehouse,
+ posting_date,
+ posting_time,
+ with_valuation_rate=with_valuation_rate,
+ with_serial_no=with_serial_no,
+ )
if with_serial_no:
qty, rate, serial_nos = data
@@ -634,20 +734,20 @@ def get_stock_balance_for(item_code, warehouse,
qty, rate = data
if item_dict.get("has_batch_no"):
- qty = get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0
+ qty = (
+ get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0
+ )
+
+ return {"qty": qty, "rate": rate, "serial_nos": serial_nos}
- return {
- 'qty': qty,
- 'rate': rate,
- 'serial_nos': serial_nos
- }
@frappe.whitelist()
def get_difference_account(purpose, company):
- if purpose == 'Stock Reconciliation':
+ if purpose == "Stock Reconciliation":
account = get_company_default(company, "stock_adjustment_account")
else:
- account = frappe.db.get_value('Account', {'is_group': 0,
- 'company': company, 'account_type': 'Temporary'}, 'name')
+ account = frappe.db.get_value(
+ "Account", {"is_group": 0, "company": company, "account_type": "Temporary"}, "name"
+ )
return account
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 86af0a0cf3b..cb9b5abaa23 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -6,10 +6,11 @@
import frappe
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string
from erpnext.accounts.utils import get_stock_and_account_balance
-from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
@@ -19,10 +20,9 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method
-from erpnext.tests.utils import ERPNextTestCase, change_settings
-class TestStockReconciliation(ERPNextTestCase):
+class TestStockReconciliation(FrappeTestCase):
@classmethod
def setUpClass(cls):
create_batch_or_serial_no_items()
@@ -30,8 +30,7 @@ class TestStockReconciliation(ERPNextTestCase):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
def tearDown(self):
- frappe.flags.dont_execute_stock_reposts = None
-
+ frappe.local.future_sle = {}
def test_reco_for_fifo(self):
self._test_reco_sle_gle("FIFO")
@@ -40,55 +39,73 @@ class TestStockReconciliation(ERPNextTestCase):
self._test_reco_sle_gle("Moving Average")
def _test_reco_sle_gle(self, valuation_method):
- se1, se2, se3 = insert_existing_sle(warehouse='Stores - TCP1')
- company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
+ item_code = make_item(properties={"valuation_method": valuation_method}).name
+
+ se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1", item_code=item_code)
+ company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
# [[qty, valuation_rate, posting_date,
- # posting_time, expected_stock_value, bin_qty, bin_valuation]]
+ # posting_time, expected_stock_value, bin_qty, bin_valuation]]
input_data = [
[50, 1000, "2012-12-26", "12:00"],
[25, 900, "2012-12-26", "12:00"],
["", 1000, "2012-12-20", "12:05"],
[20, "", "2012-12-26", "12:05"],
- [0, "", "2012-12-31", "12:10"]
+ [0, "", "2012-12-31", "12:10"],
]
for d in input_data:
- set_valuation_method("_Test Item", valuation_method)
-
- last_sle = get_previous_sle({
- "item_code": "_Test Item",
- "warehouse": "Stores - TCP1",
- "posting_date": d[2],
- "posting_time": d[3]
- })
+ last_sle = get_previous_sle(
+ {
+ "item_code": item_code,
+ "warehouse": "Stores - TCP1",
+ "posting_date": d[2],
+ "posting_time": d[3],
+ }
+ )
# submit stock reconciliation
- stock_reco = create_stock_reconciliation(qty=d[0], rate=d[1],
- posting_date=d[2], posting_time=d[3], warehouse="Stores - TCP1",
- company=company, expense_account = "Stock Adjustment - TCP1")
+ stock_reco = create_stock_reconciliation(
+ item_code=item_code,
+ qty=d[0],
+ rate=d[1],
+ posting_date=d[2],
+ posting_time=d[3],
+ warehouse="Stores - TCP1",
+ company=company,
+ expense_account="Stock Adjustment - TCP1",
+ )
# check stock value
- sle = frappe.db.sql("""select * from `tabStock Ledger Entry`
- where voucher_type='Stock Reconciliation' and voucher_no=%s""", stock_reco.name, as_dict=1)
+ sle = frappe.db.sql(
+ """select * from `tabStock Ledger Entry`
+ where voucher_type='Stock Reconciliation' and voucher_no=%s""",
+ stock_reco.name,
+ as_dict=1,
+ )
qty_after_transaction = flt(d[0]) if d[0] != "" else flt(last_sle.get("qty_after_transaction"))
valuation_rate = flt(d[1]) if d[1] != "" else flt(last_sle.get("valuation_rate"))
- if qty_after_transaction == last_sle.get("qty_after_transaction") \
- and valuation_rate == last_sle.get("valuation_rate"):
- self.assertFalse(sle)
+ if qty_after_transaction == last_sle.get(
+ "qty_after_transaction"
+ ) and valuation_rate == last_sle.get("valuation_rate"):
+ self.assertFalse(sle)
else:
self.assertEqual(flt(sle[0].qty_after_transaction, 1), flt(qty_after_transaction, 1))
self.assertEqual(flt(sle[0].stock_value, 1), flt(qty_after_transaction * valuation_rate, 1))
# no gl entries
- self.assertTrue(frappe.db.get_value("Stock Ledger Entry",
- {"voucher_type": "Stock Reconciliation", "voucher_no": stock_reco.name}))
+ self.assertTrue(
+ frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_type": "Stock Reconciliation", "voucher_no": stock_reco.name}
+ )
+ )
- acc_bal, stock_bal, wh_list = get_stock_and_account_balance("Stock In Hand - TCP1",
- stock_reco.posting_date, stock_reco.company)
+ acc_bal, stock_bal, wh_list = get_stock_and_account_balance(
+ "Stock In Hand - TCP1", stock_reco.posting_date, stock_reco.company
+ )
self.assertEqual(flt(acc_bal, 1), flt(stock_bal, 1))
stock_reco.cancel()
@@ -98,18 +115,33 @@ class TestStockReconciliation(ERPNextTestCase):
se1.cancel()
def test_get_items(self):
- create_warehouse("_Test Warehouse Group 1",
- {"is_group": 1, "company": "_Test Company", "parent_warehouse": "All Warehouses - _TC"})
- create_warehouse("_Test Warehouse Ledger 1",
- {"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC", "company": "_Test Company"})
+ create_warehouse(
+ "_Test Warehouse Group 1",
+ {"is_group": 1, "company": "_Test Company", "parent_warehouse": "All Warehouses - _TC"},
+ )
+ create_warehouse(
+ "_Test Warehouse Ledger 1",
+ {
+ "is_group": 0,
+ "parent_warehouse": "_Test Warehouse Group 1 - _TC",
+ "company": "_Test Company",
+ },
+ )
- create_item("_Test Stock Reco Item", is_stock_item=1, valuation_rate=100,
- warehouse="_Test Warehouse Ledger 1 - _TC", opening_stock=100)
+ create_item(
+ "_Test Stock Reco Item",
+ is_stock_item=1,
+ valuation_rate=100,
+ warehouse="_Test Warehouse Ledger 1 - _TC",
+ opening_stock=100,
+ )
items = get_items("_Test Warehouse Group 1 - _TC", nowdate(), nowtime(), "_Test Company")
- self.assertEqual(["_Test Stock Reco Item", "_Test Warehouse Ledger 1 - _TC", 100],
- [items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]])
+ self.assertEqual(
+ ["_Test Stock Reco Item", "_Test Warehouse Ledger 1 - _TC", 100],
+ [items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]],
+ )
def test_stock_reco_for_serialized_item(self):
to_delete_records = []
@@ -119,8 +151,9 @@ class TestStockReconciliation(ERPNextTestCase):
serial_item_code = "Stock-Reco-Serial-Item-1"
serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC"
- sr = create_stock_reconciliation(item_code=serial_item_code,
- warehouse = serial_warehouse, qty=5, rate=200)
+ sr = create_stock_reconciliation(
+ item_code=serial_item_code, warehouse=serial_warehouse, qty=5, rate=200
+ )
serial_nos = get_serial_nos(sr.items[0].serial_no)
self.assertEqual(len(serial_nos), 5)
@@ -130,7 +163,7 @@ class TestStockReconciliation(ERPNextTestCase):
"warehouse": serial_warehouse,
"posting_date": nowdate(),
"posting_time": nowtime(),
- "serial_no": sr.items[0].serial_no
+ "serial_no": sr.items[0].serial_no,
}
valuation_rate = get_incoming_rate(args)
@@ -138,8 +171,9 @@ class TestStockReconciliation(ERPNextTestCase):
to_delete_records.append(sr.name)
- sr = create_stock_reconciliation(item_code=serial_item_code,
- warehouse = serial_warehouse, qty=5, rate=300)
+ sr = create_stock_reconciliation(
+ item_code=serial_item_code, warehouse=serial_warehouse, qty=5, rate=300
+ )
serial_nos1 = get_serial_nos(sr.items[0].serial_no)
self.assertEqual(len(serial_nos1), 5)
@@ -149,7 +183,7 @@ class TestStockReconciliation(ERPNextTestCase):
"warehouse": serial_warehouse,
"posting_date": nowdate(),
"posting_time": nowtime(),
- "serial_no": sr.items[0].serial_no
+ "serial_no": sr.items[0].serial_no,
}
valuation_rate = get_incoming_rate(args)
@@ -162,7 +196,6 @@ class TestStockReconciliation(ERPNextTestCase):
stock_doc = frappe.get_doc("Stock Reconciliation", d)
stock_doc.cancel()
-
def test_stock_reco_for_merge_serialized_item(self):
to_delete_records = []
@@ -170,23 +203,34 @@ class TestStockReconciliation(ERPNextTestCase):
serial_item_code = "Stock-Reco-Serial-Item-2"
serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC"
- sr = create_stock_reconciliation(item_code=serial_item_code, serial_no=random_string(6),
- warehouse = serial_warehouse, qty=1, rate=100, do_not_submit=True, purpose='Opening Stock')
+ sr = create_stock_reconciliation(
+ item_code=serial_item_code,
+ serial_no=random_string(6),
+ warehouse=serial_warehouse,
+ qty=1,
+ rate=100,
+ do_not_submit=True,
+ purpose="Opening Stock",
+ )
for i in range(3):
- sr.append('items', {
- 'item_code': serial_item_code,
- 'warehouse': serial_warehouse,
- 'qty': 1,
- 'valuation_rate': 100,
- 'serial_no': random_string(6)
- })
+ sr.append(
+ "items",
+ {
+ "item_code": serial_item_code,
+ "warehouse": serial_warehouse,
+ "qty": 1,
+ "valuation_rate": 100,
+ "serial_no": random_string(6),
+ },
+ )
sr.save()
sr.submit()
- sle_entries = frappe.get_all('Stock Ledger Entry', filters= {'voucher_no': sr.name},
- fields = ['name', 'incoming_rate'])
+ sle_entries = frappe.get_all(
+ "Stock Ledger Entry", filters={"voucher_no": sr.name}, fields=["name", "incoming_rate"]
+ )
self.assertEqual(len(sle_entries), 1)
self.assertEqual(sle_entries[0].incoming_rate, 100)
@@ -206,16 +250,18 @@ class TestStockReconciliation(ERPNextTestCase):
item_code = "Stock-Reco-batch-Item-1"
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
- sr = create_stock_reconciliation(item_code=item_code,
- warehouse = warehouse, qty=5, rate=200, do_not_submit=1)
+ sr = create_stock_reconciliation(
+ item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_submit=1
+ )
sr.save(ignore_permissions=True)
sr.submit()
self.assertTrue(sr.items[0].batch_no)
to_delete_records.append(sr.name)
- sr1 = create_stock_reconciliation(item_code=item_code,
- warehouse = warehouse, qty=6, rate=300, batch_no=sr.items[0].batch_no)
+ sr1 = create_stock_reconciliation(
+ item_code=item_code, warehouse=warehouse, qty=6, rate=300, batch_no=sr.items[0].batch_no
+ )
args = {
"item_code": item_code,
@@ -228,9 +274,9 @@ class TestStockReconciliation(ERPNextTestCase):
self.assertEqual(valuation_rate, 300)
to_delete_records.append(sr1.name)
-
- sr2 = create_stock_reconciliation(item_code=item_code,
- warehouse = warehouse, qty=0, rate=0, batch_no=sr.items[0].batch_no)
+ sr2 = create_stock_reconciliation(
+ item_code=item_code, warehouse=warehouse, qty=0, rate=0, batch_no=sr.items[0].batch_no
+ )
stock_value = get_stock_value_on(warehouse, nowdate(), item_code)
self.assertEqual(stock_value, 0)
@@ -242,11 +288,12 @@ class TestStockReconciliation(ERPNextTestCase):
stock_doc.cancel()
def test_customer_provided_items(self):
- item_code = 'Stock-Reco-customer-Item-100'
- create_item(item_code, is_customer_provided_item = 1,
- customer = '_Test Customer', is_purchase_item = 0)
+ item_code = "Stock-Reco-customer-Item-100"
+ create_item(
+ item_code, is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0
+ )
- sr = create_stock_reconciliation(item_code = item_code, qty = 10, rate = 420)
+ sr = create_stock_reconciliation(item_code=item_code, qty=10, rate=420)
self.assertEqual(sr.get("items")[0].allow_zero_valuation_rate, 1)
self.assertEqual(sr.get("items")[0].valuation_rate, 0)
@@ -254,65 +301,78 @@ class TestStockReconciliation(ERPNextTestCase):
def test_backdated_stock_reco_qty_reposting(self):
"""
- Test if a backdated stock reco recalculates future qty until next reco.
- -------------------------------------------
- Var | Doc | Qty | Balance
- -------------------------------------------
- SR5 | Reco | 0 | 8 (posting date: today-4) [backdated]
- PR1 | PR | 10 | 18 (posting date: today-3)
- PR2 | PR | 1 | 19 (posting date: today-2)
- SR4 | Reco | 0 | 6 (posting date: today-1) [backdated]
- PR3 | PR | 1 | 7 (posting date: today) # can't post future PR
+ Test if a backdated stock reco recalculates future qty until next reco.
+ -------------------------------------------
+ Var | Doc | Qty | Balance
+ -------------------------------------------
+ SR5 | Reco | 0 | 8 (posting date: today-4) [backdated]
+ PR1 | PR | 10 | 18 (posting date: today-3)
+ PR2 | PR | 1 | 19 (posting date: today-2)
+ SR4 | Reco | 0 | 6 (posting date: today-1) [backdated]
+ PR3 | PR | 1 | 7 (posting date: today) # can't post future PR
"""
- item_code = "Backdated-Reco-Item"
+ item_code = make_item().name
warehouse = "_Test Warehouse - _TC"
- create_item(item_code)
- pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100,
- posting_date=add_days(nowdate(), -3))
- pr2 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100,
- posting_date=add_days(nowdate(), -2))
- pr3 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100,
- posting_date=nowdate())
+ pr1 = make_purchase_receipt(
+ item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
+ )
+ pr2 = make_purchase_receipt(
+ item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=add_days(nowdate(), -2)
+ )
+ pr3 = make_purchase_receipt(
+ item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate()
+ )
- pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
- "qty_after_transaction")
- pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0},
- "qty_after_transaction")
+ pr1_balance = frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
+ )
+ pr3_balance = frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction"
+ )
self.assertEqual(pr1_balance, 10)
self.assertEqual(pr3_balance, 12)
# post backdated stock reco in between
- sr4 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=6, rate=100,
- posting_date=add_days(nowdate(), -1))
- pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0},
- "qty_after_transaction")
+ sr4 = create_stock_reconciliation(
+ item_code=item_code, warehouse=warehouse, qty=6, rate=100, posting_date=add_days(nowdate(), -1)
+ )
+ pr3_balance = frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction"
+ )
self.assertEqual(pr3_balance, 7)
# post backdated stock reco at the start
- sr5 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=8, rate=100,
- posting_date=add_days(nowdate(), -4))
- pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
- "qty_after_transaction")
- pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0},
- "qty_after_transaction")
- sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0},
- "qty_after_transaction")
+ sr5 = create_stock_reconciliation(
+ item_code=item_code, warehouse=warehouse, qty=8, rate=100, posting_date=add_days(nowdate(), -4)
+ )
+ pr1_balance = frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
+ )
+ pr2_balance = frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction"
+ )
+ sr4_balance = frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction"
+ )
self.assertEqual(pr1_balance, 18)
self.assertEqual(pr2_balance, 19)
- self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
+ self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
# cancel backdated stock reco and check future impact
sr5.cancel()
- pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
- "qty_after_transaction")
- pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0},
- "qty_after_transaction")
- sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0},
- "qty_after_transaction")
+ pr1_balance = frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
+ )
+ pr2_balance = frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction"
+ )
+ sr4_balance = frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction"
+ )
self.assertEqual(pr1_balance, 10)
self.assertEqual(pr2_balance, 11)
- self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
+ self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
# teardown
sr4.cancel()
@@ -323,37 +383,45 @@ class TestStockReconciliation(ERPNextTestCase):
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_backdated_stock_reco_future_negative_stock(self):
"""
- Test if a backdated stock reco causes future negative stock and is blocked.
- -------------------------------------------
- Var | Doc | Qty | Balance
- -------------------------------------------
- PR1 | PR | 10 | 10 (posting date: today-2)
- SR3 | Reco | 0 | 1 (posting date: today-1) [backdated & blocked]
- DN2 | DN | -2 | 8(-1) (posting date: today)
+ Test if a backdated stock reco causes future negative stock and is blocked.
+ -------------------------------------------
+ Var | Doc | Qty | Balance
+ -------------------------------------------
+ PR1 | PR | 10 | 10 (posting date: today-2)
+ SR3 | Reco | 0 | 1 (posting date: today-1) [backdated & blocked]
+ DN2 | DN | -2 | 8(-1) (posting date: today)
"""
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.stock_ledger import NegativeStockError
- item_code = "Backdated-Reco-Item"
+ item_code = make_item().name
warehouse = "_Test Warehouse - _TC"
- create_item(item_code)
+ pr1 = make_purchase_receipt(
+ item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -2)
+ )
+ dn2 = create_delivery_note(
+ item_code=item_code, warehouse=warehouse, qty=2, rate=120, posting_date=nowdate()
+ )
- pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100,
- posting_date=add_days(nowdate(), -2))
- dn2 = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=2, rate=120,
- posting_date=nowdate())
-
- pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
- "qty_after_transaction")
- dn2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn2.name, "is_cancelled": 0},
- "qty_after_transaction")
+ pr1_balance = frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction"
+ )
+ dn2_balance = frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_no": dn2.name, "is_cancelled": 0}, "qty_after_transaction"
+ )
self.assertEqual(pr1_balance, 10)
self.assertEqual(dn2_balance, 8)
# check if stock reco is blocked
- sr3 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=1, rate=100,
- posting_date=add_days(nowdate(), -1), do_not_submit=True)
+ sr3 = create_stock_reconciliation(
+ item_code=item_code,
+ warehouse=warehouse,
+ qty=1,
+ rate=100,
+ posting_date=add_days(nowdate(), -1),
+ do_not_submit=True,
+ )
self.assertRaises(NegativeStockError, sr3.submit)
# teardown
@@ -361,33 +429,37 @@ class TestStockReconciliation(ERPNextTestCase):
dn2.cancel()
pr1.cancel()
-
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_backdated_stock_reco_cancellation_future_negative_stock(self):
"""
- Test if a backdated stock reco cancellation that causes future negative stock is blocked.
- -------------------------------------------
- Var | Doc | Qty | Balance
- -------------------------------------------
- SR | Reco | 100 | 100 (posting date: today-1) (shouldn't be cancelled after DN)
- DN | DN | 100 | 0 (posting date: today)
+ Test if a backdated stock reco cancellation that causes future negative stock is blocked.
+ -------------------------------------------
+ Var | Doc | Qty | Balance
+ -------------------------------------------
+ SR | Reco | 100 | 100 (posting date: today-1) (shouldn't be cancelled after DN)
+ DN | DN | 100 | 0 (posting date: today)
"""
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.stock_ledger import NegativeStockError
- item_code = "Backdated-Reco-Cancellation-Item"
+ item_code = make_item().name
warehouse = "_Test Warehouse - _TC"
- create_item(item_code)
+ sr = create_stock_reconciliation(
+ item_code=item_code,
+ warehouse=warehouse,
+ qty=100,
+ rate=100,
+ posting_date=add_days(nowdate(), -1),
+ )
- sr = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=100, rate=100,
- posting_date=add_days(nowdate(), -1))
+ dn = create_delivery_note(
+ item_code=item_code, warehouse=warehouse, qty=100, rate=120, posting_date=nowdate()
+ )
- dn = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=100, rate=120,
- posting_date=nowdate())
-
- dn_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn.name, "is_cancelled": 0},
- "qty_after_transaction")
+ dn_balance = frappe.db.get_value(
+ "Stock Ledger Entry", {"voucher_no": dn.name, "is_cancelled": 0}, "qty_after_transaction"
+ )
self.assertEqual(dn_balance, 0)
# check if cancellation of stock reco is blocked
@@ -399,43 +471,51 @@ class TestStockReconciliation(ERPNextTestCase):
def test_intermediate_sr_bin_update(self):
"""Bin should show correct qty even for backdated entries.
- -------------------------------------------
- | creation | Var | Doc | Qty | balance qty
- -------------------------------------------
- | 1 | SR | Reco | 10 | 10 (posting date: today+10)
- | 3 | SR2 | Reco | 11 | 11 (posting date: today+11)
- | 2 | DN | DN | 5 | 6 <-- assert in BIN (posting date: today+12)
+ -------------------------------------------
+ | creation | Var | Doc | Qty | balance qty
+ -------------------------------------------
+ | 1 | SR | Reco | 10 | 10 (posting date: today+10)
+ | 3 | SR2 | Reco | 11 | 11 (posting date: today+11)
+ | 2 | DN | DN | 5 | 6 <-- assert in BIN (posting date: today+12)
"""
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
- # repost will make this test useless, qty should update in realtime without reposts
- frappe.flags.dont_execute_stock_reposts = True
frappe.db.rollback()
- item_code = "Backdated-Reco-Cancellation-Item"
+ # repost will make this test useless, qty should update in realtime without reposts
+ frappe.flags.dont_execute_stock_reposts = True
+ self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts")
+
+ item_code = make_item().name
warehouse = "_Test Warehouse - _TC"
- create_item(item_code)
- sr = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=10, rate=100,
- posting_date=add_days(nowdate(), 10))
+ sr = create_stock_reconciliation(
+ item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), 10)
+ )
- dn = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=5, rate=120,
- posting_date=add_days(nowdate(), 12))
- old_bin_qty = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty")
+ dn = create_delivery_note(
+ item_code=item_code, warehouse=warehouse, qty=5, rate=120, posting_date=add_days(nowdate(), 12)
+ )
+ old_bin_qty = frappe.db.get_value(
+ "Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty"
+ )
- sr2 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=11, rate=100,
- posting_date=add_days(nowdate(), 11))
- new_bin_qty = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty")
+ sr2 = create_stock_reconciliation(
+ item_code=item_code, warehouse=warehouse, qty=11, rate=100, posting_date=add_days(nowdate(), 11)
+ )
+ new_bin_qty = frappe.db.get_value(
+ "Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty"
+ )
self.assertEqual(old_bin_qty + 1, new_bin_qty)
frappe.db.rollback()
-
def test_valid_batch(self):
create_batch_item_with_batch("Testing Batch Item 1", "001")
create_batch_item_with_batch("Testing Batch Item 2", "002")
- sr = create_stock_reconciliation(item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002"
- , do_not_submit=True)
+ sr = create_stock_reconciliation(
+ item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002", do_not_submit=True
+ )
self.assertRaises(frappe.ValidationError, sr.submit)
def test_serial_no_cancellation(self):
@@ -457,15 +537,17 @@ class TestStockReconciliation(ERPNextTestCase):
serial_nos.pop()
new_serial_nos = "\n".join(serial_nos)
- sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse, serial_no=new_serial_nos, qty=9)
+ sr = create_stock_reconciliation(
+ item_code=item.name, warehouse=warehouse, serial_no=new_serial_nos, qty=9
+ )
sr.cancel()
- active_sr_no = frappe.get_all("Serial No",
- filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"})
+ active_sr_no = frappe.get_all(
+ "Serial No", filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"}
+ )
self.assertEqual(len(active_sr_no), 10)
-
def test_serial_no_creation_and_inactivation(self):
item = create_item("_TestItemCreatedWithStockReco", is_stock_item=1)
if not item.has_serial_no:
@@ -475,19 +557,27 @@ class TestStockReconciliation(ERPNextTestCase):
item_code = item.name
warehouse = "_Test Warehouse - _TC"
- sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse,
- serial_no="SR-CREATED-SR-NO", qty=1, do_not_submit=True, rate=100)
+ sr = create_stock_reconciliation(
+ item_code=item.name,
+ warehouse=warehouse,
+ serial_no="SR-CREATED-SR-NO",
+ qty=1,
+ do_not_submit=True,
+ rate=100,
+ )
sr.save()
self.assertEqual(cstr(sr.items[0].current_serial_no), "")
sr.submit()
- active_sr_no = frappe.get_all("Serial No",
- filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"})
+ active_sr_no = frappe.get_all(
+ "Serial No", filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"}
+ )
self.assertEqual(len(active_sr_no), 1)
sr.cancel()
- active_sr_no = frappe.get_all("Serial No",
- filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"})
+ active_sr_no = frappe.get_all(
+ "Serial No", filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"}
+ )
self.assertEqual(len(active_sr_no), 0)
@@ -498,32 +588,51 @@ def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc.create_new_batch = 1
batch_item_doc.save(ignore_permissions=True)
- if not frappe.db.exists('Batch', batch_id):
- b = frappe.new_doc('Batch')
+ if not frappe.db.exists("Batch", batch_id):
+ b = frappe.new_doc("Batch")
b.item = item_name
b.batch_id = batch_id
b.save()
-def insert_existing_sle(warehouse):
+
+def insert_existing_sle(warehouse, item_code="_Test Item"):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
- se1 = make_stock_entry(posting_date="2012-12-15", posting_time="02:00", item_code="_Test Item",
- target=warehouse, qty=10, basic_rate=700)
+ se1 = make_stock_entry(
+ posting_date="2012-12-15",
+ posting_time="02:00",
+ item_code=item_code,
+ target=warehouse,
+ qty=10,
+ basic_rate=700,
+ )
- se2 = make_stock_entry(posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item",
- source=warehouse, qty=15)
+ se2 = make_stock_entry(
+ posting_date="2012-12-25", posting_time="03:00", item_code=item_code, source=warehouse, qty=15
+ )
- se3 = make_stock_entry(posting_date="2013-01-05", posting_time="07:00", item_code="_Test Item",
- target=warehouse, qty=15, basic_rate=1200)
+ se3 = make_stock_entry(
+ posting_date="2013-01-05",
+ posting_time="07:00",
+ item_code=item_code,
+ target=warehouse,
+ qty=15,
+ basic_rate=1200,
+ )
return se1, se2, se3
-def create_batch_or_serial_no_items():
- create_warehouse("_Test Warehouse for Stock Reco1",
- {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"})
- create_warehouse("_Test Warehouse for Stock Reco2",
- {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"})
+def create_batch_or_serial_no_items():
+ create_warehouse(
+ "_Test Warehouse for Stock Reco1",
+ {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"},
+ )
+
+ create_warehouse(
+ "_Test Warehouse for Stock Reco2",
+ {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"},
+ )
serial_item_doc = create_item("Stock-Reco-Serial-Item-1", is_stock_item=1)
if not serial_item_doc.has_serial_no:
@@ -544,6 +653,7 @@ def create_batch_or_serial_no_items():
serial_item_doc.batch_number_series = "BASR.#####"
batch_item_doc.save(ignore_permissions=True)
+
def create_stock_reconciliation(**args):
args = frappe._dict(args)
sr = frappe.new_doc("Stock Reconciliation")
@@ -552,20 +662,26 @@ def create_stock_reconciliation(**args):
sr.posting_time = args.posting_time or nowtime()
sr.set_posting_time = 1
sr.company = args.company or "_Test Company"
- sr.expense_account = args.expense_account or \
- ("Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC")
- sr.cost_center = args.cost_center \
- or frappe.get_cached_value("Company", sr.company, "cost_center") \
+ sr.expense_account = args.expense_account or (
+ "Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC"
+ )
+ sr.cost_center = (
+ args.cost_center
+ or frappe.get_cached_value("Company", sr.company, "cost_center")
or "_Test Cost Center - _TC"
+ )
- sr.append("items", {
- "item_code": args.item_code or "_Test Item",
- "warehouse": args.warehouse or "_Test Warehouse - _TC",
- "qty": args.qty,
- "valuation_rate": args.rate,
- "serial_no": args.serial_no,
- "batch_no": args.batch_no
- })
+ sr.append(
+ "items",
+ {
+ "item_code": args.item_code or "_Test Item",
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "qty": args.qty,
+ "valuation_rate": args.rate,
+ "serial_no": args.serial_no,
+ "batch_no": args.batch_no,
+ },
+ )
try:
if not args.do_not_submit:
@@ -574,6 +690,7 @@ def create_stock_reconciliation(**args):
pass
return sr
+
def set_valuation_method(item_code, valuation_method):
existing_valuation_method = get_valuation_method(item_code)
if valuation_method == existing_valuation_method:
@@ -581,11 +698,13 @@ def set_valuation_method(item_code, valuation_method):
frappe.db.set_value("Item", item_code, "valuation_method", valuation_method)
- for warehouse in frappe.get_all("Warehouse", filters={"company": "_Test Company"}, fields=["name", "is_group"]):
+ for warehouse in frappe.get_all(
+ "Warehouse", filters={"company": "_Test Company"}, fields=["name", "is_group"]
+ ):
if not warehouse.is_group:
- update_entries_after({
- "item_code": item_code,
- "warehouse": warehouse.name
- }, allow_negative_stock=1)
+ update_entries_after(
+ {"item_code": item_code, "warehouse": warehouse.name}, allow_negative_stock=1
+ )
+
test_dependencies = ["Item", "Warehouse"]
diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
index bab521d69fc..e0c8ed12e7d 100644
--- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
+++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
@@ -6,8 +6,6 @@ from frappe.utils import add_to_date, get_datetime, get_time_str, time_diff_in_h
class StockRepostingSettings(Document):
-
-
def validate(self):
self.set_minimum_reposting_time_slot()
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js
index 6167becdaac..66da215dbbe 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.js
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.js
@@ -13,6 +13,25 @@ frappe.ui.form.on('Stock Settings', {
frm.set_query("default_warehouse", filters);
frm.set_query("sample_retention_warehouse", filters);
+ },
+ allow_negative_stock: function(frm) {
+ if (!frm.doc.allow_negative_stock) {
+ return;
+ }
+
+ let msg = __("Using negative stock disables FIFO/Moving average valuation when inventory is negative.");
+ msg += " ";
+ msg += __("This is considered dangerous from accounting point of view.")
+ msg += " ";
+ msg += ("Do you still want to enable negative inventory?");
+
+ frappe.confirm(
+ msg,
+ () => {},
+ () => {
+ frm.set_value("allow_negative_stock", 0);
+ }
+ );
}
});
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index c1293cbf0fa..e592a4be3c6 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -16,24 +16,40 @@ from erpnext.stock.utils import check_pending_reposting
class StockSettings(Document):
def validate(self):
- for key in ["item_naming_by", "item_group", "stock_uom",
- "allow_negative_stock", "default_warehouse", "set_qty_in_transactions_based_on_serial_no_input"]:
- frappe.db.set_default(key, self.get(key, ""))
+ for key in [
+ "item_naming_by",
+ "item_group",
+ "stock_uom",
+ "allow_negative_stock",
+ "default_warehouse",
+ "set_qty_in_transactions_based_on_serial_no_input",
+ ]:
+ frappe.db.set_default(key, self.get(key, ""))
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
- set_by_naming_series("Item", "item_code",
- self.get("item_naming_by")=="Naming Series", hide_name_field=True, make_mandatory=0)
+
+ set_by_naming_series(
+ "Item",
+ "item_code",
+ self.get("item_naming_by") == "Naming Series",
+ hide_name_field=True,
+ make_mandatory=0,
+ )
stock_frozen_limit = 356
submitted_stock_frozen = self.stock_frozen_upto_days or 0
if submitted_stock_frozen > stock_frozen_limit:
self.stock_frozen_upto_days = stock_frozen_limit
- frappe.msgprint (_("`Freeze Stocks Older Than` should be smaller than %d days.") %stock_frozen_limit)
+ frappe.msgprint(
+ _("`Freeze Stocks Older Than` should be smaller than %d days.") % stock_frozen_limit
+ )
# show/hide barcode field
for name in ["barcode", "barcodes", "scan_barcode"]:
- frappe.make_property_setter({'fieldname': name, 'property': 'hidden',
- 'value': 0 if self.show_barcode_field else 1}, validate_fields_for_doctype=False)
+ frappe.make_property_setter(
+ {"fieldname": name, "property": "hidden", "value": 0 if self.show_barcode_field else 1},
+ validate_fields_for_doctype=False,
+ )
self.validate_warehouses()
self.cant_change_valuation_method()
@@ -44,8 +60,12 @@ class StockSettings(Document):
warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
for field in warehouse_fields:
if frappe.db.get_value("Warehouse", self.get(field), "is_group"):
- frappe.throw(_("Group Warehouses cannot be used in transactions. Please change the value of {0}") \
- .format(frappe.bold(self.meta.get_field(field).label)), title =_("Incorrect Warehouse"))
+ frappe.throw(
+ _("Group Warehouses cannot be used in transactions. Please change the value of {0}").format(
+ frappe.bold(self.meta.get_field(field).label)
+ ),
+ title=_("Incorrect Warehouse"),
+ )
def cant_change_valuation_method(self):
db_valuation_method = frappe.db.get_single_value("Stock Settings", "valuation_method")
@@ -53,38 +73,73 @@ class StockSettings(Document):
if db_valuation_method and db_valuation_method != self.valuation_method:
# check if there are any stock ledger entries against items
# which does not have it's own valuation method
- sle = frappe.db.sql("""select name from `tabStock Ledger Entry` sle
+ sle = frappe.db.sql(
+ """select name from `tabStock Ledger Entry` sle
where exists(select name from tabItem
where name=sle.item_code and (valuation_method is null or valuation_method='')) limit 1
- """)
+ """
+ )
if sle:
- frappe.throw(_("Can't change the valuation method, as there are transactions against some items which do not have its own valuation method"))
+ frappe.throw(
+ _(
+ "Can't change the valuation method, as there are transactions against some items which do not have its own valuation method"
+ )
+ )
def validate_clean_description_html(self):
- if int(self.clean_description_html or 0) \
- and not int(self.db_get('clean_description_html') or 0):
+ if int(self.clean_description_html or 0) and not int(self.db_get("clean_description_html") or 0):
# changed to text
- frappe.enqueue('erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions', now=frappe.flags.in_test)
+ frappe.enqueue(
+ "erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions",
+ now=frappe.flags.in_test,
+ )
def validate_pending_reposts(self):
if self.stock_frozen_upto:
check_pending_reposting(self.stock_frozen_upto)
-
def on_update(self):
self.toggle_warehouse_field_for_inter_warehouse_transfer()
def toggle_warehouse_field_for_inter_warehouse_transfer(self):
- make_property_setter("Sales Invoice Item", "target_warehouse", "hidden", 1 - cint(self.allow_from_dn), "Check", validate_fields_for_doctype=False)
- make_property_setter("Delivery Note Item", "target_warehouse", "hidden", 1 - cint(self.allow_from_dn), "Check", validate_fields_for_doctype=False)
- make_property_setter("Purchase Invoice Item", "from_warehouse", "hidden", 1 - cint(self.allow_from_pr), "Check", validate_fields_for_doctype=False)
- make_property_setter("Purchase Receipt Item", "from_warehouse", "hidden", 1 - cint(self.allow_from_pr), "Check", validate_fields_for_doctype=False)
+ make_property_setter(
+ "Sales Invoice Item",
+ "target_warehouse",
+ "hidden",
+ 1 - cint(self.allow_from_dn),
+ "Check",
+ validate_fields_for_doctype=False,
+ )
+ make_property_setter(
+ "Delivery Note Item",
+ "target_warehouse",
+ "hidden",
+ 1 - cint(self.allow_from_dn),
+ "Check",
+ validate_fields_for_doctype=False,
+ )
+ make_property_setter(
+ "Purchase Invoice Item",
+ "from_warehouse",
+ "hidden",
+ 1 - cint(self.allow_from_pr),
+ "Check",
+ validate_fields_for_doctype=False,
+ )
+ make_property_setter(
+ "Purchase Receipt Item",
+ "from_warehouse",
+ "hidden",
+ 1 - cint(self.allow_from_pr),
+ "Check",
+ validate_fields_for_doctype=False,
+ )
def clean_all_descriptions():
- for item in frappe.get_all('Item', ['name', 'description']):
+ for item in frappe.get_all("Item", ["name", "description"]):
if item.description:
clean_description = clean_html(item.description)
if item.description != clean_description:
- frappe.db.set_value('Item', item.name, 'description', clean_description)
+ frappe.db.set_value("Item", item.name, "description", clean_description)
diff --git a/erpnext/stock/doctype/stock_settings/test_stock_settings.py b/erpnext/stock/doctype/stock_settings/test_stock_settings.py
index 072b54b8205..974e16339b7 100644
--- a/erpnext/stock/doctype/stock_settings/test_stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/test_stock_settings.py
@@ -4,45 +4,54 @@
import unittest
import frappe
-
-from erpnext.tests.utils import ERPNextTestCase
+from frappe.tests.utils import FrappeTestCase
-class TestStockSettings(ERPNextTestCase):
+class TestStockSettings(FrappeTestCase):
def setUp(self):
super().setUp()
frappe.db.set_value("Stock Settings", None, "clean_description_html", 0)
def test_settings(self):
- item = frappe.get_doc(dict(
- doctype = 'Item',
- item_code = 'Item for description test',
- item_group = 'Products',
- description = '
Drawing No. 07-xxx-PO132 1800 x 1685 x 750 All parts made of Marine Ply Top w/ Corian dd CO, CS, VIP Day Cabin
')
+ msg = (
+ f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
+ The list of the transactions are as below."""
+ + "
"
+ )
- msg += '
'.join(vouchers)
- msg += '
'
+ msg += "
".join(vouchers)
+ msg += "
"
- title = 'Cannot Submit' if not sle.get('is_cancelled') else 'Cannot Cancel'
+ title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
+
def validate_cancellation(args):
if args[0].get("is_cancelled"):
- repost_entry = frappe.db.get_value("Repost Item Valuation", {
- 'voucher_type': args[0].voucher_type,
- 'voucher_no': args[0].voucher_no,
- 'docstatus': 1
- }, ['name', 'status'], as_dict=1)
+ repost_entry = frappe.db.get_value(
+ "Repost Item Valuation",
+ {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
+ ["name", "status"],
+ as_dict=1,
+ )
if repost_entry:
- if repost_entry.status == 'In Progress':
- frappe.throw(_("Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."))
- if repost_entry.status == 'Queued':
+ if repost_entry.status == "In Progress":
+ frappe.throw(
+ _(
+ "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
+ )
+ )
+ if repost_entry.status == "Queued":
doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
+ doc.status = "Skipped"
doc.flags.ignore_permissions = True
doc.cancel()
- doc.delete()
+
def set_as_cancel(voucher_type, voucher_no):
- frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled=1,
+ frappe.db.sql(
+ """update `tabStock Ledger Entry` set is_cancelled=1,
modified=%s, modified_by=%s
where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
- (now(), frappe.session.user, voucher_type, voucher_no))
+ (now(), frappe.session.user, voucher_type, voucher_no),
+ )
+
def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
args["doctype"] = "Stock Ledger Entry"
sle = frappe.get_doc(args)
sle.flags.ignore_permissions = 1
- sle.allow_negative_stock=allow_negative_stock
+ sle.allow_negative_stock = allow_negative_stock
sle.via_landed_cost_voucher = via_landed_cost_voucher
sle.submit()
return sle
-def repost_future_sle(args=None, doc=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False):
+
+def repost_future_sle(
+ args=None,
+ doc=None,
+ voucher_type=None,
+ voucher_no=None,
+ allow_negative_stock=None,
+ via_landed_cost_voucher=False,
+):
if not args and voucher_type and voucher_no:
args = get_items_to_be_repost(voucher_type, voucher_no, doc)
distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
+ affected_transactions = get_affected_transactions(doc)
i = get_current_index(doc) or 0
while i < len(args):
- obj = update_entries_after({
- "item_code": args[i].get('item_code'),
- "warehouse": args[i].get('warehouse'),
- "posting_date": args[i].get('posting_date'),
- "posting_time": args[i].get('posting_time'),
- "creation": args[i].get("creation"),
- "distinct_item_warehouses": distinct_item_warehouses
- }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
+ obj = update_entries_after(
+ {
+ "item_code": args[i].get("item_code"),
+ "warehouse": args[i].get("warehouse"),
+ "posting_date": args[i].get("posting_date"),
+ "posting_time": args[i].get("posting_time"),
+ "creation": args[i].get("creation"),
+ "distinct_item_warehouses": distinct_item_warehouses,
+ },
+ allow_negative_stock=allow_negative_stock,
+ via_landed_cost_voucher=via_landed_cost_voucher,
+ )
+ affected_transactions.update(obj.affected_transactions)
- distinct_item_warehouses[(args[i].get('item_code'), args[i].get('warehouse'))].reposting_status = True
+ distinct_item_warehouses[
+ (args[i].get("item_code"), args[i].get("warehouse"))
+ ].reposting_status = True
if obj.new_items_found:
for item_wh, data in iteritems(distinct_item_warehouses):
- if ('args_idx' not in data and not data.reposting_status) or (data.sle_changed and data.reposting_status):
+ if ("args_idx" not in data and not data.reposting_status) or (
+ data.sle_changed and data.reposting_status
+ ):
data.args_idx = len(args)
args.append(data.sle)
elif data.sle_changed and not data.reposting_status:
@@ -200,82 +248,116 @@ def repost_future_sle(args=None, doc=None, voucher_type=None, voucher_no=None, a
i += 1
if doc and i % 2 == 0:
- update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses)
+ update_args_in_repost_item_valuation(
+ doc, i, args, distinct_item_warehouses, affected_transactions
+ )
if doc and args:
- update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses)
+ update_args_in_repost_item_valuation(
+ doc, i, args, distinct_item_warehouses, affected_transactions
+ )
-def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses):
- frappe.db.set_value(doc.doctype, doc.name, {
- 'items_to_be_repost': json.dumps(args, default=str),
- 'distinct_item_and_warehouse': json.dumps({str(k): v for k,v in distinct_item_warehouses.items()}, default=str),
- 'current_index': index
- })
- frappe.db.commit()
+def update_args_in_repost_item_valuation(
+ doc, index, args, distinct_item_warehouses, affected_transactions
+):
+ doc.db_set(
+ {
+ "items_to_be_repost": json.dumps(args, default=str),
+ "distinct_item_and_warehouse": json.dumps(
+ {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
+ ),
+ "current_index": index,
+ "affected_transactions": frappe.as_json(affected_transactions),
+ }
+ )
+
+ if not frappe.flags.in_test:
+ frappe.db.commit()
+
+ frappe.publish_realtime(
+ "item_reposting_progress",
+ {"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index},
+ )
- frappe.publish_realtime('item_reposting_progress', {
- 'name': doc.name,
- 'items_to_be_repost': json.dumps(args, default=str),
- 'current_index': index
- })
def get_items_to_be_repost(voucher_type, voucher_no, doc=None):
if doc and doc.items_to_be_repost:
return json.loads(doc.items_to_be_repost) or []
- return frappe.db.get_all("Stock Ledger Entry",
+ return frappe.db.get_all(
+ "Stock Ledger Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
order_by="creation asc",
- group_by="item_code, warehouse"
+ group_by="item_code, warehouse",
)
+
def get_distinct_item_warehouse(args=None, doc=None):
distinct_item_warehouses = {}
if doc and doc.distinct_item_and_warehouse:
distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
- distinct_item_warehouses = {frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()}
+ distinct_item_warehouses = {
+ frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
+ }
else:
for i, d in enumerate(args):
- distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({
- "reposting_status": False,
- "sle": d,
- "args_idx": i
- }))
+ distinct_item_warehouses.setdefault(
+ (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
+ )
return distinct_item_warehouses
+
+def get_affected_transactions(doc) -> Set[Tuple[str, str]]:
+ if not doc.affected_transactions:
+ return set()
+
+ transactions = frappe.parse_json(doc.affected_transactions)
+ return {tuple(transaction) for transaction in transactions}
+
+
def get_current_index(doc=None):
if doc and doc.current_index:
return doc.current_index
+
class update_entries_after(object):
"""
- update valution rate and qty after transaction
- from the current time-bucket onwards
+ update valution rate and qty after transaction
+ from the current time-bucket onwards
- :param args: args as dict
+ :param args: args as dict
- args = {
- "item_code": "ABC",
- "warehouse": "XYZ",
- "posting_date": "2012-12-12",
- "posting_time": "12:00"
- }
+ args = {
+ "item_code": "ABC",
+ "warehouse": "XYZ",
+ "posting_date": "2012-12-12",
+ "posting_time": "12:00"
+ }
"""
- def __init__(self, args, allow_zero_rate=False, allow_negative_stock=None, via_landed_cost_voucher=False, verbose=1):
+
+ def __init__(
+ self,
+ args,
+ allow_zero_rate=False,
+ allow_negative_stock=None,
+ via_landed_cost_voucher=False,
+ verbose=1,
+ ):
self.exceptions = {}
self.verbose = verbose
self.allow_zero_rate = allow_zero_rate
self.via_landed_cost_voucher = via_landed_cost_voucher
- self.allow_negative_stock = allow_negative_stock \
- or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
+ self.allow_negative_stock = allow_negative_stock or cint(
+ frappe.db.get_single_value("Stock Settings", "allow_negative_stock")
+ )
self.args = frappe._dict(args)
self.item_code = args.get("item_code")
if self.args.sle_id:
- self.args['name'] = self.args.sle_id
+ self.args["name"] = self.args.sle_id
self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
self.get_precision()
@@ -283,34 +365,36 @@ class update_entries_after(object):
self.new_items_found = False
self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
+ self.affected_transactions: Set[Tuple[str, str]] = set()
self.data = frappe._dict()
self.initialize_previous_data(self.args)
self.build()
def get_precision(self):
- company_base_currency = frappe.get_cached_value('Company', self.company, "default_currency")
- self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"),
- currency=company_base_currency)
+ company_base_currency = frappe.get_cached_value("Company", self.company, "default_currency")
+ self.precision = get_field_precision(
+ frappe.get_meta("Stock Ledger Entry").get_field("stock_value"), currency=company_base_currency
+ )
def initialize_previous_data(self, args):
"""
- Get previous sl entries for current item for each related warehouse
- and assigns into self.data dict
+ Get previous sl entries for current item for each related warehouse
+ and assigns into self.data dict
- :Data Structure:
+ :Data Structure:
- self.data = {
- warehouse1: {
- 'previus_sle': {},
- 'qty_after_transaction': 10,
- 'valuation_rate': 100,
- 'stock_value': 1000,
- 'prev_stock_value': 1000,
- 'stock_queue': '[[10, 100]]',
- 'stock_value_difference': 1000
- }
- }
+ self.data = {
+ warehouse1: {
+ 'previus_sle': {},
+ 'qty_after_transaction': 10,
+ 'valuation_rate': 100,
+ 'stock_value': 1000,
+ 'prev_stock_value': 1000,
+ 'stock_queue': '[[10, 100]]',
+ 'stock_value_difference': 1000
+ }
+ }
"""
self.data.setdefault(args.warehouse, frappe._dict())
@@ -321,11 +405,13 @@ class update_entries_after(object):
for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
setattr(warehouse_dict, key, flt(previous_sle.get(key)))
- warehouse_dict.update({
- "prev_stock_value": previous_sle.stock_value or 0.0,
- "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
- "stock_value_difference": 0.0
- })
+ warehouse_dict.update(
+ {
+ "prev_stock_value": previous_sle.stock_value or 0.0,
+ "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
+ "stock_value_difference": 0.0,
+ }
+ )
def build(self):
from erpnext.controllers.stock_controller import future_sle_exists
@@ -358,9 +444,10 @@ class update_entries_after(object):
self.process_sle(sle)
def get_sle_against_current_voucher(self):
- self.args['time_format'] = '%H:%i:%s'
+ self.args["time_format"] = "%H:%i:%s"
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
select
*, timestamp(posting_date, posting_time) as "timestamp"
from
@@ -374,22 +461,29 @@ class update_entries_after(object):
order by
creation ASC
for update
- """, self.args, as_dict=1)
+ """,
+ self.args,
+ as_dict=1,
+ )
def get_future_entries_to_fix(self):
# includes current entry!
- args = self.data[self.args.warehouse].previous_sle \
- or frappe._dict({"item_code": self.item_code, "warehouse": self.args.warehouse})
+ args = self.data[self.args.warehouse].previous_sle or frappe._dict(
+ {"item_code": self.item_code, "warehouse": self.args.warehouse}
+ )
return list(self.get_sle_after_datetime(args))
def get_dependent_entries_to_fix(self, entries_to_fix, sle):
- dependant_sle = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no,
- excluded_sle=sle.name)
+ dependant_sle = get_sle_by_voucher_detail_no(
+ sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
+ )
if not dependant_sle:
return entries_to_fix
- elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse:
+ elif (
+ dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
+ ):
return entries_to_fix
elif dependant_sle.item_code != self.item_code:
self.update_distinct_item_warehouses(dependant_sle)
@@ -401,14 +495,14 @@ class update_entries_after(object):
def update_distinct_item_warehouses(self, dependant_sle):
key = (dependant_sle.item_code, dependant_sle.warehouse)
- val = frappe._dict({
- "sle": dependant_sle
- })
+ val = frappe._dict({"sle": dependant_sle})
if key not in self.distinct_item_warehouses:
self.distinct_item_warehouses[key] = val
self.new_items_found = True
else:
- existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
+ existing_sle_posting_date = (
+ self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
+ )
if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
val.sle_changed = True
self.distinct_item_warehouses[key] = val
@@ -417,18 +511,20 @@ class update_entries_after(object):
def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix):
self.initialize_previous_data(dependant_sle)
- args = self.data[dependant_sle.warehouse].previous_sle \
- or frappe._dict({"item_code": self.item_code, "warehouse": dependant_sle.warehouse})
+ args = self.data[dependant_sle.warehouse].previous_sle or frappe._dict(
+ {"item_code": self.item_code, "warehouse": dependant_sle.warehouse}
+ )
future_sle_for_dependant = list(self.get_sle_after_datetime(args))
entries_to_fix.extend(future_sle_for_dependant)
- return sorted(entries_to_fix, key=lambda k: k['timestamp'])
+ return sorted(entries_to_fix, key=lambda k: k["timestamp"])
def process_sle(self, sle):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
# previous sle data for this warehouse
self.wh_data = self.data[sle.warehouse]
+ self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
# validate negative stock for serialized items, fifo valuation
@@ -447,24 +543,32 @@ class update_entries_after(object):
if sle.voucher_type == "Stock Reconciliation":
self.wh_data.qty_after_transaction = sle.qty_after_transaction
- self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
+ self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
+ self.wh_data.valuation_rate
+ )
else:
- if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no:
+ if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
# assert
self.wh_data.valuation_rate = sle.valuation_rate
self.wh_data.qty_after_transaction = sle.qty_after_transaction
- self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
+ self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
+ self.wh_data.valuation_rate
+ )
if self.valuation_method != "Moving Average":
self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
else:
if self.valuation_method == "Moving Average":
self.get_moving_average_values(sle)
self.wh_data.qty_after_transaction += flt(sle.actual_qty)
- self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
+ self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
+ self.wh_data.valuation_rate
+ )
else:
self.get_fifo_values(sle)
self.wh_data.qty_after_transaction += flt(sle.actual_qty)
- self.wh_data.stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue))
+ self.wh_data.stock_value = sum(
+ (flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue)
+ )
# rounding as per precision
self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
@@ -479,7 +583,7 @@ class update_entries_after(object):
sle.stock_value = self.wh_data.stock_value
sle.stock_queue = json.dumps(self.wh_data.stock_queue)
sle.stock_value_difference = stock_value_difference
- sle.doctype="Stock Ledger Entry"
+ sle.doctype = "Stock Ledger Entry"
frappe.get_doc(sle).db_update()
if not self.args.get("sle_id"):
@@ -487,8 +591,8 @@ class update_entries_after(object):
def validate_negative_stock(self, sle):
"""
- validate negative stock for entries current datetime onwards
- will not consider cancelled entries
+ validate negative stock for entries current datetime onwards
+ will not consider cancelled entries
"""
diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
@@ -517,13 +621,24 @@ class update_entries_after(object):
self.recalculate_amounts_in_stock_entry(sle.voucher_no)
rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
# Sales and Purchase Return
- elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"):
+ elif sle.voucher_type in (
+ "Purchase Receipt",
+ "Purchase Invoice",
+ "Delivery Note",
+ "Sales Invoice",
+ ):
if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
from erpnext.controllers.sales_and_purchase_return import (
get_rate_for_return, # don't move this import to top
)
- rate = get_rate_for_return(sle.voucher_type, sle.voucher_no, sle.item_code,
- voucher_detail_no=sle.voucher_detail_no, sle = sle)
+
+ rate = get_rate_for_return(
+ sle.voucher_type,
+ sle.voucher_no,
+ sle.item_code,
+ voucher_detail_no=sle.voucher_detail_no,
+ sle=sle,
+ )
else:
if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
rate_field = "valuation_rate"
@@ -531,8 +646,9 @@ class update_entries_after(object):
rate_field = "incoming_rate"
# check in item table
- item_code, incoming_rate = frappe.db.get_value(sle.voucher_type + " Item",
- sle.voucher_detail_no, ["item_code", rate_field])
+ item_code, incoming_rate = frappe.db.get_value(
+ sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
+ )
if item_code == sle.item_code:
rate = incoming_rate
@@ -542,15 +658,18 @@ class update_entries_after(object):
else:
ref_doctype = "Purchase Receipt Item Supplied"
- rate = frappe.db.get_value(ref_doctype, {"parent_detail_docname": sle.voucher_detail_no,
- "item_code": sle.item_code}, rate_field)
+ rate = frappe.db.get_value(
+ ref_doctype,
+ {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
+ rate_field,
+ )
return rate
def update_outgoing_rate_on_transaction(self, sle):
"""
- Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
- In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
+ Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
+ In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
"""
if sle.actual_qty and sle.voucher_detail_no:
outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
@@ -580,24 +699,33 @@ class update_entries_after(object):
# Update item's incoming rate on transaction
item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
if item_code == sle.item_code:
- frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate)
+ frappe.db.set_value(
+ sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
+ )
else:
# packed item
- frappe.db.set_value("Packed Item",
+ frappe.db.set_value(
+ "Packed Item",
{"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
- "incoming_rate", outgoing_rate)
+ "incoming_rate",
+ outgoing_rate,
+ )
def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
- frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate)
+ frappe.db.set_value(
+ sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate
+ )
else:
- frappe.db.set_value("Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate)
+ frappe.db.set_value(
+ "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
+ )
# Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
- if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == 'Yes':
+ if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == "Yes":
doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
doc.update_valuation_rate(reset_outgoing_rate=False)
- for d in (doc.items + doc.supplied_items):
+ for d in doc.items + doc.supplied_items:
d.db_update()
def get_serialized_values(self, sle):
@@ -624,29 +752,34 @@ class update_entries_after(object):
new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
if new_stock_qty > 0:
- new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + stock_value_change
+ new_stock_value = (
+ self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
+ ) + stock_value_change
if new_stock_value >= 0:
# calculate new valuation rate only if stock value is positive
# else it remains the same as that of previous entry
self.wh_data.valuation_rate = new_stock_value / new_stock_qty
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
- allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
+ allow_zero_rate = self.check_if_allow_zero_valuation_rate(
+ sle.voucher_type, sle.voucher_detail_no
+ )
if not allow_zero_rate:
self.wh_data.valuation_rate = self.get_fallback_rate(sle)
def get_incoming_value_for_serial_nos(self, sle, serial_nos):
# get rate from serial nos within same company
- all_serial_nos = frappe.get_all("Serial No",
- fields=["purchase_rate", "name", "company"],
- filters = {'name': ('in', serial_nos)})
+ all_serial_nos = frappe.get_all(
+ "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
+ )
- incoming_values = sum(flt(d.purchase_rate) for d in all_serial_nos if d.company==sle.company)
+ incoming_values = sum(flt(d.purchase_rate) for d in all_serial_nos if d.company == sle.company)
# Get rate for serial nos which has been transferred to other company
- invalid_serial_nos = [d.name for d in all_serial_nos if d.company!=sle.company]
+ invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
for serial_no in invalid_serial_nos:
- incoming_rate = frappe.db.sql("""
+ incoming_rate = frappe.db.sql(
+ """
select incoming_rate
from `tabStock Ledger Entry`
where
@@ -660,7 +793,9 @@ class update_entries_after(object):
)
order by posting_date desc
limit 1
- """, (sle.company, serial_no, serial_no+'\n%', '%\n'+serial_no, '%\n'+serial_no+'\n%'))
+ """,
+ (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
+ )
incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
@@ -674,15 +809,17 @@ class update_entries_after(object):
if flt(self.wh_data.qty_after_transaction) <= 0:
self.wh_data.valuation_rate = sle.incoming_rate
else:
- new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \
- (actual_qty * sle.incoming_rate)
+ new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
+ actual_qty * sle.incoming_rate
+ )
self.wh_data.valuation_rate = new_stock_value / new_stock_qty
elif sle.outgoing_rate:
if new_stock_qty:
- new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \
- (actual_qty * sle.outgoing_rate)
+ new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
+ actual_qty * sle.outgoing_rate
+ )
self.wh_data.valuation_rate = new_stock_value / new_stock_qty
else:
@@ -697,7 +834,9 @@ class update_entries_after(object):
# Get valuation rate from previous SLE or Item master, if item does not have the
# allow zero valuration rate flag set
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
- allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
+ allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
+ sle.voucher_type, sle.voucher_detail_no
+ )
if not allow_zero_valuation_rate:
self.wh_data.valuation_rate = self.get_fallback_rate(sle)
@@ -711,24 +850,26 @@ class update_entries_after(object):
self.wh_data.stock_queue.append([0, 0])
# last row has the same rate, just updated the qty
- if self.wh_data.stock_queue[-1][1]==incoming_rate:
+ if self.wh_data.stock_queue[-1][1] == incoming_rate:
self.wh_data.stock_queue[-1][0] += actual_qty
else:
# Item has a positive balance qty, add new entry
if self.wh_data.stock_queue[-1][0] > 0:
self.wh_data.stock_queue.append([actual_qty, incoming_rate])
- else: # negative balance qty
+ else: # negative balance qty
qty = self.wh_data.stock_queue[-1][0] + actual_qty
- if qty > 0: # new balance qty is positive
+ if qty > 0: # new balance qty is positive
self.wh_data.stock_queue[-1] = [qty, incoming_rate]
- else: # new balance qty is still negative, maintain same rate
+ else: # new balance qty is still negative, maintain same rate
self.wh_data.stock_queue[-1][0] = qty
else:
qty_to_pop = abs(actual_qty)
while qty_to_pop:
if not self.wh_data.stock_queue:
# Get valuation rate from last sle if exists or from valuation rate field in item master
- allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
+ allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
+ sle.voucher_type, sle.voucher_detail_no
+ )
if not allow_zero_valuation_rate:
_rate = self.get_fallback_rate(sle)
else:
@@ -744,12 +885,9 @@ class update_entries_after(object):
index = i
break
- # If no entry found with outgoing rate, collapse stack
+ # If no entry found with outgoing rate, consume as per FIFO
if index is None: # nosemgrep
- new_stock_value = sum((d[0]*d[1] for d in self.wh_data.stock_queue)) - qty_to_pop*outgoing_rate
- new_stock_qty = sum((d[0] for d in self.wh_data.stock_queue)) - qty_to_pop
- self.wh_data.stock_queue = [[new_stock_qty, new_stock_value/new_stock_qty if new_stock_qty > 0 else outgoing_rate]]
- break
+ index = 0
else:
index = 0
@@ -771,14 +909,18 @@ class update_entries_after(object):
batch[0] = batch[0] - qty_to_pop
qty_to_pop = 0
- stock_value = _round_off_if_near_zero(sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue)))
+ stock_value = _round_off_if_near_zero(
+ sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue))
+ )
stock_qty = _round_off_if_near_zero(sum((flt(batch[0]) for batch in self.wh_data.stock_queue)))
if stock_qty:
self.wh_data.valuation_rate = stock_value / flt(stock_qty)
if not self.wh_data.stock_queue:
- self.wh_data.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate])
+ self.wh_data.stock_queue.append(
+ [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
+ )
def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
ref_item_dt = ""
@@ -795,10 +937,16 @@ class update_entries_after(object):
def get_fallback_rate(self, sle) -> float:
"""When exact incoming rate isn't available use any of other "average" rates as fallback.
- This should only get used for negative stock."""
- return get_valuation_rate(sle.item_code, sle.warehouse,
- sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
- currency=erpnext.get_company_currency(sle.company), company=sle.company)
+ This should only get used for negative stock."""
+ return get_valuation_rate(
+ sle.item_code,
+ sle.warehouse,
+ sle.voucher_type,
+ sle.voucher_no,
+ self.allow_zero_rate,
+ currency=erpnext.get_company_currency(sle.company),
+ company=sle.company,
+ )
def get_sle_before_datetime(self, args):
"""get previous stock ledger entry before current time-bucket"""
@@ -815,18 +963,27 @@ class update_entries_after(object):
for warehouse, exceptions in iteritems(self.exceptions):
deficiency = min(e["diff"] for e in exceptions)
- if ((exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]) in
- frappe.local.flags.currently_saving):
+ if (
+ exceptions[0]["voucher_type"],
+ exceptions[0]["voucher_no"],
+ ) in frappe.local.flags.currently_saving:
msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
- abs(deficiency), frappe.get_desk_link('Item', exceptions[0]["item_code"]),
- frappe.get_desk_link('Warehouse', warehouse))
+ abs(deficiency),
+ frappe.get_desk_link("Item", exceptions[0]["item_code"]),
+ frappe.get_desk_link("Warehouse", warehouse),
+ )
else:
- msg = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
- abs(deficiency), frappe.get_desk_link('Item', exceptions[0]["item_code"]),
- frappe.get_desk_link('Warehouse', warehouse),
- exceptions[0]["posting_date"], exceptions[0]["posting_time"],
- frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]))
+ msg = _(
+ "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
+ ).format(
+ abs(deficiency),
+ frappe.get_desk_link("Item", exceptions[0]["item_code"]),
+ frappe.get_desk_link("Warehouse", warehouse),
+ exceptions[0]["posting_date"],
+ exceptions[0]["posting_time"],
+ frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
+ )
if msg:
msg_list.append(msg)
@@ -834,7 +991,7 @@ class update_entries_after(object):
if msg_list:
message = "\n\n".join(msg_list)
if self.verbose:
- frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+ frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
else:
raise NegativeStockError(message)
@@ -843,17 +1000,16 @@ class update_entries_after(object):
for warehouse, data in self.data.items():
bin_name = get_or_make_bin(self.item_code, warehouse)
- frappe.db.set_value('Bin', bin_name, {
- "valuation_rate": data.valuation_rate,
- "actual_qty": data.qty_after_transaction,
- "stock_value": data.stock_value
- })
+ updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
+ if data.valuation_rate is not None:
+ updated_values["valuation_rate"] = data.valuation_rate
+ frappe.db.set_value("Bin", bin_name, updated_values)
def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
"""get stock ledger entries filtered by specific posting datetime conditions"""
- args['time_format'] = '%H:%i:%s'
+ args["time_format"] = "%H:%i:%s"
if not args.get("posting_date"):
args["posting_date"] = "1900-01-01"
if not args.get("posting_time"):
@@ -864,7 +1020,8 @@ def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
voucher_no = args.get("voucher_no")
voucher_condition = f"and voucher_no != '{voucher_no}'"
- sle = frappe.db.sql("""
+ sle = frappe.db.sql(
+ """
select *, timestamp(posting_date, posting_time) as "timestamp"
from `tabStock Ledger Entry`
where item_code = %(item_code)s
@@ -874,32 +1031,48 @@ def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
order by timestamp(posting_date, posting_time) desc, creation desc
limit 1
- for update""".format(voucher_condition=voucher_condition), args, as_dict=1)
+ for update""".format(
+ voucher_condition=voucher_condition
+ ),
+ args,
+ as_dict=1,
+ )
return sle[0] if sle else frappe._dict()
+
def get_previous_sle(args, for_update=False):
"""
- get the last sle on or before the current time-bucket,
- to get actual qty before transaction, this function
- is called from various transaction like stock entry, reco etc
+ get the last sle on or before the current time-bucket,
+ to get actual qty before transaction, this function
+ is called from various transaction like stock entry, reco etc
- args = {
- "item_code": "ABC",
- "warehouse": "XYZ",
- "posting_date": "2012-12-12",
- "posting_time": "12:00",
- "sle": "name of reference Stock Ledger Entry"
- }
+ args = {
+ "item_code": "ABC",
+ "warehouse": "XYZ",
+ "posting_date": "2012-12-12",
+ "posting_time": "12:00",
+ "sle": "name of reference Stock Ledger Entry"
+ }
"""
args["name"] = args.get("sle", None) or ""
sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
return sle and sle[0] or {}
-def get_stock_ledger_entries(previous_sle, operator=None,
- order="desc", limit=None, for_update=False, debug=False, check_serial_no=True):
+
+def get_stock_ledger_entries(
+ previous_sle,
+ operator=None,
+ order="desc",
+ limit=None,
+ for_update=False,
+ debug=False,
+ check_serial_no=True,
+):
"""get stock ledger entries filtered by specific posting datetime conditions"""
- conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(operator)
+ conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
+ operator
+ )
if previous_sle.get("warehouse"):
conditions += " and warehouse = %(warehouse)s"
elif previous_sle.get("warehouse_condition"):
@@ -908,15 +1081,21 @@ def get_stock_ledger_entries(previous_sle, operator=None,
if check_serial_no and previous_sle.get("serial_no"):
# conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
serial_no = previous_sle.get("serial_no")
- conditions += (""" and
+ conditions += (
+ """ and
(
serial_no = {0}
or serial_no like {1}
or serial_no like {2}
or serial_no like {3}
)
- """).format(frappe.db.escape(serial_no), frappe.db.escape('{}\n%'.format(serial_no)),
- frappe.db.escape('%\n{}'.format(serial_no)), frappe.db.escape('%\n{}\n%'.format(serial_no)))
+ """
+ ).format(
+ frappe.db.escape(serial_no),
+ frappe.db.escape("{}\n%".format(serial_no)),
+ frappe.db.escape("%\n{}".format(serial_no)),
+ frappe.db.escape("%\n{}\n%".format(serial_no)),
+ )
if not previous_sle.get("posting_date"):
previous_sle["posting_date"] = "1900-01-01"
@@ -926,34 +1105,59 @@ def get_stock_ledger_entries(previous_sle, operator=None,
if operator in (">", "<=") and previous_sle.get("name"):
conditions += " and name!=%(name)s"
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
select *, timestamp(posting_date, posting_time) as "timestamp"
from `tabStock Ledger Entry`
where item_code = %%(item_code)s
and is_cancelled = 0
%(conditions)s
order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
- %(limit)s %(for_update)s""" % {
+ %(limit)s %(for_update)s"""
+ % {
"conditions": conditions,
"limit": limit or "",
"for_update": for_update and "for update" or "",
- "order": order
- }, previous_sle, as_dict=1, debug=debug)
+ "order": order,
+ },
+ previous_sle,
+ as_dict=1,
+ debug=debug,
+ )
+
def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
- return frappe.db.get_value('Stock Ledger Entry',
- {'voucher_detail_no': voucher_detail_no, 'name': ['!=', excluded_sle]},
- ['item_code', 'warehouse', 'posting_date', 'posting_time', 'timestamp(posting_date, posting_time) as timestamp'],
- as_dict=1)
+ return frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]},
+ [
+ "item_code",
+ "warehouse",
+ "posting_date",
+ "posting_time",
+ "timestamp(posting_date, posting_time) as timestamp",
+ ],
+ as_dict=1,
+ )
-def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
- allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True):
+
+def get_valuation_rate(
+ item_code,
+ warehouse,
+ voucher_type,
+ voucher_no,
+ allow_zero_rate=False,
+ currency=None,
+ company=None,
+ raise_error_if_no_rate=True,
+):
if not company:
- company = frappe.get_cached_value("Warehouse", warehouse, "company")
+ company = frappe.get_cached_value("Warehouse", warehouse, "company")
# Get valuation rate from last sle for the same item and warehouse
- last_valuation_rate = frappe.db.sql("""select valuation_rate
+ last_valuation_rate = frappe.db.sql(
+ """select valuation_rate
from `tabStock Ledger Entry` force index (item_warehouse)
where
item_code = %s
@@ -961,18 +1165,23 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
AND valuation_rate >= 0
AND is_cancelled = 0
AND NOT (voucher_no = %s AND voucher_type = %s)
- order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
+ order by posting_date desc, posting_time desc, name desc limit 1""",
+ (item_code, warehouse, voucher_no, voucher_type),
+ )
if not last_valuation_rate:
# Get valuation rate from last sle for the item against any warehouse
- last_valuation_rate = frappe.db.sql("""select valuation_rate
+ last_valuation_rate = frappe.db.sql(
+ """select valuation_rate
from `tabStock Ledger Entry` force index (item_code)
where
item_code = %s
AND valuation_rate > 0
AND is_cancelled = 0
AND NOT(voucher_no = %s AND voucher_type = %s)
- order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type))
+ order by posting_date desc, posting_time desc, name desc limit 1""",
+ (item_code, voucher_no, voucher_type),
+ )
if last_valuation_rate:
return flt(last_valuation_rate[0][0])
@@ -987,19 +1196,37 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
if not valuation_rate:
# try in price list
- valuation_rate = frappe.db.get_value('Item Price',
- dict(item_code=item_code, buying=1, currency=currency),
- 'price_list_rate')
+ valuation_rate = frappe.db.get_value(
+ "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
+ )
- if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \
- and cint(erpnext.is_perpetual_inventory_enabled(company)):
+ if (
+ not allow_zero_rate
+ and not valuation_rate
+ and raise_error_if_no_rate
+ and cint(erpnext.is_perpetual_inventory_enabled(company))
+ ):
frappe.local.message_log = []
form_link = get_link_to_form("Item", item_code)
- message = _("Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}.").format(form_link, voucher_type, voucher_no)
+ message = _(
+ "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
+ ).format(form_link, voucher_type, voucher_no)
message += "
" + _("Here are the options to proceed:")
- solutions = "
" + _("If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table.").format(voucher_type) + "
"
- solutions += "
" + _("If not, you can Cancel / Submit this entry") + " {0} ".format(frappe.bold("after")) + _("performing either one below:") + "
"
+ solutions = (
+ "
"
+ + _(
+ "If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table."
+ ).format(voucher_type)
+ + "
"
+ )
+ solutions += (
+ "
"
+ + _("If not, you can Cancel / Submit this entry")
+ + " {0} ".format(frappe.bold("after"))
+ + _("performing either one below:")
+ + "
"
+ )
sub_solutions = "
" + _("Create an incoming stock transaction for the Item.") + "
"
sub_solutions += "
" + _("Mention Valuation Rate in the Item master.") + "
"
msg = message + solutions + sub_solutions + ""
@@ -1008,6 +1235,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
return valuation_rate
+
def update_qty_in_future_sle(args, allow_negative_stock=False):
"""Recalculate Qty after Transaction in future SLEs based on current SLE."""
datetime_limit_condition = ""
@@ -1024,7 +1252,8 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
# add condition to update SLEs before this date & time
datetime_limit_condition = get_datetime_limit_condition(detail)
- frappe.db.sql("""
+ frappe.db.sql(
+ """
update `tabStock Ledger Entry`
set qty_after_transaction = qty_after_transaction + {qty_shift}
where
@@ -1039,10 +1268,15 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
)
)
{datetime_limit_condition}
- """.format(qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition), args)
+ """.format(
+ qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition
+ ),
+ args,
+ )
validate_negative_qty_in_future_sle(args, allow_negative_stock)
+
def get_stock_reco_qty_shift(args):
stock_reco_qty_shift = 0
if args.get("is_cancelled"):
@@ -1054,8 +1288,9 @@ def get_stock_reco_qty_shift(args):
stock_reco_qty_shift = flt(args.actual_qty)
else:
# reco is being submitted
- last_balance = get_previous_sle_of_current_voucher(args,
- exclude_current_voucher=True).get("qty_after_transaction")
+ last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get(
+ "qty_after_transaction"
+ )
if last_balance is not None:
stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
@@ -1064,10 +1299,12 @@ def get_stock_reco_qty_shift(args):
return stock_reco_qty_shift
+
def get_next_stock_reco(args):
"""Returns next nearest stock reconciliaton's details."""
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
select
name, posting_date, posting_time, creation, voucher_no
from
@@ -1085,7 +1322,11 @@ def get_next_stock_reco(args):
)
)
limit 1
- """, args, as_dict=1)
+ """,
+ args,
+ as_dict=1,
+ )
+
def get_datetime_limit_condition(detail):
return f"""
@@ -1097,9 +1338,11 @@ def get_datetime_limit_condition(detail):
)
)"""
+
def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
- allow_negative_stock = cint(allow_negative_stock) \
- or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
+ allow_negative_stock = cint(allow_negative_stock) or cint(
+ frappe.db.get_single_value("Stock Settings", "allow_negative_stock")
+ )
if allow_negative_stock:
return
@@ -1108,32 +1351,40 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
neg_sle = get_future_sle_with_negative_qty(args)
if neg_sle:
- message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
+ message = _(
+ "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
+ ).format(
abs(neg_sle[0]["qty_after_transaction"]),
- frappe.get_desk_link('Item', args.item_code),
- frappe.get_desk_link('Warehouse', args.warehouse),
- neg_sle[0]["posting_date"], neg_sle[0]["posting_time"],
- frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]))
-
- frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+ frappe.get_desk_link("Item", args.item_code),
+ frappe.get_desk_link("Warehouse", args.warehouse),
+ neg_sle[0]["posting_date"],
+ neg_sle[0]["posting_time"],
+ frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
+ )
+ frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
if not args.batch_no:
return
neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
if neg_batch_sle:
- message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
+ message = _(
+ "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
+ ).format(
abs(neg_batch_sle[0]["cumulative_total"]),
- frappe.get_desk_link('Batch', args.batch_no),
- frappe.get_desk_link('Warehouse', args.warehouse),
- neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"],
- frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]))
- frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch")
+ frappe.get_desk_link("Batch", args.batch_no),
+ frappe.get_desk_link("Warehouse", args.warehouse),
+ neg_batch_sle[0]["posting_date"],
+ neg_batch_sle[0]["posting_time"],
+ frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
+ )
+ frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
def get_future_sle_with_negative_qty(args):
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
select
qty_after_transaction, posting_date, posting_time,
voucher_type, voucher_no
@@ -1147,11 +1398,15 @@ def get_future_sle_with_negative_qty(args):
and qty_after_transaction < 0
order by timestamp(posting_date, posting_time) asc
limit 1
- """, args, as_dict=1)
+ """,
+ args,
+ as_dict=1,
+ )
def get_future_sle_with_negative_batch_qty(args):
- batch_ledger = frappe.db.sql("""
+ batch_ledger = frappe.db.sql(
+ """
select
posting_date, posting_time, voucher_type, voucher_no, actual_qty
from `tabStock Ledger Entry`
@@ -1161,7 +1416,10 @@ def get_future_sle_with_negative_batch_qty(args):
and batch_no=%(batch_no)s
and is_cancelled = 0
order by timestamp(posting_date, posting_time), creation
- """, args, as_dict=1)
+ """,
+ args,
+ as_dict=1,
+ )
cumulative_total = 0.0
current_posting_datetime = get_datetime(str(args.posting_date) + " " + str(args.posting_time))
@@ -1170,16 +1428,18 @@ def get_future_sle_with_negative_batch_qty(args):
if cumulative_total > -1e-6:
continue
- if (get_datetime(str(entry.posting_date) + " " + str(entry.posting_time))
- >= current_posting_datetime):
+ if (
+ get_datetime(str(entry.posting_date) + " " + str(entry.posting_time))
+ >= current_posting_datetime
+ ):
entry.cumulative_total = cumulative_total
return [entry]
def _round_off_if_near_zero(number: float, precision: int = 6) -> float:
- """ Rounds off the number to zero only if number is close to zero for decimal
- specified in precision. Precision defaults to 6.
+ """Rounds off the number to zero only if number is close to zero for decimal
+ specified in precision. Precision defaults to 6.
"""
if abs(0.0 - flt(number)) < (1.0 / (10**precision)):
return 0.0
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index b8bdf39301e..98eaecf7907 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -12,8 +12,13 @@ from six import string_types
import erpnext
-class InvalidWarehouseCompany(frappe.ValidationError): pass
-class PendingRepostingError(frappe.ValidationError): pass
+class InvalidWarehouseCompany(frappe.ValidationError):
+ pass
+
+
+class PendingRepostingError(frappe.ValidationError):
+ pass
+
def get_stock_value_from_bin(warehouse=None, item_code=None):
values = {}
@@ -26,22 +31,27 @@ def get_stock_value_from_bin(warehouse=None, item_code=None):
and w2.lft between w1.lft and w1.rgt
) """
- values['warehouse'] = warehouse
+ values["warehouse"] = warehouse
if item_code:
conditions += " and `tabBin`.item_code = %(item_code)s"
- values['item_code'] = item_code
+ values["item_code"] = item_code
- query = """select sum(stock_value) from `tabBin`, `tabItem` where 1 = 1
- and `tabItem`.name = `tabBin`.item_code and ifnull(`tabItem`.disabled, 0) = 0 %s""" % conditions
+ query = (
+ """select sum(stock_value) from `tabBin`, `tabItem` where 1 = 1
+ and `tabItem`.name = `tabBin`.item_code and ifnull(`tabItem`.disabled, 0) = 0 %s"""
+ % conditions
+ )
stock_value = frappe.db.sql(query, values)
return stock_value
+
def get_stock_value_on(warehouse=None, posting_date=None, item_code=None):
- if not posting_date: posting_date = nowdate()
+ if not posting_date:
+ posting_date = nowdate()
values, condition = [posting_date], ""
@@ -63,13 +73,19 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None):
values.append(item_code)
condition += " AND item_code = %s"
- stock_ledger_entries = frappe.db.sql("""
+ stock_ledger_entries = frappe.db.sql(
+ """
SELECT item_code, stock_value, name, warehouse
FROM `tabStock Ledger Entry` sle
WHERE posting_date <= %s {0}
and is_cancelled = 0
ORDER BY timestamp(posting_date, posting_time) DESC, creation DESC
- """.format(condition), values, as_dict=1)
+ """.format(
+ condition
+ ),
+ values,
+ as_dict=1,
+ )
sle_map = {}
for sle in stock_ledger_entries:
@@ -78,23 +94,32 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None):
return sum(sle_map.values())
+
@frappe.whitelist()
-def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None,
- with_valuation_rate=False, with_serial_no=False):
+def get_stock_balance(
+ item_code,
+ warehouse,
+ posting_date=None,
+ posting_time=None,
+ with_valuation_rate=False,
+ with_serial_no=False,
+):
"""Returns stock balance quantity at given warehouse on given posting date or current date.
If `with_valuation_rate` is True, will return tuple (qty, rate)"""
from erpnext.stock.stock_ledger import get_previous_sle
- if posting_date is None: posting_date = nowdate()
- if posting_time is None: posting_time = nowtime()
+ if posting_date is None:
+ posting_date = nowdate()
+ if posting_time is None:
+ posting_time = nowtime()
args = {
"item_code": item_code,
- "warehouse":warehouse,
+ "warehouse": warehouse,
"posting_date": posting_date,
- "posting_time": posting_time
+ "posting_time": posting_time,
}
last_entry = get_previous_sle(args)
@@ -103,33 +128,41 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None
if with_serial_no:
serial_nos = get_serial_nos_data_after_transactions(args)
- return ((last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos)
- if last_entry else (0.0, 0.0, None))
+ return (
+ (last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos)
+ if last_entry
+ else (0.0, 0.0, None)
+ )
else:
- return (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0)
+ return (
+ (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0)
+ )
else:
return last_entry.qty_after_transaction if last_entry else 0.0
+
def get_serial_nos_data_after_transactions(args):
from pypika import CustomFunction
serial_nos = set()
args = frappe._dict(args)
- sle = frappe.qb.DocType('Stock Ledger Entry')
- Timestamp = CustomFunction('timestamp', ['date', 'time'])
+ sle = frappe.qb.DocType("Stock Ledger Entry")
+ Timestamp = CustomFunction("timestamp", ["date", "time"])
- stock_ledger_entries = frappe.qb.from_(
- sle
- ).select(
- 'serial_no','actual_qty'
- ).where(
- (sle.item_code == args.item_code)
- & (sle.warehouse == args.warehouse)
- & (Timestamp(sle.posting_date, sle.posting_time) < Timestamp(args.posting_date, args.posting_time))
- & (sle.is_cancelled == 0)
- ).orderby(
- sle.posting_date, sle.posting_time, sle.creation
- ).run(as_dict=1)
+ stock_ledger_entries = (
+ frappe.qb.from_(sle)
+ .select("serial_no", "actual_qty")
+ .where(
+ (sle.item_code == args.item_code)
+ & (sle.warehouse == args.warehouse)
+ & (
+ Timestamp(sle.posting_date, sle.posting_time) < Timestamp(args.posting_date, args.posting_time)
+ )
+ & (sle.is_cancelled == 0)
+ )
+ .orderby(sle.posting_date, sle.posting_time, sle.creation)
+ .run(as_dict=1)
+ )
for stock_ledger_entry in stock_ledger_entries:
changed_serial_no = get_serial_nos_data(stock_ledger_entry.serial_no)
@@ -138,12 +171,15 @@ def get_serial_nos_data_after_transactions(args):
else:
serial_nos.difference_update(changed_serial_no)
- return '\n'.join(serial_nos)
+ return "\n".join(serial_nos)
+
def get_serial_nos_data(serial_nos):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
return get_serial_nos(serial_nos)
+
@frappe.whitelist()
def get_latest_stock_qty(item_code, warehouse=None):
values, condition = [item_code], ""
@@ -160,37 +196,48 @@ def get_latest_stock_qty(item_code, warehouse=None):
values.append(warehouse)
condition += " AND warehouse = %s"
- actual_qty = frappe.db.sql("""select sum(actual_qty) from tabBin
- where item_code=%s {0}""".format(condition), values)[0][0]
+ actual_qty = frappe.db.sql(
+ """select sum(actual_qty) from tabBin
+ where item_code=%s {0}""".format(
+ condition
+ ),
+ values,
+ )[0][0]
return actual_qty
def get_latest_stock_balance():
bin_map = {}
- for d in frappe.db.sql("""SELECT item_code, warehouse, stock_value as stock_value
- FROM tabBin""", as_dict=1):
- bin_map.setdefault(d.warehouse, {}).setdefault(d.item_code, flt(d.stock_value))
+ for d in frappe.db.sql(
+ """SELECT item_code, warehouse, stock_value as stock_value
+ FROM tabBin""",
+ as_dict=1,
+ ):
+ bin_map.setdefault(d.warehouse, {}).setdefault(d.item_code, flt(d.stock_value))
return bin_map
+
def get_bin(item_code, warehouse):
bin = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse})
if not bin:
bin_obj = _create_bin(item_code, warehouse)
else:
- bin_obj = frappe.get_doc('Bin', bin, for_update=True)
+ bin_obj = frappe.get_doc("Bin", bin, for_update=True)
bin_obj.flags.ignore_permissions = True
return bin_obj
-def get_or_make_bin(item_code: str , warehouse: str) -> str:
- bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse})
+
+def get_or_make_bin(item_code: str, warehouse: str) -> str:
+ bin_record = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse})
if not bin_record:
bin_obj = _create_bin(item_code, warehouse)
bin_record = bin_obj.name
return bin_record
+
def _create_bin(item_code, warehouse):
"""Create a bin and take care of concurrent inserts."""
@@ -206,20 +253,24 @@ def _create_bin(item_code, warehouse):
return bin_obj
+
def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
"""WARNING: This function is deprecated. Inline this function instead of using it."""
from erpnext.stock.doctype.bin.bin import update_stock
- is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
+
+ is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
if is_stock_item:
bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
update_stock(bin_name, args, allow_negative_stock, via_landed_cost_voucher)
else:
frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))
+
@frappe.whitelist()
def get_incoming_rate(args, raise_error_if_no_rate=True):
"""Get Incoming Rate based on valuation method"""
from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate
+
if isinstance(args, string_types):
args = json.loads(args)
@@ -229,37 +280,53 @@ def get_incoming_rate(args, raise_error_if_no_rate=True):
else:
valuation_method = get_valuation_method(args.get("item_code"))
previous_sle = get_previous_sle(args)
- if valuation_method == 'FIFO':
+ if valuation_method == "FIFO":
if previous_sle:
- previous_stock_queue = json.loads(previous_sle.get('stock_queue', '[]') or '[]')
- in_rate = get_fifo_rate(previous_stock_queue, args.get("qty") or 0) if previous_stock_queue else 0
- elif valuation_method == 'Moving Average':
- in_rate = previous_sle.get('valuation_rate') or 0
+ previous_stock_queue = json.loads(previous_sle.get("stock_queue", "[]") or "[]")
+ in_rate = (
+ get_fifo_rate(previous_stock_queue, args.get("qty") or 0) if previous_stock_queue else 0
+ )
+ elif valuation_method == "Moving Average":
+ in_rate = previous_sle.get("valuation_rate") or 0
if not in_rate:
- voucher_no = args.get('voucher_no') or args.get('name')
- in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'),
- args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'),
- currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'),
- raise_error_if_no_rate=raise_error_if_no_rate)
+ voucher_no = args.get("voucher_no") or args.get("name")
+ in_rate = get_valuation_rate(
+ args.get("item_code"),
+ args.get("warehouse"),
+ args.get("voucher_type"),
+ voucher_no,
+ args.get("allow_zero_valuation"),
+ currency=erpnext.get_company_currency(args.get("company")),
+ company=args.get("company"),
+ raise_error_if_no_rate=raise_error_if_no_rate,
+ )
return flt(in_rate)
+
def get_avg_purchase_rate(serial_nos):
"""get average value of serial numbers"""
serial_nos = get_valid_serial_nos(serial_nos)
- return flt(frappe.db.sql("""select avg(purchase_rate) from `tabSerial No`
- where name in (%s)""" % ", ".join(["%s"] * len(serial_nos)),
- tuple(serial_nos))[0][0])
+ return flt(
+ frappe.db.sql(
+ """select avg(purchase_rate) from `tabSerial No`
+ where name in (%s)"""
+ % ", ".join(["%s"] * len(serial_nos)),
+ tuple(serial_nos),
+ )[0][0]
+ )
+
def get_valuation_method(item_code):
"""get valuation method from item or default"""
- val_method = frappe.db.get_value('Item', item_code, 'valuation_method', cache=True)
+ val_method = frappe.db.get_value("Item", item_code, "valuation_method", cache=True)
if not val_method:
val_method = frappe.db.get_value("Stock Settings", None, "valuation_method") or "FIFO"
return val_method
+
def get_fifo_rate(previous_stock_queue, qty):
"""get FIFO (average) Rate from Queue"""
if flt(qty) >= 0:
@@ -286,10 +353,11 @@ def get_fifo_rate(previous_stock_queue, qty):
return outgoing_cost / available_qty_for_outgoing
-def get_valid_serial_nos(sr_nos, qty=0, item_code=''):
+
+def get_valid_serial_nos(sr_nos, qty=0, item_code=""):
"""split serial nos, validate and return list of valid serial nos"""
# TODO: remove duplicates in client side
- serial_nos = cstr(sr_nos).strip().replace(',', '\n').split('\n')
+ serial_nos = cstr(sr_nos).strip().replace(",", "\n").split("\n")
valid_serial_nos = []
for val in serial_nos:
@@ -305,19 +373,29 @@ def get_valid_serial_nos(sr_nos, qty=0, item_code=''):
return valid_serial_nos
+
def validate_warehouse_company(warehouse, company):
warehouse_company = frappe.db.get_value("Warehouse", warehouse, "company", cache=True)
if warehouse_company and warehouse_company != company:
- frappe.throw(_("Warehouse {0} does not belong to company {1}").format(warehouse, company),
- InvalidWarehouseCompany)
+ frappe.throw(
+ _("Warehouse {0} does not belong to company {1}").format(warehouse, company),
+ InvalidWarehouseCompany,
+ )
+
def is_group_warehouse(warehouse):
if frappe.db.get_value("Warehouse", warehouse, "is_group", cache=True):
frappe.throw(_("Group node warehouse is not allowed to select for transactions"))
+
def validate_disabled_warehouse(warehouse):
if frappe.db.get_value("Warehouse", warehouse, "disabled", cache=True):
- frappe.throw(_("Disabled Warehouse {0} cannot be used for this transaction.").format(get_link_to_form('Warehouse', warehouse)))
+ frappe.throw(
+ _("Disabled Warehouse {0} cannot be used for this transaction.").format(
+ get_link_to_form("Warehouse", warehouse)
+ )
+ )
+
def update_included_uom_in_report(columns, result, include_uom, conversion_factors):
if not include_uom or not conversion_factors:
@@ -335,11 +413,14 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto
convertible_columns.setdefault(key, d.get("convertible"))
# Add new column to show qty/rate as per the selected UOM
- columns.insert(idx+1, {
- 'label': "{0} (per {1})".format(d.get("label"), include_uom),
- 'fieldname': "{0}_{1}".format(d.get("fieldname"), frappe.scrub(include_uom)),
- 'fieldtype': 'Currency' if d.get("convertible") == 'rate' else 'Float'
- })
+ columns.insert(
+ idx + 1,
+ {
+ "label": "{0} (per {1})".format(d.get("label"), include_uom),
+ "fieldname": "{0}_{1}".format(d.get("fieldname"), frappe.scrub(include_uom)),
+ "fieldtype": "Currency" if d.get("convertible") == "rate" else "Float",
+ },
+ )
update_dict_values = []
for row_idx, row in enumerate(result):
@@ -351,13 +432,13 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto
if not conversion_factors[row_idx]:
conversion_factors[row_idx] = 1
- if convertible_columns.get(key) == 'rate':
+ if convertible_columns.get(key) == "rate":
new_value = flt(value) * conversion_factors[row_idx]
else:
new_value = flt(value) / conversion_factors[row_idx]
if not is_dict_obj:
- row.insert(key+1, new_value)
+ row.insert(key + 1, new_value)
else:
new_key = "{0}_{1}".format(key, frappe.scrub(include_uom))
update_dict_values.append([row, new_key, new_value])
@@ -366,11 +447,17 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto
row, key, value = data
row[key] = value
+
def get_available_serial_nos(args):
- return frappe.db.sql(""" SELECT name from `tabSerial No`
+ return frappe.db.sql(
+ """ SELECT name from `tabSerial No`
WHERE item_code = %(item_code)s and warehouse = %(warehouse)s
and timestamp(purchase_date, purchase_time) <= timestamp(%(posting_date)s, %(posting_time)s)
- """, args, as_dict=1)
+ """,
+ args,
+ as_dict=1,
+ )
+
def add_additional_uom_columns(columns, result, include_uom, conversion_factors):
if not include_uom or not conversion_factors:
@@ -379,60 +466,54 @@ def add_additional_uom_columns(columns, result, include_uom, conversion_factors)
convertible_column_map = {}
for col_idx in list(reversed(range(0, len(columns)))):
col = columns[col_idx]
- if isinstance(col, dict) and col.get('convertible') in ['rate', 'qty']:
+ if isinstance(col, dict) and col.get("convertible") in ["rate", "qty"]:
next_col = col_idx + 1
columns.insert(next_col, col.copy())
- columns[next_col]['fieldname'] += '_alt'
- convertible_column_map[col.get('fieldname')] = frappe._dict({
- 'converted_col': columns[next_col]['fieldname'],
- 'for_type': col.get('convertible')
- })
- if col.get('convertible') == 'rate':
- columns[next_col]['label'] += ' (per {})'.format(include_uom)
+ columns[next_col]["fieldname"] += "_alt"
+ convertible_column_map[col.get("fieldname")] = frappe._dict(
+ {"converted_col": columns[next_col]["fieldname"], "for_type": col.get("convertible")}
+ )
+ if col.get("convertible") == "rate":
+ columns[next_col]["label"] += " (per {})".format(include_uom)
else:
- columns[next_col]['label'] += ' ({})'.format(include_uom)
+ columns[next_col]["label"] += " ({})".format(include_uom)
for row_idx, row in enumerate(result):
for convertible_col, data in convertible_column_map.items():
- conversion_factor = conversion_factors[row.get('item_code')] or 1
+ conversion_factor = conversion_factors[row.get("item_code")] or 1
for_type = data.for_type
value_before_conversion = row.get(convertible_col)
- if for_type == 'rate':
+ if for_type == "rate":
row[data.converted_col] = flt(value_before_conversion) * conversion_factor
else:
row[data.converted_col] = flt(value_before_conversion) / conversion_factor
result[row_idx] = row
+
def get_incoming_outgoing_rate_for_cancel(item_code, voucher_type, voucher_no, voucher_detail_no):
- outgoing_rate = frappe.db.sql("""SELECT abs(stock_value_difference / actual_qty)
+ outgoing_rate = frappe.db.sql(
+ """SELECT abs(stock_value_difference / actual_qty)
FROM `tabStock Ledger Entry`
WHERE voucher_type = %s and voucher_no = %s
and item_code = %s and voucher_detail_no = %s
ORDER BY CREATION DESC limit 1""",
- (voucher_type, voucher_no, item_code, voucher_detail_no))
+ (voucher_type, voucher_no, item_code, voucher_detail_no),
+ )
outgoing_rate = outgoing_rate[0][0] if outgoing_rate else 0.0
return outgoing_rate
+
def is_reposting_item_valuation_in_progress():
- reposting_in_progress = frappe.db.exists("Repost Item Valuation",
- {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
+ reposting_in_progress = frappe.db.exists(
+ "Repost Item Valuation", {"docstatus": 1, "status": ["in", ["Queued", "In Progress"]]}
+ )
if reposting_in_progress:
- frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1)
-
-
-def calculate_mapped_packed_items_return(return_doc):
- parent_items = set([item.parent_item for item in return_doc.packed_items])
- against_doc = frappe.get_doc(return_doc.doctype, return_doc.return_against)
-
- for original_bundle, returned_bundle in zip(against_doc.items, return_doc.items):
- if original_bundle.item_code in parent_items:
- for returned_packed_item, original_packed_item in zip(return_doc.packed_items, against_doc.packed_items):
- if returned_packed_item.parent_item == original_bundle.item_code:
- returned_packed_item.parent_detail_docname = returned_bundle.name
- returned_packed_item.qty = (original_packed_item.qty / original_bundle.qty) * returned_bundle.qty
+ frappe.msgprint(
+ _("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1
+ )
def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool:
@@ -440,22 +521,25 @@ def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool
filters = {
"docstatus": 1,
- "status": ["in", ["Queued","In Progress", "Failed"]],
+ "status": ["in", ["Queued", "In Progress"]],
"posting_date": ["<=", posting_date],
}
- reposting_pending = frappe.db.exists("Repost Item Valuation", filters)
+ reposting_pending = frappe.db.exists("Repost Item Valuation", filters)
if reposting_pending and throw_error:
- msg = _("Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.")
- frappe.msgprint(msg,
- raise_exception=PendingRepostingError,
- title="Stock Reposting Ongoing",
- indicator="red",
- primary_action={
- "label": _("Show pending entries"),
- "client_action": "erpnext.route_to_pending_reposts",
- "args": filters,
- }
- )
+ msg = _(
+ "Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later."
+ )
+ frappe.msgprint(
+ msg,
+ raise_exception=PendingRepostingError,
+ title="Stock Reposting Ongoing",
+ indicator="red",
+ primary_action={
+ "label": _("Show pending entries"),
+ "client_action": "erpnext.route_to_pending_reposts",
+ "args": filters,
+ },
+ )
return bool(reposting_pending)
diff --git a/erpnext/support/__init__.py b/erpnext/support/__init__.py
index b9a7c1e8cee..7b6845d2fd1 100644
--- a/erpnext/support/__init__.py
+++ b/erpnext/support/__init__.py
@@ -1,6 +1,5 @@
-
install_docs = [
- {'doctype':'Role', 'role_name':'Support Team', 'name':'Support Team'},
- {'doctype':'Role', 'role_name':'Maintenance User', 'name':'Maintenance User'},
- {'doctype':'Role', 'role_name':'Maintenance Manager', 'name':'Maintenance Manager'}
+ {"doctype": "Role", "role_name": "Support Team", "name": "Support Team"},
+ {"doctype": "Role", "role_name": "Maintenance User", "name": "Maintenance User"},
+ {"doctype": "Role", "role_name": "Maintenance Manager", "name": "Maintenance Manager"},
]
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index 744b2989ff2..62e8d342f94 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -67,8 +67,9 @@ class Issue(Document):
self.customer = contact.get_link_for("Customer")
if not self.company:
- self.company = frappe.db.get_value("Lead", self.lead, "company") or \
- frappe.db.get_default("Company")
+ self.company = frappe.db.get_value("Lead", self.lead, "company") or frappe.db.get_default(
+ "Company"
+ )
def reset_sla_fields(self):
self.agreement_status = ""
@@ -103,19 +104,20 @@ class Issue(Document):
def handle_hold_time(self, status):
if self.service_level_agreement:
# set response and resolution variance as None as the issue is on Hold
- pause_sla_on = frappe.db.get_all("Pause SLA On Status", fields=["status"],
- filters={"parent": self.service_level_agreement})
+ pause_sla_on = frappe.db.get_all(
+ "Pause SLA On Status", fields=["status"], filters={"parent": self.service_level_agreement}
+ )
hold_statuses = [entry.status for entry in pause_sla_on]
update_values = {}
if hold_statuses:
if self.status in hold_statuses and status not in hold_statuses:
- update_values['on_hold_since'] = frappe.flags.current_time or now_datetime()
+ update_values["on_hold_since"] = frappe.flags.current_time or now_datetime()
if not self.first_responded_on:
- update_values['response_by'] = None
- update_values['response_by_variance'] = 0
- update_values['resolution_by'] = None
- update_values['resolution_by_variance'] = 0
+ update_values["response_by"] = None
+ update_values["response_by_variance"] = 0
+ update_values["resolution_by"] = None
+ update_values["resolution_by_variance"] = 0
# calculate hold time when status is changed from any hold status to any non-hold status
if self.status not in hold_statuses and status in hold_statuses:
@@ -125,7 +127,7 @@ class Issue(Document):
if self.on_hold_since:
# last_hold_time will be added to the sla variables
last_hold_time = time_diff_in_seconds(now_time, self.on_hold_since)
- update_values['total_hold_time'] = hold_time + last_hold_time
+ update_values["total_hold_time"] = hold_time + last_hold_time
# re-calculate SLA variables after issue changes from any hold status to any non-hold status
# add hold time to SLA variables
@@ -134,25 +136,31 @@ class Issue(Document):
now_time = frappe.flags.current_time or now_datetime()
if not self.first_responded_on:
- response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
+ response_by = get_expected_time_for(
+ parameter="response", service_level=priority, start_date_time=start_date_time
+ )
response_by = add_to_date(response_by, seconds=round(last_hold_time))
response_by_variance = round(time_diff_in_seconds(response_by, now_time))
- update_values['response_by'] = response_by
- update_values['response_by_variance'] = response_by_variance + last_hold_time
+ update_values["response_by"] = response_by
+ update_values["response_by_variance"] = response_by_variance + last_hold_time
- resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
+ resolution_by = get_expected_time_for(
+ parameter="resolution", service_level=priority, start_date_time=start_date_time
+ )
resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time))
resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time))
- update_values['resolution_by'] = resolution_by
- update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time
- update_values['on_hold_since'] = None
+ update_values["resolution_by"] = resolution_by
+ update_values["resolution_by_variance"] = resolution_by_variance + last_hold_time
+ update_values["on_hold_since"] = None
self.db_set(update_values)
def update_agreement_status(self):
if self.service_level_agreement and self.agreement_status == "Ongoing":
- if cint(frappe.db.get_value("Issue", self.name, "response_by_variance")) < 0 or \
- cint(frappe.db.get_value("Issue", self.name, "resolution_by_variance")) < 0:
+ if (
+ cint(frappe.db.get_value("Issue", self.name, "response_by_variance")) < 0
+ or cint(frappe.db.get_value("Issue", self.name, "resolution_by_variance")) < 0
+ ):
self.agreement_status = "Failed"
else:
@@ -160,30 +168,34 @@ class Issue(Document):
def update_agreement_status_on_custom_status(self):
"""
- Update Agreement Fulfilled status using Custom Scripts for Custom Issue Status
+ Update Agreement Fulfilled status using Custom Scripts for Custom Issue Status
"""
- if not self.first_responded_on: # first_responded_on set when first reply is sent to customer
+ if not self.first_responded_on: # first_responded_on set when first reply is sent to customer
self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()), 2)
- if not self.resolution_date: # resolution_date set when issue has been closed
+ if not self.resolution_date: # resolution_date set when issue has been closed
self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime()), 2)
- self.agreement_status = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed"
+ self.agreement_status = (
+ "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed"
+ )
def create_communication(self):
communication = frappe.new_doc("Communication")
- communication.update({
- "communication_type": "Communication",
- "communication_medium": "Email",
- "sent_or_received": "Received",
- "email_status": "Open",
- "subject": self.subject,
- "sender": self.raised_by,
- "content": self.description,
- "status": "Linked",
- "reference_doctype": "Issue",
- "reference_name": self.name
- })
+ communication.update(
+ {
+ "communication_type": "Communication",
+ "communication_medium": "Email",
+ "sent_or_received": "Received",
+ "email_status": "Open",
+ "subject": self.subject,
+ "sender": self.raised_by,
+ "content": self.description,
+ "status": "Linked",
+ "reference_doctype": "Issue",
+ "reference_name": self.name,
+ }
+ )
communication.ignore_permissions = True
communication.ignore_mandatory = True
communication.save()
@@ -216,23 +228,31 @@ class Issue(Document):
# Replicate linked Communications
# TODO: get all communications in timeline before this, and modify them to append them to new doc
comm_to_split_from = frappe.get_doc("Communication", communication_id)
- communications = frappe.get_all("Communication",
- filters={"reference_doctype": "Issue",
+ communications = frappe.get_all(
+ "Communication",
+ filters={
+ "reference_doctype": "Issue",
"reference_name": comm_to_split_from.reference_name,
- "creation": (">=", comm_to_split_from.creation)})
+ "creation": (">=", comm_to_split_from.creation),
+ },
+ )
for communication in communications:
doc = frappe.get_doc("Communication", communication.name)
doc.reference_name = replicated_issue.name
doc.save(ignore_permissions=True)
- frappe.get_doc({
- "doctype": "Comment",
- "comment_type": "Info",
- "reference_doctype": "Issue",
- "reference_name": replicated_issue.name,
- "content": " - Split the Issue from {1}".format(self.name, frappe.bold(self.name)),
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Comment",
+ "comment_type": "Info",
+ "reference_doctype": "Issue",
+ "reference_name": replicated_issue.name,
+ "content": " - Split the Issue from {1}".format(
+ self.name, frappe.bold(self.name)
+ ),
+ }
+ ).insert(ignore_permissions=True)
return replicated_issue.name
@@ -243,7 +263,9 @@ class Issue(Document):
def before_insert(self):
if frappe.db.get_single_value("Support Settings", "track_service_level_agreement"):
if frappe.flags.in_test:
- self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
+ self.set_response_and_resolution_time(
+ priority=self.priority, service_level_agreement=self.service_level_agreement
+ )
else:
self.set_response_and_resolution_time()
@@ -252,11 +274,19 @@ class Issue(Document):
if not service_level_agreement:
if frappe.db.get_value("Issue", self.name, "service_level_agreement"):
- frappe.throw(_("Couldn't Set Service Level Agreement {0}.").format(self.service_level_agreement))
+ frappe.throw(
+ _("Couldn't Set Service Level Agreement {0}.").format(self.service_level_agreement)
+ )
return
- if (service_level_agreement.customer and self.customer) and not (service_level_agreement.customer == self.customer):
- frappe.throw(_("This Service Level Agreement is specific to Customer {0}").format(service_level_agreement.customer))
+ if (service_level_agreement.customer and self.customer) and not (
+ service_level_agreement.customer == self.customer
+ ):
+ frappe.throw(
+ _("This Service Level Agreement is specific to Customer {0}").format(
+ service_level_agreement.customer
+ )
+ )
self.service_level_agreement = service_level_agreement.name
if not self.priority:
@@ -269,40 +299,59 @@ class Issue(Document):
self.service_level_agreement_creation = now_datetime()
start_date_time = get_datetime(self.service_level_agreement_creation)
- self.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
- self.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
+ self.response_by = get_expected_time_for(
+ parameter="response", service_level=priority, start_date_time=start_date_time
+ )
+ self.resolution_by = get_expected_time_for(
+ parameter="resolution", service_level=priority, start_date_time=start_date_time
+ )
self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()))
self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime()))
def change_service_level_agreement_and_priority(self):
- if self.service_level_agreement and frappe.db.exists("Issue", self.name) and \
- frappe.db.get_single_value("Support Settings", "track_service_level_agreement"):
+ if (
+ self.service_level_agreement
+ and frappe.db.exists("Issue", self.name)
+ and frappe.db.get_single_value("Support Settings", "track_service_level_agreement")
+ ):
if not self.priority == frappe.db.get_value("Issue", self.name, "priority"):
- self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
+ self.set_response_and_resolution_time(
+ priority=self.priority, service_level_agreement=self.service_level_agreement
+ )
frappe.msgprint(_("Priority has been changed to {0}.").format(self.priority))
- if not self.service_level_agreement == frappe.db.get_value("Issue", self.name, "service_level_agreement"):
- self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
- frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement))
+ if not self.service_level_agreement == frappe.db.get_value(
+ "Issue", self.name, "service_level_agreement"
+ ):
+ self.set_response_and_resolution_time(
+ priority=self.priority, service_level_agreement=self.service_level_agreement
+ )
+ frappe.msgprint(
+ _("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement)
+ )
@frappe.whitelist()
def reset_service_level_agreement(self, reason, user):
if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"):
frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings."))
- frappe.get_doc({
- "doctype": "Comment",
- "comment_type": "Info",
- "reference_doctype": self.doctype,
- "reference_name": self.name,
- "comment_email": user,
- "content": " resetted Service Level Agreement - {0}".format(_(reason)),
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Comment",
+ "comment_type": "Info",
+ "reference_doctype": self.doctype,
+ "reference_name": self.name,
+ "comment_email": user,
+ "content": " resetted Service Level Agreement - {0}".format(_(reason)),
+ }
+ ).insert(ignore_permissions=True)
self.service_level_agreement_creation = now_datetime()
- self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
+ self.set_response_and_resolution_time(
+ priority=self.priority, service_level_agreement=self.service_level_agreement
+ )
self.agreement_status = "Ongoing"
self.save()
@@ -310,10 +359,12 @@ class Issue(Document):
def get_priority(issue):
service_level_agreement = frappe.get_doc("Service Level Agreement", issue.service_level_agreement)
priority = service_level_agreement.get_service_level_agreement_priority(issue.priority)
- priority.update({
- "support_and_resolution": service_level_agreement.support_and_resolution,
- "holiday_list": service_level_agreement.holiday_list
- })
+ priority.update(
+ {
+ "support_and_resolution": service_level_agreement.support_and_resolution,
+ "holiday_list": service_level_agreement.holiday_list,
+ }
+ )
return priority
@@ -334,10 +385,12 @@ def get_expected_time_for(parameter, service_level, start_date_time):
support_days = {}
for service in service_level.get("support_and_resolution"):
- support_days[service.workday] = frappe._dict({
- "start_time": service.start_time,
- "end_time": service.end_time,
- })
+ support_days[service.workday] = frappe._dict(
+ {
+ "start_time": service.start_time,
+ "end_time": service.end_time,
+ }
+ )
holidays = get_holidays(service_level.get("holiday_list"))
weekdays = get_weekdays()
@@ -346,14 +399,19 @@ def get_expected_time_for(parameter, service_level, start_date_time):
current_weekday = weekdays[current_date_time.weekday()]
if not is_holiday(current_date_time, holidays) and current_weekday in support_days:
- start_time = current_date_time - datetime(current_date_time.year, current_date_time.month, current_date_time.day) \
- if getdate(current_date_time) == getdate(start_date_time) and get_time_in_timedelta(current_date_time.time()) > support_days[current_weekday].start_time \
+ start_time = (
+ current_date_time
+ - datetime(current_date_time.year, current_date_time.month, current_date_time.day)
+ if getdate(current_date_time) == getdate(start_date_time)
+ and get_time_in_timedelta(current_date_time.time()) > support_days[current_weekday].start_time
else support_days[current_weekday].start_time
+ )
end_time = support_days[current_weekday].end_time
time_left_today = time_diff_in_seconds(end_time, start_time)
# no time left for support today
- if time_left_today <= 0: pass
+ if time_left_today <= 0:
+ pass
elif allotted_seconds:
if time_left_today >= allotted_seconds:
expected_time = datetime.combine(getdate(current_date_time), get_time(start_time))
@@ -372,6 +430,7 @@ def get_expected_time_for(parameter, service_level, start_date_time):
return current_date_time
+
def set_service_level_agreement_variance(issue=None):
current_time = frappe.flags.current_time or now_datetime()
@@ -382,17 +441,25 @@ def set_service_level_agreement_variance(issue=None):
for issue in frappe.get_list("Issue", filters=filters):
doc = frappe.get_doc("Issue", issue.name)
- if not doc.first_responded_on: # first_responded_on set when first reply is sent to customer
+ if not doc.first_responded_on: # first_responded_on set when first reply is sent to customer
variance = round(time_diff_in_seconds(doc.response_by, current_time), 2)
- frappe.db.set_value(dt="Issue", dn=doc.name, field="response_by_variance", val=variance, update_modified=False)
+ frappe.db.set_value(
+ dt="Issue", dn=doc.name, field="response_by_variance", val=variance, update_modified=False
+ )
if variance < 0:
- frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False)
+ frappe.db.set_value(
+ dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False
+ )
- if not doc.resolution_date: # resolution_date set when issue has been closed
+ if not doc.resolution_date: # resolution_date set when issue has been closed
variance = round(time_diff_in_seconds(doc.resolution_by, current_time), 2)
- frappe.db.set_value(dt="Issue", dn=doc.name, field="resolution_by_variance", val=variance, update_modified=False)
+ frappe.db.set_value(
+ dt="Issue", dn=doc.name, field="resolution_by_variance", val=variance, update_modified=False
+ )
if variance < 0:
- frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False)
+ frappe.db.set_value(
+ dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False
+ )
def set_resolution_time(issue):
@@ -403,18 +470,20 @@ def set_resolution_time(issue):
def set_user_resolution_time(issue):
# total time taken by a user to close the issue apart from wait_time
- communications = frappe.get_list("Communication", filters={
- "reference_doctype": issue.doctype,
- "reference_name": issue.name
- },
+ communications = frappe.get_list(
+ "Communication",
+ filters={"reference_doctype": issue.doctype, "reference_name": issue.name},
fields=["sent_or_received", "name", "creation"],
- order_by="creation"
+ order_by="creation",
)
pending_time = []
for i in range(len(communications)):
- if communications[i].sent_or_received == "Received" and communications[i-1].sent_or_received == "Sent":
- wait_time = time_diff_in_seconds(communications[i].creation, communications[i-1].creation)
+ if (
+ communications[i].sent_or_received == "Received"
+ and communications[i - 1].sent_or_received == "Sent"
+ ):
+ wait_time = time_diff_in_seconds(communications[i].creation, communications[i - 1].creation)
if wait_time > 0:
pending_time.append(wait_time)
@@ -431,7 +500,7 @@ def get_list_context(context=None):
"row_template": "templates/includes/issue_row.html",
"show_sidebar": True,
"show_search": True,
- "no_breadcrumbs": True
+ "no_breadcrumbs": True,
}
@@ -448,7 +517,8 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord
ignore_permissions = False
if is_website_user():
- if not filters: filters = {}
+ if not filters:
+ filters = {}
if customer:
filters["customer"] = customer
@@ -457,7 +527,9 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord
ignore_permissions = True
- return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions)
+ return get_list(
+ doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions
+ )
@frappe.whitelist()
@@ -466,18 +538,26 @@ def set_multiple_status(names, status):
for name in names:
set_status(name, status)
+
@frappe.whitelist()
def set_status(name, status):
st = frappe.get_doc("Issue", name)
st.status = status
st.save()
+
def auto_close_tickets():
"""Auto-close replied support tickets after 7 days"""
- auto_close_after_days = frappe.db.get_value("Support Settings", "Support Settings", "close_issue_after_days") or 7
+ auto_close_after_days = (
+ frappe.db.get_value("Support Settings", "Support Settings", "close_issue_after_days") or 7
+ )
- issues = frappe.db.sql(""" select name from tabIssue where status='Replied' and
- modified resolution:
- frappe.throw(_("Response Time for {0} priority in row {1} can't be greater than Resolution Time.").format(priority.priority, priority.idx))
+ frappe.throw(
+ _("Response Time for {0} priority in row {1} can't be greater than Resolution Time.").format(
+ priority.priority, priority.idx
+ )
+ )
# Check if repeated priority
if not len(set(priorities)) == len(priorities):
@@ -59,15 +66,27 @@ class ServiceLevelAgreement(Document):
for support_and_resolution in self.support_and_resolution:
# Check if start and end time is set for every support day
if not (support_and_resolution.start_time or support_and_resolution.end_time):
- frappe.throw(_("Set Start Time and End Time for \
- Support Day {0} at index {1}.".format(support_and_resolution.workday, support_and_resolution.idx)))
+ frappe.throw(
+ _(
+ "Set Start Time and End Time for \
+ Support Day {0} at index {1}.".format(
+ support_and_resolution.workday, support_and_resolution.idx
+ )
+ )
+ )
support_days.append(support_and_resolution.workday)
support_and_resolution.idx = week.index(support_and_resolution.workday) + 1
if support_and_resolution.start_time >= support_and_resolution.end_time:
- frappe.throw(_("Start Time can't be greater than or equal to End Time \
- for {0}.".format(support_and_resolution.workday)))
+ frappe.throw(
+ _(
+ "Start Time can't be greater than or equal to End Time \
+ for {0}.".format(
+ support_and_resolution.workday
+ )
+ )
+ )
# Check for repeated workday
if not len(set(support_days)) == len(support_days):
@@ -75,12 +94,21 @@ class ServiceLevelAgreement(Document):
frappe.throw(_("Workday {0} has been repeated.").format(repeated_days))
def validate_doc(self):
- if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement") and self.enable:
- frappe.throw(_("{0} is not enabled in {1}").format(frappe.bold("Track Service Level Agreement"),
- get_link_to_form("Support Settings", "Support Settings")))
+ if (
+ not frappe.db.get_single_value("Support Settings", "track_service_level_agreement")
+ and self.enable
+ ):
+ frappe.throw(
+ _("{0} is not enabled in {1}").format(
+ frappe.bold("Track Service Level Agreement"),
+ get_link_to_form("Support Settings", "Support Settings"),
+ )
+ )
if self.default_service_level_agreement:
- if frappe.db.exists("Service Level Agreement", {"default_service_level_agreement": "1", "name": ["!=", self.name]}):
+ if frappe.db.exists(
+ "Service Level Agreement", {"default_service_level_agreement": "1", "name": ["!=", self.name]}
+ ):
frappe.throw(_("A Default Service Level Agreement already exists."))
else:
if self.start_date and self.end_date:
@@ -91,11 +119,18 @@ class ServiceLevelAgreement(Document):
frappe.throw(_("End Date of Agreement can't be less than today."))
if self.entity_type and self.entity:
- if frappe.db.exists("Service Level Agreement", {"entity_type": self.entity_type, "entity": self.entity, "name": ["!=", self.name]}):
- frappe.throw(_("Service Level Agreement with Entity Type {0} and Entity {1} already exists.").format(self.entity_type, self.entity))
+ if frappe.db.exists(
+ "Service Level Agreement",
+ {"entity_type": self.entity_type, "entity": self.entity, "name": ["!=", self.name]},
+ ):
+ frappe.throw(
+ _("Service Level Agreement with Entity Type {0} and Entity {1} already exists.").format(
+ self.entity_type, self.entity
+ )
+ )
def validate_condition(self):
- temp_doc = frappe.new_doc('Issue')
+ temp_doc = frappe.new_doc("Issue")
if self.condition:
try:
frappe.safe_eval(self.condition, None, get_context(temp_doc))
@@ -105,58 +140,77 @@ class ServiceLevelAgreement(Document):
def get_service_level_agreement_priority(self, priority):
priority = frappe.get_doc("Service Level Priority", {"priority": priority, "parent": self.name})
- return frappe._dict({
- "priority": priority.priority,
- "response_time": priority.response_time,
- "resolution_time": priority.resolution_time
- })
+ return frappe._dict(
+ {
+ "priority": priority.priority,
+ "response_time": priority.response_time,
+ "resolution_time": priority.resolution_time,
+ }
+ )
+
def check_agreement_status():
- service_level_agreements = frappe.get_list("Service Level Agreement", filters=[
- {"active": 1},
- {"default_service_level_agreement": 0}
- ], fields=["name"])
+ service_level_agreements = frappe.get_list(
+ "Service Level Agreement",
+ filters=[{"active": 1}, {"default_service_level_agreement": 0}],
+ fields=["name"],
+ )
for service_level_agreement in service_level_agreements:
doc = frappe.get_doc("Service Level Agreement", service_level_agreement.name)
if doc.end_date and getdate(doc.end_date) < getdate(frappe.utils.getdate()):
frappe.db.set_value("Service Level Agreement", service_level_agreement.name, "active", 0)
+
def get_active_service_level_agreement_for(doc):
if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"):
return
filters = [
["Service Level Agreement", "active", "=", 1],
- ["Service Level Agreement", "enable", "=", 1]
+ ["Service Level Agreement", "enable", "=", 1],
]
- if doc.get('priority'):
- filters.append(["Service Level Priority", "priority", "=", doc.get('priority')])
+ if doc.get("priority"):
+ filters.append(["Service Level Priority", "priority", "=", doc.get("priority")])
- customer = doc.get('customer')
+ customer = doc.get("customer")
or_filters = [
- ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]]
+ [
+ "Service Level Agreement",
+ "entity",
+ "in",
+ [customer, get_customer_group(customer), get_customer_territory(customer)],
+ ]
]
- service_level_agreement = doc.get('service_level_agreement')
+ service_level_agreement = doc.get("service_level_agreement")
if service_level_agreement:
or_filters = [
- ["Service Level Agreement", "name", "=", doc.get('service_level_agreement')],
+ ["Service Level Agreement", "name", "=", doc.get("service_level_agreement")],
]
- default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]]
- default_sla = frappe.get_all("Service Level Agreement", filters=default_sla_filter,
- fields=["name", "default_priority", "condition"])
+ default_sla_filter = filters + [
+ ["Service Level Agreement", "default_service_level_agreement", "=", 1]
+ ]
+ default_sla = frappe.get_all(
+ "Service Level Agreement",
+ filters=default_sla_filter,
+ fields=["name", "default_priority", "condition"],
+ )
filters += [["Service Level Agreement", "default_service_level_agreement", "=", 0]]
- agreements = frappe.get_all("Service Level Agreement", filters=filters, or_filters=or_filters,
- fields=["name", "default_priority", "condition"])
+ agreements = frappe.get_all(
+ "Service Level Agreement",
+ filters=filters,
+ or_filters=or_filters,
+ fields=["name", "default_priority", "condition"],
+ )
# check if the current document on which SLA is to be applied fulfills all the conditions
filtered_agreements = []
for agreement in agreements:
- condition = agreement.get('condition')
+ condition = agreement.get("condition")
if not condition or (condition and frappe.safe_eval(condition, None, get_context(doc))):
filtered_agreements.append(agreement)
@@ -165,17 +219,25 @@ def get_active_service_level_agreement_for(doc):
return filtered_agreements[0] if filtered_agreements else None
+
def get_context(doc):
- return {"doc": doc.as_dict(), "nowdate": nowdate, "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils"))}
+ return {
+ "doc": doc.as_dict(),
+ "nowdate": nowdate,
+ "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils")),
+ }
+
def get_customer_group(customer):
if customer:
return frappe.db.get_value("Customer", customer, "customer_group")
+
def get_customer_territory(customer):
if customer:
return frappe.db.get_value("Customer", customer, "territory")
+
@frappe.whitelist()
def get_service_level_agreement_filters(name, customer=None):
if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"):
@@ -183,25 +245,37 @@ def get_service_level_agreement_filters(name, customer=None):
filters = [
["Service Level Agreement", "active", "=", 1],
- ["Service Level Agreement", "enable", "=", 1]
+ ["Service Level Agreement", "enable", "=", 1],
]
if not customer:
- or_filters = [
- ["Service Level Agreement", "default_service_level_agreement", "=", 1]
- ]
+ or_filters = [["Service Level Agreement", "default_service_level_agreement", "=", 1]]
else:
# Include SLA with No Entity and Entity Type
or_filters = [
- ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer), ""]],
- ["Service Level Agreement", "default_service_level_agreement", "=", 1]
+ [
+ "Service Level Agreement",
+ "entity",
+ "in",
+ [customer, get_customer_group(customer), get_customer_territory(customer), ""],
+ ],
+ ["Service Level Agreement", "default_service_level_agreement", "=", 1],
]
return {
- "priority": [priority.priority for priority in frappe.get_list("Service Level Priority", filters={"parent": name}, fields=["priority"])],
- "service_level_agreements": [d.name for d in frappe.get_list("Service Level Agreement", filters=filters, or_filters=or_filters)]
+ "priority": [
+ priority.priority
+ for priority in frappe.get_list(
+ "Service Level Priority", filters={"parent": name}, fields=["priority"]
+ )
+ ],
+ "service_level_agreements": [
+ d.name
+ for d in frappe.get_list("Service Level Agreement", filters=filters, or_filters=or_filters)
+ ],
}
+
def get_repeated(values):
unique_list = []
diff = []
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py
index 22e2c374e12..8fd3025c30a 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py
@@ -3,11 +3,6 @@ from frappe import _
def get_data():
return {
- 'fieldname': 'service_level_agreement',
- 'transactions': [
- {
- 'label': _('Issue'),
- 'items': ['Issue']
- }
- ]
+ "fieldname": "service_level_agreement",
+ "transactions": [{"label": _("Issue"), "items": ["Issue"]}],
}
diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
index 4c4a684333f..0137be08f8d 100644
--- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
@@ -16,55 +16,131 @@ class TestServiceLevelAgreement(unittest.TestCase):
def test_service_level_agreement(self):
# Default Service Level Agreement
- create_default_service_level_agreement = create_service_level_agreement(default_service_level_agreement=1,
- holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
- entity_type=None, entity=None, response_time=14400, resolution_time=21600)
+ create_default_service_level_agreement = create_service_level_agreement(
+ default_service_level_agreement=1,
+ holiday_list="__Test Holiday List",
+ employee_group="_Test Employee Group",
+ entity_type=None,
+ entity=None,
+ response_time=14400,
+ resolution_time=21600,
+ )
- get_default_service_level_agreement = get_service_level_agreement(default_service_level_agreement=1)
+ get_default_service_level_agreement = get_service_level_agreement(
+ default_service_level_agreement=1
+ )
- self.assertEqual(create_default_service_level_agreement.name, get_default_service_level_agreement.name)
- self.assertEqual(create_default_service_level_agreement.entity_type, get_default_service_level_agreement.entity_type)
- self.assertEqual(create_default_service_level_agreement.entity, get_default_service_level_agreement.entity)
- self.assertEqual(create_default_service_level_agreement.default_service_level_agreement, get_default_service_level_agreement.default_service_level_agreement)
+ self.assertEqual(
+ create_default_service_level_agreement.name, get_default_service_level_agreement.name
+ )
+ self.assertEqual(
+ create_default_service_level_agreement.entity_type,
+ get_default_service_level_agreement.entity_type,
+ )
+ self.assertEqual(
+ create_default_service_level_agreement.entity, get_default_service_level_agreement.entity
+ )
+ self.assertEqual(
+ create_default_service_level_agreement.default_service_level_agreement,
+ get_default_service_level_agreement.default_service_level_agreement,
+ )
# Service Level Agreement for Customer
customer = create_customer()
- create_customer_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0,
- holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
- entity_type="Customer", entity=customer, response_time=7200, resolution_time=10800)
- get_customer_service_level_agreement = get_service_level_agreement(entity_type="Customer", entity=customer)
+ create_customer_service_level_agreement = create_service_level_agreement(
+ default_service_level_agreement=0,
+ holiday_list="__Test Holiday List",
+ employee_group="_Test Employee Group",
+ entity_type="Customer",
+ entity=customer,
+ response_time=7200,
+ resolution_time=10800,
+ )
+ get_customer_service_level_agreement = get_service_level_agreement(
+ entity_type="Customer", entity=customer
+ )
- self.assertEqual(create_customer_service_level_agreement.name, get_customer_service_level_agreement.name)
- self.assertEqual(create_customer_service_level_agreement.entity_type, get_customer_service_level_agreement.entity_type)
- self.assertEqual(create_customer_service_level_agreement.entity, get_customer_service_level_agreement.entity)
- self.assertEqual(create_customer_service_level_agreement.default_service_level_agreement, get_customer_service_level_agreement.default_service_level_agreement)
+ self.assertEqual(
+ create_customer_service_level_agreement.name, get_customer_service_level_agreement.name
+ )
+ self.assertEqual(
+ create_customer_service_level_agreement.entity_type,
+ get_customer_service_level_agreement.entity_type,
+ )
+ self.assertEqual(
+ create_customer_service_level_agreement.entity, get_customer_service_level_agreement.entity
+ )
+ self.assertEqual(
+ create_customer_service_level_agreement.default_service_level_agreement,
+ get_customer_service_level_agreement.default_service_level_agreement,
+ )
# Service Level Agreement for Customer Group
customer_group = create_customer_group()
- create_customer_group_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0,
- holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
- entity_type="Customer Group", entity=customer_group, response_time=7200, resolution_time=10800)
- get_customer_group_service_level_agreement = get_service_level_agreement(entity_type="Customer Group", entity=customer_group)
+ create_customer_group_service_level_agreement = create_service_level_agreement(
+ default_service_level_agreement=0,
+ holiday_list="__Test Holiday List",
+ employee_group="_Test Employee Group",
+ entity_type="Customer Group",
+ entity=customer_group,
+ response_time=7200,
+ resolution_time=10800,
+ )
+ get_customer_group_service_level_agreement = get_service_level_agreement(
+ entity_type="Customer Group", entity=customer_group
+ )
- self.assertEqual(create_customer_group_service_level_agreement.name, get_customer_group_service_level_agreement.name)
- self.assertEqual(create_customer_group_service_level_agreement.entity_type, get_customer_group_service_level_agreement.entity_type)
- self.assertEqual(create_customer_group_service_level_agreement.entity, get_customer_group_service_level_agreement.entity)
- self.assertEqual(create_customer_group_service_level_agreement.default_service_level_agreement, get_customer_group_service_level_agreement.default_service_level_agreement)
+ self.assertEqual(
+ create_customer_group_service_level_agreement.name,
+ get_customer_group_service_level_agreement.name,
+ )
+ self.assertEqual(
+ create_customer_group_service_level_agreement.entity_type,
+ get_customer_group_service_level_agreement.entity_type,
+ )
+ self.assertEqual(
+ create_customer_group_service_level_agreement.entity,
+ get_customer_group_service_level_agreement.entity,
+ )
+ self.assertEqual(
+ create_customer_group_service_level_agreement.default_service_level_agreement,
+ get_customer_group_service_level_agreement.default_service_level_agreement,
+ )
# Service Level Agreement for Territory
territory = create_territory()
- create_territory_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0,
- holiday_list="__Test Holiday List", employee_group="_Test Employee Group",
- entity_type="Territory", entity=territory, response_time=7200, resolution_time=10800)
- get_territory_service_level_agreement = get_service_level_agreement(entity_type="Territory", entity=territory)
+ create_territory_service_level_agreement = create_service_level_agreement(
+ default_service_level_agreement=0,
+ holiday_list="__Test Holiday List",
+ employee_group="_Test Employee Group",
+ entity_type="Territory",
+ entity=territory,
+ response_time=7200,
+ resolution_time=10800,
+ )
+ get_territory_service_level_agreement = get_service_level_agreement(
+ entity_type="Territory", entity=territory
+ )
- self.assertEqual(create_territory_service_level_agreement.name, get_territory_service_level_agreement.name)
- self.assertEqual(create_territory_service_level_agreement.entity_type, get_territory_service_level_agreement.entity_type)
- self.assertEqual(create_territory_service_level_agreement.entity, get_territory_service_level_agreement.entity)
- self.assertEqual(create_territory_service_level_agreement.default_service_level_agreement, get_territory_service_level_agreement.default_service_level_agreement)
+ self.assertEqual(
+ create_territory_service_level_agreement.name, get_territory_service_level_agreement.name
+ )
+ self.assertEqual(
+ create_territory_service_level_agreement.entity_type,
+ get_territory_service_level_agreement.entity_type,
+ )
+ self.assertEqual(
+ create_territory_service_level_agreement.entity, get_territory_service_level_agreement.entity
+ )
+ self.assertEqual(
+ create_territory_service_level_agreement.default_service_level_agreement,
+ get_territory_service_level_agreement.default_service_level_agreement,
+ )
-def get_service_level_agreement(default_service_level_agreement=None, entity_type=None, entity=None):
+def get_service_level_agreement(
+ default_service_level_agreement=None, entity_type=None, entity=None
+):
if default_service_level_agreement:
filters = {"default_service_level_agreement": default_service_level_agreement}
else:
@@ -73,93 +149,96 @@ def get_service_level_agreement(default_service_level_agreement=None, entity_typ
service_level_agreement = frappe.get_doc("Service Level Agreement", filters)
return service_level_agreement
-def create_service_level_agreement(default_service_level_agreement, holiday_list, employee_group,
- response_time, entity_type, entity, resolution_time):
+
+def create_service_level_agreement(
+ default_service_level_agreement,
+ holiday_list,
+ employee_group,
+ response_time,
+ entity_type,
+ entity,
+ resolution_time,
+):
employee_group = make_employee_group()
make_holiday_list()
make_priorities()
- service_level_agreement = frappe.get_doc({
- "doctype": "Service Level Agreement",
- "enable": 1,
- "service_level": "__Test Service Level",
- "default_service_level_agreement": default_service_level_agreement,
- "default_priority": "Medium",
- "holiday_list": holiday_list,
- "employee_group": employee_group,
- "entity_type": entity_type,
- "entity": entity,
- "start_date": frappe.utils.getdate(),
- "end_date": frappe.utils.add_to_date(frappe.utils.getdate(), days=100),
- "priorities": [
- {
- "priority": "Low",
- "response_time": response_time,
- "response_time_period": "Hour",
- "resolution_time": resolution_time,
- "resolution_time_period": "Hour",
- },
- {
- "priority": "Medium",
- "response_time": response_time,
- "default_priority": 1,
- "response_time_period": "Hour",
- "resolution_time": resolution_time,
- "resolution_time_period": "Hour",
- },
- {
- "priority": "High",
- "response_time": response_time,
- "response_time_period": "Hour",
- "resolution_time": resolution_time,
- "resolution_time_period": "Hour",
- }
- ],
- "pause_sla_on": [
- {
- "status": "Replied"
- }
- ],
- "support_and_resolution": [
- {
- "workday": "Monday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
- },
- {
- "workday": "Tuesday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
- },
- {
- "workday": "Wednesday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
- },
- {
- "workday": "Thursday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
- },
- {
- "workday": "Friday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
- }
- ]
- })
+ service_level_agreement = frappe.get_doc(
+ {
+ "doctype": "Service Level Agreement",
+ "enable": 1,
+ "service_level": "__Test Service Level",
+ "default_service_level_agreement": default_service_level_agreement,
+ "default_priority": "Medium",
+ "holiday_list": holiday_list,
+ "employee_group": employee_group,
+ "entity_type": entity_type,
+ "entity": entity,
+ "start_date": frappe.utils.getdate(),
+ "end_date": frappe.utils.add_to_date(frappe.utils.getdate(), days=100),
+ "priorities": [
+ {
+ "priority": "Low",
+ "response_time": response_time,
+ "response_time_period": "Hour",
+ "resolution_time": resolution_time,
+ "resolution_time_period": "Hour",
+ },
+ {
+ "priority": "Medium",
+ "response_time": response_time,
+ "default_priority": 1,
+ "response_time_period": "Hour",
+ "resolution_time": resolution_time,
+ "resolution_time_period": "Hour",
+ },
+ {
+ "priority": "High",
+ "response_time": response_time,
+ "response_time_period": "Hour",
+ "resolution_time": resolution_time,
+ "resolution_time_period": "Hour",
+ },
+ ],
+ "pause_sla_on": [{"status": "Replied"}],
+ "support_and_resolution": [
+ {
+ "workday": "Monday",
+ "start_time": "10:00:00",
+ "end_time": "18:00:00",
+ },
+ {
+ "workday": "Tuesday",
+ "start_time": "10:00:00",
+ "end_time": "18:00:00",
+ },
+ {
+ "workday": "Wednesday",
+ "start_time": "10:00:00",
+ "end_time": "18:00:00",
+ },
+ {
+ "workday": "Thursday",
+ "start_time": "10:00:00",
+ "end_time": "18:00:00",
+ },
+ {
+ "workday": "Friday",
+ "start_time": "10:00:00",
+ "end_time": "18:00:00",
+ },
+ ],
+ }
+ )
filters = {
"default_service_level_agreement": service_level_agreement.default_service_level_agreement,
- "service_level": service_level_agreement.service_level
+ "service_level": service_level_agreement.service_level,
}
if not default_service_level_agreement:
- filters.update({
- "entity_type": entity_type,
- "entity": entity
- })
+ filters.update({"entity_type": entity_type, "entity": entity})
service_level_agreement_exists = frappe.db.exists("Service Level Agreement", filters)
@@ -171,24 +250,26 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list
def create_customer():
- customer = frappe.get_doc({
- "doctype": "Customer",
- "customer_name": "_Test Customer",
- "customer_group": "Commercial",
- "customer_type": "Individual",
- "territory": "Rest Of The World"
- })
+ customer = frappe.get_doc(
+ {
+ "doctype": "Customer",
+ "customer_name": "_Test Customer",
+ "customer_group": "Commercial",
+ "customer_type": "Individual",
+ "territory": "Rest Of The World",
+ }
+ )
if not frappe.db.exists("Customer", "_Test Customer"):
customer.insert(ignore_permissions=True)
return customer.name
else:
return frappe.db.exists("Customer", "_Test Customer")
+
def create_customer_group():
- customer_group = frappe.get_doc({
- "doctype": "Customer Group",
- "customer_group_name": "_Test SLA Customer Group"
- })
+ customer_group = frappe.get_doc(
+ {"doctype": "Customer Group", "customer_group_name": "_Test SLA Customer Group"}
+ )
if not frappe.db.exists("Customer Group", {"customer_group_name": "_Test SLA Customer Group"}):
customer_group.insert()
@@ -196,11 +277,14 @@ def create_customer_group():
else:
return frappe.db.exists("Customer Group", {"customer_group_name": "_Test SLA Customer Group"})
+
def create_territory():
- territory = frappe.get_doc({
- "doctype": "Territory",
- "territory_name": "_Test SLA Territory",
- })
+ territory = frappe.get_doc(
+ {
+ "doctype": "Territory",
+ "territory_name": "_Test SLA Territory",
+ }
+ )
if not frappe.db.exists("Territory", {"territory_name": "_Test SLA Territory"}):
territory.insert()
@@ -208,42 +292,65 @@ def create_territory():
else:
return frappe.db.exists("Territory", {"territory_name": "_Test SLA Territory"})
+
def create_service_level_agreements_for_issues():
- create_service_level_agreement(default_service_level_agreement=1, holiday_list="__Test Holiday List",
- employee_group="_Test Employee Group", entity_type=None, entity=None, response_time=14400, resolution_time=21600)
+ create_service_level_agreement(
+ default_service_level_agreement=1,
+ holiday_list="__Test Holiday List",
+ employee_group="_Test Employee Group",
+ entity_type=None,
+ entity=None,
+ response_time=14400,
+ resolution_time=21600,
+ )
create_customer()
- create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List",
- employee_group="_Test Employee Group", entity_type="Customer", entity="_Test Customer", response_time=7200, resolution_time=10800)
+ create_service_level_agreement(
+ default_service_level_agreement=0,
+ holiday_list="__Test Holiday List",
+ employee_group="_Test Employee Group",
+ entity_type="Customer",
+ entity="_Test Customer",
+ response_time=7200,
+ resolution_time=10800,
+ )
create_customer_group()
- create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List",
- employee_group="_Test Employee Group", entity_type="Customer Group", entity="_Test SLA Customer Group", response_time=7200, resolution_time=10800)
+ create_service_level_agreement(
+ default_service_level_agreement=0,
+ holiday_list="__Test Holiday List",
+ employee_group="_Test Employee Group",
+ entity_type="Customer Group",
+ entity="_Test SLA Customer Group",
+ response_time=7200,
+ resolution_time=10800,
+ )
create_territory()
- create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List",
- employee_group="_Test Employee Group", entity_type="Territory", entity="_Test SLA Territory", response_time=7200, resolution_time=10800)
+ create_service_level_agreement(
+ default_service_level_agreement=0,
+ holiday_list="__Test Holiday List",
+ employee_group="_Test Employee Group",
+ entity_type="Territory",
+ entity="_Test SLA Territory",
+ response_time=7200,
+ resolution_time=10800,
+ )
+
def make_holiday_list():
holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List")
if not holiday_list:
- holiday_list = frappe.get_doc({
- "doctype": "Holiday List",
- "holiday_list_name": "__Test Holiday List",
- "from_date": "2019-01-01",
- "to_date": "2019-12-31",
- "holidays": [
- {
- "description": "Test Holiday 1",
- "holiday_date": "2019-03-05"
- },
- {
- "description": "Test Holiday 2",
- "holiday_date": "2019-03-07"
- },
- {
- "description": "Test Holiday 3",
- "holiday_date": "2019-02-11"
- },
- ]
- }).insert()
+ holiday_list = frappe.get_doc(
+ {
+ "doctype": "Holiday List",
+ "holiday_list_name": "__Test Holiday List",
+ "from_date": "2019-01-01",
+ "to_date": "2019-12-31",
+ "holidays": [
+ {"description": "Test Holiday 1", "holiday_date": "2019-03-05"},
+ {"description": "Test Holiday 2", "holiday_date": "2019-03-07"},
+ {"description": "Test Holiday 3", "holiday_date": "2019-02-11"},
+ ],
+ }
+ ).insert()
diff --git a/erpnext/support/doctype/warranty_claim/test_warranty_claim.py b/erpnext/support/doctype/warranty_claim/test_warranty_claim.py
index f022d55a4b1..19e23493fe5 100644
--- a/erpnext/support/doctype/warranty_claim/test_warranty_claim.py
+++ b/erpnext/support/doctype/warranty_claim/test_warranty_claim.py
@@ -5,7 +5,8 @@ import unittest
import frappe
-test_records = frappe.get_test_records('Warranty Claim')
+test_records = frappe.get_test_records("Warranty Claim")
+
class TestWarrantyClaim(unittest.TestCase):
pass
diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.py b/erpnext/support/doctype/warranty_claim/warranty_claim.py
index 87e95410264..5e2ea067a86 100644
--- a/erpnext/support/doctype/warranty_claim/warranty_claim.py
+++ b/erpnext/support/doctype/warranty_claim/warranty_claim.py
@@ -2,7 +2,6 @@
# License: GNU General Public License v3. See license.txt
-
import frappe
from frappe import _, session
from frappe.utils import now_datetime
@@ -15,27 +14,33 @@ class WarrantyClaim(TransactionBase):
return _("{0}: From {1}").format(self.status, self.customer_name)
def validate(self):
- if session['user'] != 'Guest' and not self.customer:
+ if session["user"] != "Guest" and not self.customer:
frappe.throw(_("Customer is required"))
- if self.status=="Closed" and not self.resolution_date and \
- frappe.db.get_value("Warranty Claim", self.name, "status")!="Closed":
+ if (
+ self.status == "Closed"
+ and not self.resolution_date
+ and frappe.db.get_value("Warranty Claim", self.name, "status") != "Closed"
+ ):
self.resolution_date = now_datetime()
def on_cancel(self):
- lst = frappe.db.sql("""select t1.name
+ lst = frappe.db.sql(
+ """select t1.name
from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2
where t2.parent = t1.name and t2.prevdoc_docname = %s and t1.docstatus!=2""",
- (self.name))
+ (self.name),
+ )
if lst:
- lst1 = ','.join(x[0] for x in lst)
+ lst1 = ",".join(x[0] for x in lst)
frappe.throw(_("Cancel Material Visit {0} before cancelling this Warranty Claim").format(lst1))
else:
- frappe.db.set(self, 'status', 'Cancelled')
+ frappe.db.set(self, "status", "Cancelled")
def on_update(self):
pass
+
@frappe.whitelist()
def make_maintenance_visit(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc, map_child_doc
@@ -44,25 +49,25 @@ def make_maintenance_visit(source_name, target_doc=None):
target_doc.prevdoc_doctype = source_parent.doctype
target_doc.prevdoc_docname = source_parent.name
- visit = frappe.db.sql("""select t1.name
+ visit = frappe.db.sql(
+ """select t1.name
from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2
where t2.parent=t1.name and t2.prevdoc_docname=%s
- and t1.docstatus=1 and t1.completion_status='Fully Completed'""", source_name)
+ and t1.docstatus=1 and t1.completion_status='Fully Completed'""",
+ source_name,
+ )
if not visit:
- target_doc = get_mapped_doc("Warranty Claim", source_name, {
- "Warranty Claim": {
- "doctype": "Maintenance Visit",
- "field_map": {}
- }
- }, target_doc)
+ target_doc = get_mapped_doc(
+ "Warranty Claim",
+ source_name,
+ {"Warranty Claim": {"doctype": "Maintenance Visit", "field_map": {}}},
+ target_doc,
+ )
source_doc = frappe.get_doc("Warranty Claim", source_name)
if source_doc.get("item_code"):
- table_map = {
- "doctype": "Maintenance Visit Purpose",
- "postprocess": _update_links
- }
+ table_map = {"doctype": "Maintenance Visit Purpose", "postprocess": _update_links}
map_child_doc(source_doc, target_doc, table_map, source_doc)
return target_doc
diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py
index 2ab0fb88a7f..5b51ef81c7b 100644
--- a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py
+++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py
@@ -7,21 +7,17 @@ import frappe
def execute(filters=None):
columns = [
+ {"fieldname": "creation_date", "label": "Date", "fieldtype": "Date", "width": 300},
{
- 'fieldname': 'creation_date',
- 'label': 'Date',
- 'fieldtype': 'Date',
- 'width': 300
- },
- {
- 'fieldname': 'first_response_time',
- 'fieldtype': 'Duration',
- 'label': 'First Response Time',
- 'width': 300
+ "fieldname": "first_response_time",
+ "fieldtype": "Duration",
+ "label": "First Response Time",
+ "width": 300,
},
]
- data = frappe.db.sql('''
+ data = frappe.db.sql(
+ """
SELECT
date(creation) as creation_date,
avg(first_response_time) as avg_response_time
@@ -31,6 +27,8 @@ def execute(filters=None):
and first_response_time > 0
GROUP BY creation_date
ORDER BY creation_date desc
- ''', (filters.from_date, filters.to_date))
+ """,
+ (filters.from_date, filters.to_date),
+ )
return columns, data
diff --git a/erpnext/support/report/issue_analytics/issue_analytics.py b/erpnext/support/report/issue_analytics/issue_analytics.py
index 543a34cb304..d8a200ad686 100644
--- a/erpnext/support/report/issue_analytics/issue_analytics.py
+++ b/erpnext/support/report/issue_analytics/issue_analytics.py
@@ -15,6 +15,7 @@ from erpnext.accounts.utils import get_fiscal_year
def execute(filters=None):
return IssueAnalytics(filters).run()
+
class IssueAnalytics(object):
def __init__(self, filters=None):
"""Issue Analytics Report"""
@@ -31,101 +32,98 @@ class IssueAnalytics(object):
def get_columns(self):
self.columns = []
- if self.filters.based_on == 'Customer':
- self.columns.append({
- 'label': _('Customer'),
- 'options': 'Customer',
- 'fieldname': 'customer',
- 'fieldtype': 'Link',
- 'width': 200
- })
+ if self.filters.based_on == "Customer":
+ self.columns.append(
+ {
+ "label": _("Customer"),
+ "options": "Customer",
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "width": 200,
+ }
+ )
- elif self.filters.based_on == 'Assigned To':
- self.columns.append({
- 'label': _('User'),
- 'fieldname': 'user',
- 'fieldtype': 'Link',
- 'options': 'User',
- 'width': 200
- })
+ elif self.filters.based_on == "Assigned To":
+ self.columns.append(
+ {"label": _("User"), "fieldname": "user", "fieldtype": "Link", "options": "User", "width": 200}
+ )
- elif self.filters.based_on == 'Issue Type':
- self.columns.append({
- 'label': _('Issue Type'),
- 'fieldname': 'issue_type',
- 'fieldtype': 'Link',
- 'options': 'Issue Type',
- 'width': 200
- })
+ elif self.filters.based_on == "Issue Type":
+ self.columns.append(
+ {
+ "label": _("Issue Type"),
+ "fieldname": "issue_type",
+ "fieldtype": "Link",
+ "options": "Issue Type",
+ "width": 200,
+ }
+ )
- elif self.filters.based_on == 'Issue Priority':
- self.columns.append({
- 'label': _('Issue Priority'),
- 'fieldname': 'priority',
- 'fieldtype': 'Link',
- 'options': 'Issue Priority',
- 'width': 200
- })
+ elif self.filters.based_on == "Issue Priority":
+ self.columns.append(
+ {
+ "label": _("Issue Priority"),
+ "fieldname": "priority",
+ "fieldtype": "Link",
+ "options": "Issue Priority",
+ "width": 200,
+ }
+ )
for end_date in self.periodic_daterange:
period = self.get_period(end_date)
- self.columns.append({
- 'label': _(period),
- 'fieldname': scrub(period),
- 'fieldtype': 'Int',
- 'width': 120
- })
+ self.columns.append(
+ {"label": _(period), "fieldname": scrub(period), "fieldtype": "Int", "width": 120}
+ )
- self.columns.append({
- 'label': _('Total'),
- 'fieldname': 'total',
- 'fieldtype': 'Int',
- 'width': 120
- })
+ self.columns.append(
+ {"label": _("Total"), "fieldname": "total", "fieldtype": "Int", "width": 120}
+ )
def get_data(self):
self.get_issues()
self.get_rows()
def get_period(self, date):
- months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+ months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
- if self.filters.range == 'Weekly':
- period = 'Week ' + str(date.isocalendar()[1])
- elif self.filters.range == 'Monthly':
+ if self.filters.range == "Weekly":
+ period = "Week " + str(date.isocalendar()[1])
+ elif self.filters.range == "Monthly":
period = str(months[date.month - 1])
- elif self.filters.range == 'Quarterly':
- period = 'Quarter ' + str(((date.month - 1) // 3) + 1)
+ elif self.filters.range == "Quarterly":
+ period = "Quarter " + str(((date.month - 1) // 3) + 1)
else:
year = get_fiscal_year(date, self.filters.company)
period = str(year[0])
- if getdate(self.filters.from_date).year != getdate(self.filters.to_date).year and self.filters.range != 'Yearly':
- period += ' ' + str(date.year)
+ if (
+ getdate(self.filters.from_date).year != getdate(self.filters.to_date).year
+ and self.filters.range != "Yearly"
+ ):
+ period += " " + str(date.year)
return period
def get_period_date_ranges(self):
from dateutil.relativedelta import MO, relativedelta
+
from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date)
- increment = {
- 'Monthly': 1,
- 'Quarterly': 3,
- 'Half-Yearly': 6,
- 'Yearly': 12
- }.get(self.filters.range, 1)
+ increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get(
+ self.filters.range, 1
+ )
- if self.filters.range in ['Monthly', 'Quarterly']:
+ if self.filters.range in ["Monthly", "Quarterly"]:
from_date = from_date.replace(day=1)
- elif self.filters.range == 'Yearly':
+ elif self.filters.range == "Yearly":
from_date = get_fiscal_year(from_date)[1]
else:
from_date = from_date + relativedelta(from_date, weekday=MO(-1))
self.periodic_daterange = []
for dummy in range(1, 53):
- if self.filters.range == 'Weekly':
+ if self.filters.range == "Weekly":
period_end_date = add_days(from_date, 6)
else:
period_end_date = add_to_date(from_date, months=increment, days=-1)
@@ -142,25 +140,26 @@ class IssueAnalytics(object):
def get_issues(self):
filters = self.get_common_filters()
self.field_map = {
- 'Customer': 'customer',
- 'Issue Type': 'issue_type',
- 'Issue Priority': 'priority',
- 'Assigned To': '_assign'
+ "Customer": "customer",
+ "Issue Type": "issue_type",
+ "Issue Priority": "priority",
+ "Assigned To": "_assign",
}
- self.entries = frappe.db.get_all('Issue',
- fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date'],
- filters=filters
+ self.entries = frappe.db.get_all(
+ "Issue",
+ fields=[self.field_map.get(self.filters.based_on), "name", "opening_date"],
+ filters=filters,
)
def get_common_filters(self):
filters = {}
- filters['opening_date'] = ('between', [self.filters.from_date, self.filters.to_date])
+ filters["opening_date"] = ("between", [self.filters.from_date, self.filters.to_date])
- if self.filters.get('assigned_to'):
- filters['_assign'] = ('like', '%' + self.filters.get('assigned_to') + '%')
+ if self.filters.get("assigned_to"):
+ filters["_assign"] = ("like", "%" + self.filters.get("assigned_to") + "%")
- for entry in ['company', 'status', 'priority', 'customer', 'project']:
+ for entry in ["company", "status", "priority", "customer", "project"]:
if self.filters.get(entry):
filters[entry] = self.filters.get(entry)
@@ -171,14 +170,14 @@ class IssueAnalytics(object):
self.get_periodic_data()
for entity, period_data in iteritems(self.issue_periodic_data):
- if self.filters.based_on == 'Customer':
- row = {'customer': entity}
- elif self.filters.based_on == 'Assigned To':
- row = {'user': entity}
- elif self.filters.based_on == 'Issue Type':
- row = {'issue_type': entity}
- elif self.filters.based_on == 'Issue Priority':
- row = {'priority': entity}
+ if self.filters.based_on == "Customer":
+ row = {"customer": entity}
+ elif self.filters.based_on == "Assigned To":
+ row = {"user": entity}
+ elif self.filters.based_on == "Issue Type":
+ row = {"issue_type": entity}
+ elif self.filters.based_on == "Issue Priority":
+ row = {"priority": entity}
total = 0
for end_date in self.periodic_daterange:
@@ -187,7 +186,7 @@ class IssueAnalytics(object):
row[scrub(period)] = amount
total += amount
- row['total'] = total
+ row["total"] = total
self.data.append(row)
@@ -195,9 +194,9 @@ class IssueAnalytics(object):
self.issue_periodic_data = frappe._dict()
for d in self.entries:
- period = self.get_period(d.get('opening_date'))
+ period = self.get_period(d.get("opening_date"))
- if self.filters.based_on == 'Assigned To':
+ if self.filters.based_on == "Assigned To":
if d._assign:
for entry in json.loads(d._assign):
self.issue_periodic_data.setdefault(entry, frappe._dict()).setdefault(period, 0.0)
@@ -207,18 +206,12 @@ class IssueAnalytics(object):
field = self.field_map.get(self.filters.based_on)
value = d.get(field)
if not value:
- value = _('Not Specified')
+ value = _("Not Specified")
self.issue_periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0.0)
self.issue_periodic_data[value][period] += 1
def get_chart_data(self):
length = len(self.columns)
- labels = [d.get('label') for d in self.columns[1:length-1]]
- self.chart = {
- 'data': {
- 'labels': labels,
- 'datasets': []
- },
- 'type': 'line'
- }
+ labels = [d.get("label") for d in self.columns[1 : length - 1]]
+ self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"}
diff --git a/erpnext/support/report/issue_analytics/test_issue_analytics.py b/erpnext/support/report/issue_analytics/test_issue_analytics.py
index b27dc46ad28..169392e5e92 100644
--- a/erpnext/support/report/issue_analytics/test_issue_analytics.py
+++ b/erpnext/support/report/issue_analytics/test_issue_analytics.py
@@ -1,4 +1,3 @@
-
import unittest
import frappe
@@ -11,7 +10,8 @@ from erpnext.support.doctype.service_level_agreement.test_service_level_agreemen
)
from erpnext.support.report.issue_analytics.issue_analytics import execute
-months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
+
class TestIssueAnalytics(unittest.TestCase):
@classmethod
@@ -24,8 +24,8 @@ class TestIssueAnalytics(unittest.TestCase):
self.current_month = str(months[current_month_date.month - 1]).lower()
self.last_month = str(months[last_month_date.month - 1]).lower()
if current_month_date.year != last_month_date.year:
- self.current_month += '_' + str(current_month_date.year)
- self.last_month += '_' + str(last_month_date.year)
+ self.current_month += "_" + str(current_month_date.year)
+ self.last_month += "_" + str(last_month_date.year)
def test_issue_analytics(self):
create_service_level_agreements_for_issues()
@@ -39,146 +39,88 @@ class TestIssueAnalytics(unittest.TestCase):
def compare_result_for_customer(self):
filters = {
- 'company': '_Test Company',
- 'based_on': 'Customer',
- 'from_date': add_months(getdate(), -1),
- 'to_date': getdate(),
- 'range': 'Monthly'
+ "company": "_Test Company",
+ "based_on": "Customer",
+ "from_date": add_months(getdate(), -1),
+ "to_date": getdate(),
+ "range": "Monthly",
}
report = execute(filters)
expected_data = [
- {
- 'customer': '__Test Customer 2',
- self.last_month: 1.0,
- self.current_month: 0.0,
- 'total': 1.0
- },
- {
- 'customer': '__Test Customer 1',
- self.last_month: 0.0,
- self.current_month: 1.0,
- 'total': 1.0
- },
- {
- 'customer': '__Test Customer',
- self.last_month: 1.0,
- self.current_month: 1.0,
- 'total': 2.0
- }
+ {"customer": "__Test Customer 2", self.last_month: 1.0, self.current_month: 0.0, "total": 1.0},
+ {"customer": "__Test Customer 1", self.last_month: 0.0, self.current_month: 1.0, "total": 1.0},
+ {"customer": "__Test Customer", self.last_month: 1.0, self.current_month: 1.0, "total": 2.0},
]
- self.assertEqual(expected_data, report[1]) # rows
- self.assertEqual(len(report[0]), 4) # cols
+ self.assertEqual(expected_data, report[1]) # rows
+ self.assertEqual(len(report[0]), 4) # cols
def compare_result_for_issue_type(self):
filters = {
- 'company': '_Test Company',
- 'based_on': 'Issue Type',
- 'from_date': add_months(getdate(), -1),
- 'to_date': getdate(),
- 'range': 'Monthly'
+ "company": "_Test Company",
+ "based_on": "Issue Type",
+ "from_date": add_months(getdate(), -1),
+ "to_date": getdate(),
+ "range": "Monthly",
}
report = execute(filters)
expected_data = [
- {
- 'issue_type': 'Discomfort',
- self.last_month: 1.0,
- self.current_month: 0.0,
- 'total': 1.0
- },
- {
- 'issue_type': 'Service Request',
- self.last_month: 0.0,
- self.current_month: 1.0,
- 'total': 1.0
- },
- {
- 'issue_type': 'Bug',
- self.last_month: 1.0,
- self.current_month: 1.0,
- 'total': 2.0
- }
+ {"issue_type": "Discomfort", self.last_month: 1.0, self.current_month: 0.0, "total": 1.0},
+ {"issue_type": "Service Request", self.last_month: 0.0, self.current_month: 1.0, "total": 1.0},
+ {"issue_type": "Bug", self.last_month: 1.0, self.current_month: 1.0, "total": 2.0},
]
- self.assertEqual(expected_data, report[1]) # rows
- self.assertEqual(len(report[0]), 4) # cols
+ self.assertEqual(expected_data, report[1]) # rows
+ self.assertEqual(len(report[0]), 4) # cols
def compare_result_for_issue_priority(self):
filters = {
- 'company': '_Test Company',
- 'based_on': 'Issue Priority',
- 'from_date': add_months(getdate(), -1),
- 'to_date': getdate(),
- 'range': 'Monthly'
+ "company": "_Test Company",
+ "based_on": "Issue Priority",
+ "from_date": add_months(getdate(), -1),
+ "to_date": getdate(),
+ "range": "Monthly",
}
report = execute(filters)
expected_data = [
- {
- 'priority': 'Medium',
- self.last_month: 1.0,
- self.current_month: 1.0,
- 'total': 2.0
- },
- {
- 'priority': 'Low',
- self.last_month: 1.0,
- self.current_month: 0.0,
- 'total': 1.0
- },
- {
- 'priority': 'High',
- self.last_month: 0.0,
- self.current_month: 1.0,
- 'total': 1.0
- }
+ {"priority": "Medium", self.last_month: 1.0, self.current_month: 1.0, "total": 2.0},
+ {"priority": "Low", self.last_month: 1.0, self.current_month: 0.0, "total": 1.0},
+ {"priority": "High", self.last_month: 0.0, self.current_month: 1.0, "total": 1.0},
]
- self.assertEqual(expected_data, report[1]) # rows
- self.assertEqual(len(report[0]), 4) # cols
+ self.assertEqual(expected_data, report[1]) # rows
+ self.assertEqual(len(report[0]), 4) # cols
def compare_result_for_assignment(self):
filters = {
- 'company': '_Test Company',
- 'based_on': 'Assigned To',
- 'from_date': add_months(getdate(), -1),
- 'to_date': getdate(),
- 'range': 'Monthly'
+ "company": "_Test Company",
+ "based_on": "Assigned To",
+ "from_date": add_months(getdate(), -1),
+ "to_date": getdate(),
+ "range": "Monthly",
}
report = execute(filters)
expected_data = [
- {
- 'user': 'test@example.com',
- self.last_month: 1.0,
- self.current_month: 1.0,
- 'total': 2.0
- },
- {
- 'user': 'test1@example.com',
- self.last_month: 2.0,
- self.current_month: 1.0,
- 'total': 3.0
- }
+ {"user": "test@example.com", self.last_month: 1.0, self.current_month: 1.0, "total": 2.0},
+ {"user": "test1@example.com", self.last_month: 2.0, self.current_month: 1.0, "total": 3.0},
]
- self.assertEqual(expected_data, report[1]) # rows
- self.assertEqual(len(report[0]), 4) # cols
+ self.assertEqual(expected_data, report[1]) # rows
+ self.assertEqual(len(report[0]), 4) # cols
def create_issue_types():
- for entry in ['Bug', 'Service Request', 'Discomfort']:
- if not frappe.db.exists('Issue Type', entry):
- frappe.get_doc({
- 'doctype': 'Issue Type',
- '__newname': entry
- }).insert()
+ for entry in ["Bug", "Service Request", "Discomfort"]:
+ if not frappe.db.exists("Issue Type", entry):
+ frappe.get_doc({"doctype": "Issue Type", "__newname": entry}).insert()
def create_records():
@@ -190,29 +132,15 @@ def create_records():
last_month_date = add_months(current_month_date, -1)
issue = make_issue(current_month_date, "__Test Customer", 2, "High", "Bug")
- add_assignment({
- "assign_to": ["test@example.com"],
- "doctype": "Issue",
- "name": issue.name
- })
+ add_assignment({"assign_to": ["test@example.com"], "doctype": "Issue", "name": issue.name})
issue = make_issue(last_month_date, "__Test Customer", 2, "Low", "Bug")
- add_assignment({
- "assign_to": ["test1@example.com"],
- "doctype": "Issue",
- "name": issue.name
- })
+ add_assignment({"assign_to": ["test1@example.com"], "doctype": "Issue", "name": issue.name})
issue = make_issue(current_month_date, "__Test Customer 1", 2, "Medium", "Service Request")
- add_assignment({
- "assign_to": ["test1@example.com"],
- "doctype": "Issue",
- "name": issue.name
- })
+ add_assignment({"assign_to": ["test1@example.com"], "doctype": "Issue", "name": issue.name})
issue = make_issue(last_month_date, "__Test Customer 2", 2, "Medium", "Discomfort")
- add_assignment({
- "assign_to": ["test@example.com", "test1@example.com"],
- "doctype": "Issue",
- "name": issue.name
- })
+ add_assignment(
+ {"assign_to": ["test@example.com", "test1@example.com"], "doctype": "Issue", "name": issue.name}
+ )
diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py
index b5d52278b89..a81bcc7f32a 100644
--- a/erpnext/support/report/issue_summary/issue_summary.py
+++ b/erpnext/support/report/issue_summary/issue_summary.py
@@ -13,6 +13,7 @@ from six import iteritems
def execute(filters=None):
return IssueSummary(filters).run()
+
class IssueSummary(object):
def __init__(self, filters=None):
self.filters = frappe._dict(filters or {})
@@ -28,82 +29,77 @@ class IssueSummary(object):
def get_columns(self):
self.columns = []
- if self.filters.based_on == 'Customer':
- self.columns.append({
- 'label': _('Customer'),
- 'options': 'Customer',
- 'fieldname': 'customer',
- 'fieldtype': 'Link',
- 'width': 200
- })
+ if self.filters.based_on == "Customer":
+ self.columns.append(
+ {
+ "label": _("Customer"),
+ "options": "Customer",
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "width": 200,
+ }
+ )
- elif self.filters.based_on == 'Assigned To':
- self.columns.append({
- 'label': _('User'),
- 'fieldname': 'user',
- 'fieldtype': 'Link',
- 'options': 'User',
- 'width': 200
- })
+ elif self.filters.based_on == "Assigned To":
+ self.columns.append(
+ {"label": _("User"), "fieldname": "user", "fieldtype": "Link", "options": "User", "width": 200}
+ )
- elif self.filters.based_on == 'Issue Type':
- self.columns.append({
- 'label': _('Issue Type'),
- 'fieldname': 'issue_type',
- 'fieldtype': 'Link',
- 'options': 'Issue Type',
- 'width': 200
- })
+ elif self.filters.based_on == "Issue Type":
+ self.columns.append(
+ {
+ "label": _("Issue Type"),
+ "fieldname": "issue_type",
+ "fieldtype": "Link",
+ "options": "Issue Type",
+ "width": 200,
+ }
+ )
- elif self.filters.based_on == 'Issue Priority':
- self.columns.append({
- 'label': _('Issue Priority'),
- 'fieldname': 'priority',
- 'fieldtype': 'Link',
- 'options': 'Issue Priority',
- 'width': 200
- })
+ elif self.filters.based_on == "Issue Priority":
+ self.columns.append(
+ {
+ "label": _("Issue Priority"),
+ "fieldname": "priority",
+ "fieldtype": "Link",
+ "options": "Issue Priority",
+ "width": 200,
+ }
+ )
- self.statuses = ['Open', 'Replied', 'On Hold', 'Resolved', 'Closed']
+ self.statuses = ["Open", "Replied", "On Hold", "Resolved", "Closed"]
for status in self.statuses:
- self.columns.append({
- 'label': _(status),
- 'fieldname': scrub(status),
- 'fieldtype': 'Int',
- 'width': 80
- })
+ self.columns.append(
+ {"label": _(status), "fieldname": scrub(status), "fieldtype": "Int", "width": 80}
+ )
- self.columns.append({
- 'label': _('Total Issues'),
- 'fieldname': 'total_issues',
- 'fieldtype': 'Int',
- 'width': 100
- })
+ self.columns.append(
+ {"label": _("Total Issues"), "fieldname": "total_issues", "fieldtype": "Int", "width": 100}
+ )
self.sla_status_map = {
- 'SLA Failed': 'failed',
- 'SLA Fulfilled': 'fulfilled',
- 'SLA Ongoing': 'ongoing'
+ "SLA Failed": "failed",
+ "SLA Fulfilled": "fulfilled",
+ "SLA Ongoing": "ongoing",
}
for label, fieldname in self.sla_status_map.items():
- self.columns.append({
- 'label': _(label),
- 'fieldname': fieldname,
- 'fieldtype': 'Int',
- 'width': 100
- })
+ self.columns.append(
+ {"label": _(label), "fieldname": fieldname, "fieldtype": "Int", "width": 100}
+ )
- self.metrics = ['Avg First Response Time', 'Avg Response Time', 'Avg Hold Time',
- 'Avg Resolution Time', 'Avg User Resolution Time']
+ self.metrics = [
+ "Avg First Response Time",
+ "Avg Response Time",
+ "Avg Hold Time",
+ "Avg Resolution Time",
+ "Avg User Resolution Time",
+ ]
for metric in self.metrics:
- self.columns.append({
- 'label': _(metric),
- 'fieldname': scrub(metric),
- 'fieldtype': 'Duration',
- 'width': 170
- })
+ self.columns.append(
+ {"label": _(metric), "fieldname": scrub(metric), "fieldtype": "Duration", "width": 170}
+ )
def get_data(self):
self.get_issues()
@@ -112,26 +108,37 @@ class IssueSummary(object):
def get_issues(self):
filters = self.get_common_filters()
self.field_map = {
- 'Customer': 'customer',
- 'Issue Type': 'issue_type',
- 'Issue Priority': 'priority',
- 'Assigned To': '_assign'
+ "Customer": "customer",
+ "Issue Type": "issue_type",
+ "Issue Priority": "priority",
+ "Assigned To": "_assign",
}
- self.entries = frappe.db.get_all('Issue',
- fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date', 'status', 'avg_response_time',
- 'first_response_time', 'total_hold_time', 'user_resolution_time', 'resolution_time', 'agreement_status'],
- filters=filters
+ self.entries = frappe.db.get_all(
+ "Issue",
+ fields=[
+ self.field_map.get(self.filters.based_on),
+ "name",
+ "opening_date",
+ "status",
+ "avg_response_time",
+ "first_response_time",
+ "total_hold_time",
+ "user_resolution_time",
+ "resolution_time",
+ "agreement_status",
+ ],
+ filters=filters,
)
def get_common_filters(self):
filters = {}
- filters['opening_date'] = ('between', [self.filters.from_date, self.filters.to_date])
+ filters["opening_date"] = ("between", [self.filters.from_date, self.filters.to_date])
- if self.filters.get('assigned_to'):
- filters['_assign'] = ('like', '%' + self.filters.get('assigned_to') + '%')
+ if self.filters.get("assigned_to"):
+ filters["_assign"] = ("like", "%" + self.filters.get("assigned_to") + "%")
- for entry in ['company', 'status', 'priority', 'customer', 'project']:
+ for entry in ["company", "status", "priority", "customer", "project"]:
if self.filters.get(entry):
filters[entry] = self.filters.get(entry)
@@ -142,20 +149,20 @@ class IssueSummary(object):
self.get_summary_data()
for entity, data in iteritems(self.issue_summary_data):
- if self.filters.based_on == 'Customer':
- row = {'customer': entity}
- elif self.filters.based_on == 'Assigned To':
- row = {'user': entity}
- elif self.filters.based_on == 'Issue Type':
- row = {'issue_type': entity}
- elif self.filters.based_on == 'Issue Priority':
- row = {'priority': entity}
+ if self.filters.based_on == "Customer":
+ row = {"customer": entity}
+ elif self.filters.based_on == "Assigned To":
+ row = {"user": entity}
+ elif self.filters.based_on == "Issue Type":
+ row = {"issue_type": entity}
+ elif self.filters.based_on == "Issue Priority":
+ row = {"priority": entity}
for status in self.statuses:
count = flt(data.get(status, 0.0))
row[scrub(status)] = count
- row['total_issues'] = data.get('total_issues', 0.0)
+ row["total_issues"] = data.get("total_issues", 0.0)
for sla_status in self.sla_status_map.values():
value = flt(data.get(sla_status), 0.0)
@@ -174,36 +181,41 @@ class IssueSummary(object):
status = d.status
agreement_status = scrub(d.agreement_status)
- if self.filters.based_on == 'Assigned To':
+ if self.filters.based_on == "Assigned To":
if d._assign:
for entry in json.loads(d._assign):
self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(status, 0.0)
self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(agreement_status, 0.0)
- self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault('total_issues', 0.0)
+ self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault("total_issues", 0.0)
self.issue_summary_data[entry][status] += 1
self.issue_summary_data[entry][agreement_status] += 1
- self.issue_summary_data[entry]['total_issues'] += 1
+ self.issue_summary_data[entry]["total_issues"] += 1
else:
field = self.field_map.get(self.filters.based_on)
value = d.get(field)
if not value:
- value = _('Not Specified')
+ value = _("Not Specified")
self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(status, 0.0)
self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(agreement_status, 0.0)
- self.issue_summary_data.setdefault(value, frappe._dict()).setdefault('total_issues', 0.0)
+ self.issue_summary_data.setdefault(value, frappe._dict()).setdefault("total_issues", 0.0)
self.issue_summary_data[value][status] += 1
self.issue_summary_data[value][agreement_status] += 1
- self.issue_summary_data[value]['total_issues'] += 1
+ self.issue_summary_data[value]["total_issues"] += 1
self.get_metrics_data()
def get_metrics_data(self):
issues = []
- metrics_list = ['avg_response_time', 'avg_first_response_time', 'avg_hold_time',
- 'avg_resolution_time', 'avg_user_resolution_time']
+ metrics_list = [
+ "avg_response_time",
+ "avg_first_response_time",
+ "avg_hold_time",
+ "avg_resolution_time",
+ "avg_user_resolution_time",
+ ]
for entry in self.entries:
issues.append(entry.name)
@@ -211,7 +223,7 @@ class IssueSummary(object):
field = self.field_map.get(self.filters.based_on)
if issues:
- if self.filters.based_on == 'Assigned To':
+ if self.filters.based_on == "Assigned To":
assignment_map = frappe._dict()
for d in self.entries:
if d._assign:
@@ -219,11 +231,15 @@ class IssueSummary(object):
for metric in metrics_list:
self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(metric, 0.0)
- self.issue_summary_data[entry]['avg_response_time'] += d.get('avg_response_time') or 0.0
- self.issue_summary_data[entry]['avg_first_response_time'] += d.get('first_response_time') or 0.0
- self.issue_summary_data[entry]['avg_hold_time'] += d.get('total_hold_time') or 0.0
- self.issue_summary_data[entry]['avg_resolution_time'] += d.get('resolution_time') or 0.0
- self.issue_summary_data[entry]['avg_user_resolution_time'] += d.get('user_resolution_time') or 0.0
+ self.issue_summary_data[entry]["avg_response_time"] += d.get("avg_response_time") or 0.0
+ self.issue_summary_data[entry]["avg_first_response_time"] += (
+ d.get("first_response_time") or 0.0
+ )
+ self.issue_summary_data[entry]["avg_hold_time"] += d.get("total_hold_time") or 0.0
+ self.issue_summary_data[entry]["avg_resolution_time"] += d.get("resolution_time") or 0.0
+ self.issue_summary_data[entry]["avg_user_resolution_time"] += (
+ d.get("user_resolution_time") or 0.0
+ )
if not assignment_map.get(entry):
assignment_map[entry] = 0
@@ -234,7 +250,8 @@ class IssueSummary(object):
self.issue_summary_data[entry][metric] /= flt(assignment_map.get(entry))
else:
- data = frappe.db.sql("""
+ data = frappe.db.sql(
+ """
SELECT
{0}, AVG(first_response_time) as avg_frt,
AVG(avg_response_time) as avg_resp_time,
@@ -245,21 +262,30 @@ class IssueSummary(object):
WHERE
name IN %(issues)s
GROUP BY {0}
- """.format(field), {'issues': issues}, as_dict=1)
+ """.format(
+ field
+ ),
+ {"issues": issues},
+ as_dict=1,
+ )
for entry in data:
value = entry.get(field)
if not value:
- value = _('Not Specified')
+ value = _("Not Specified")
for metric in metrics_list:
self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(metric, 0.0)
- self.issue_summary_data[value]['avg_response_time'] = entry.get('avg_resp_time') or 0.0
- self.issue_summary_data[value]['avg_first_response_time'] = entry.get('avg_frt') or 0.0
- self.issue_summary_data[value]['avg_hold_time'] = entry.get('avg_hold_time') or 0.0
- self.issue_summary_data[value]['avg_resolution_time'] = entry.get('avg_resolution_time') or 0.0
- self.issue_summary_data[value]['avg_user_resolution_time'] = entry.get('avg_user_resolution_time') or 0.0
+ self.issue_summary_data[value]["avg_response_time"] = entry.get("avg_resp_time") or 0.0
+ self.issue_summary_data[value]["avg_first_response_time"] = entry.get("avg_frt") or 0.0
+ self.issue_summary_data[value]["avg_hold_time"] = entry.get("avg_hold_time") or 0.0
+ self.issue_summary_data[value]["avg_resolution_time"] = (
+ entry.get("avg_resolution_time") or 0.0
+ )
+ self.issue_summary_data[value]["avg_user_resolution_time"] = (
+ entry.get("avg_user_resolution_time") or 0.0
+ )
def get_chart_data(self):
self.chart = []
@@ -273,47 +299,30 @@ class IssueSummary(object):
entity = self.filters.based_on
entity_field = self.field_map.get(entity)
- if entity == 'Assigned To':
- entity_field = 'user'
+ if entity == "Assigned To":
+ entity_field = "user"
for entry in self.data:
labels.append(entry.get(entity_field))
- open_issues.append(entry.get('open'))
- replied_issues.append(entry.get('replied'))
- on_hold_issues.append(entry.get('on_hold'))
- resolved_issues.append(entry.get('resolved'))
- closed_issues.append(entry.get('closed'))
+ open_issues.append(entry.get("open"))
+ replied_issues.append(entry.get("replied"))
+ on_hold_issues.append(entry.get("on_hold"))
+ resolved_issues.append(entry.get("resolved"))
+ closed_issues.append(entry.get("closed"))
self.chart = {
- 'data': {
- 'labels': labels[:30],
- 'datasets': [
- {
- 'name': 'Open',
- 'values': open_issues[:30]
- },
- {
- 'name': 'Replied',
- 'values': replied_issues[:30]
- },
- {
- 'name': 'On Hold',
- 'values': on_hold_issues[:30]
- },
- {
- 'name': 'Resolved',
- 'values': resolved_issues[:30]
- },
- {
- 'name': 'Closed',
- 'values': closed_issues[:30]
- }
- ]
+ "data": {
+ "labels": labels[:30],
+ "datasets": [
+ {"name": "Open", "values": open_issues[:30]},
+ {"name": "Replied", "values": replied_issues[:30]},
+ {"name": "On Hold", "values": on_hold_issues[:30]},
+ {"name": "Resolved", "values": resolved_issues[:30]},
+ {"name": "Closed", "values": closed_issues[:30]},
+ ],
},
- 'type': 'bar',
- 'barOptions': {
- 'stacked': True
- }
+ "type": "bar",
+ "barOptions": {"stacked": True},
}
def get_report_summary(self):
@@ -326,41 +335,41 @@ class IssueSummary(object):
closed = 0
for entry in self.data:
- open_issues += entry.get('open')
- replied += entry.get('replied')
- on_hold += entry.get('on_hold')
- resolved += entry.get('resolved')
- closed += entry.get('closed')
+ open_issues += entry.get("open")
+ replied += entry.get("replied")
+ on_hold += entry.get("on_hold")
+ resolved += entry.get("resolved")
+ closed += entry.get("closed")
self.report_summary = [
{
- 'value': open_issues,
- 'indicator': 'Red',
- 'label': _('Open'),
- 'datatype': 'Int',
+ "value": open_issues,
+ "indicator": "Red",
+ "label": _("Open"),
+ "datatype": "Int",
},
{
- 'value': replied,
- 'indicator': 'Grey',
- 'label': _('Replied'),
- 'datatype': 'Int',
+ "value": replied,
+ "indicator": "Grey",
+ "label": _("Replied"),
+ "datatype": "Int",
},
{
- 'value': on_hold,
- 'indicator': 'Grey',
- 'label': _('On Hold'),
- 'datatype': 'Int',
+ "value": on_hold,
+ "indicator": "Grey",
+ "label": _("On Hold"),
+ "datatype": "Int",
},
{
- 'value': resolved,
- 'indicator': 'Green',
- 'label': _('Resolved'),
- 'datatype': 'Int',
+ "value": resolved,
+ "indicator": "Green",
+ "label": _("Resolved"),
+ "datatype": "Int",
},
{
- 'value': closed,
- 'indicator': 'Green',
- 'label': _('Closed'),
- 'datatype': 'Int',
- }
+ "value": closed,
+ "indicator": "Green",
+ "label": _("Closed"),
+ "datatype": "Int",
+ },
]
diff --git a/erpnext/support/report/support_hour_distribution/support_hour_distribution.py b/erpnext/support/report/support_hour_distribution/support_hour_distribution.py
index e3a7e5f54b1..b4bd460a67f 100644
--- a/erpnext/support/report/support_hour_distribution/support_hour_distribution.py
+++ b/erpnext/support/report/support_hour_distribution/support_hour_distribution.py
@@ -8,34 +8,36 @@ from frappe.utils import add_to_date, get_datetime, getdate
from six import iteritems
time_slots = {
- '12AM - 3AM': '00:00:00-03:00:00',
- '3AM - 6AM': '03:00:00-06:00:00',
- '6AM - 9AM': '06:00:00-09:00:00',
- '9AM - 12PM': '09:00:00-12:00:00',
- '12PM - 3PM': '12:00:00-15:00:00',
- '3PM - 6PM': '15:00:00-18:00:00',
- '6PM - 9PM': '18:00:00-21:00:00',
- '9PM - 12AM': '21:00:00-23:00:00'
+ "12AM - 3AM": "00:00:00-03:00:00",
+ "3AM - 6AM": "03:00:00-06:00:00",
+ "6AM - 9AM": "06:00:00-09:00:00",
+ "9AM - 12PM": "09:00:00-12:00:00",
+ "12PM - 3PM": "12:00:00-15:00:00",
+ "3PM - 6PM": "15:00:00-18:00:00",
+ "6PM - 9PM": "18:00:00-21:00:00",
+ "9PM - 12AM": "21:00:00-23:00:00",
}
+
def execute(filters=None):
columns, data = [], []
- if not filters.get('periodicity'):
- filters['periodicity'] = 'Daily'
+ if not filters.get("periodicity"):
+ filters["periodicity"] = "Daily"
columns = get_columns()
data, timeslot_wise_count = get_data(filters)
chart = get_chart_data(timeslot_wise_count)
return columns, data, None, chart
+
def get_data(filters):
start_date = getdate(filters.from_date)
data = []
time_slot_wise_total_count = {}
- while(start_date <= getdate(filters.to_date)):
- hours_count = {'date': start_date}
+ while start_date <= getdate(filters.to_date):
+ hours_count = {"date": start_date}
for key, value in iteritems(time_slots):
- start_time, end_time = value.split('-')
+ start_time, end_time = value.split("-")
start_time = get_datetime("{0} {1}".format(start_date.strftime("%Y-%m-%d"), start_time))
end_time = get_datetime("{0} {1}".format(start_date.strftime("%Y-%m-%d"), end_time))
hours_count[key] = get_hours_count(start_time, end_time)
@@ -48,49 +50,57 @@ def get_data(filters):
return data, time_slot_wise_total_count
+
def get_hours_count(start_time, end_time):
- data = frappe.db.sql(""" select count(*) from `tabIssue` where creation
- between %(start_time)s and %(end_time)s""", {
- 'start_time': start_time,
- 'end_time': end_time
- }, as_list=1) or []
+ data = (
+ frappe.db.sql(
+ """ select count(*) from `tabIssue` where creation
+ between %(start_time)s and %(end_time)s""",
+ {"start_time": start_time, "end_time": end_time},
+ as_list=1,
+ )
+ or []
+ )
return data[0][0] if len(data) > 0 else 0
-def get_columns():
- columns = [{
- "fieldname": "date",
- "label": _("Date"),
- "fieldtype": "Date",
- "width": 100
- }]
- for label in ['12AM - 3AM', '3AM - 6AM', '6AM - 9AM',
- '9AM - 12PM', '12PM - 3PM', '3PM - 6PM', '6PM - 9PM', '9PM - 12AM']:
- columns.append({
- "fieldname": label,
- "label": _(label),
- "fieldtype": "Data",
- "width": 120
- })
+def get_columns():
+ columns = [{"fieldname": "date", "label": _("Date"), "fieldtype": "Date", "width": 100}]
+
+ for label in [
+ "12AM - 3AM",
+ "3AM - 6AM",
+ "6AM - 9AM",
+ "9AM - 12PM",
+ "12PM - 3PM",
+ "3PM - 6PM",
+ "6PM - 9PM",
+ "9PM - 12AM",
+ ]:
+ columns.append({"fieldname": label, "label": _(label), "fieldtype": "Data", "width": 120})
return columns
+
def get_chart_data(timeslot_wise_count):
total_count = []
- timeslots = ['12AM - 3AM', '3AM - 6AM', '6AM - 9AM',
- '9AM - 12PM', '12PM - 3PM', '3PM - 6PM', '6PM - 9PM', '9PM - 12AM']
+ timeslots = [
+ "12AM - 3AM",
+ "3AM - 6AM",
+ "6AM - 9AM",
+ "9AM - 12PM",
+ "12PM - 3PM",
+ "3PM - 6PM",
+ "6PM - 9PM",
+ "9PM - 12AM",
+ ]
datasets = []
for data in timeslots:
total_count.append(timeslot_wise_count.get(data, 0))
- datasets.append({'values': total_count})
+ datasets.append({"values": total_count})
- chart = {
- "data": {
- 'labels': timeslots,
- 'datasets': datasets
- }
- }
+ chart = {"data": {"labels": timeslots, "datasets": datasets}}
chart["type"] = "line"
return chart
diff --git a/erpnext/support/web_form/issues/issues.py b/erpnext/support/web_form/issues/issues.py
index 19b550feea7..02e3e933330 100644
--- a/erpnext/support/web_form/issues/issues.py
+++ b/erpnext/support/web_form/issues/issues.py
@@ -1,5 +1,3 @@
-
-
def get_context(context):
# do your magic here
pass
diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py
index 0c24484bdfb..1c88883abce 100644
--- a/erpnext/telephony/doctype/call_log/call_log.py
+++ b/erpnext/telephony/doctype/call_log/call_log.py
@@ -11,8 +11,8 @@ from frappe.model.document import Document
from erpnext.crm.doctype.lead.lead import get_lead_with_phone_number
from erpnext.crm.doctype.utils import get_scheduled_employees_for_popup, strip_number
-END_CALL_STATUSES = ['No Answer', 'Completed', 'Busy', 'Failed']
-ONGOING_CALL_STATUSES = ['Ringing', 'In Progress']
+END_CALL_STATUSES = ["No Answer", "Completed", "Busy", "Failed"]
+ONGOING_CALL_STATUSES = ["Ringing", "In Progress"]
class CallLog(Document):
@@ -20,18 +20,17 @@ class CallLog(Document):
deduplicate_dynamic_links(self)
def before_insert(self):
- """Add lead(third party person) links to the document.
- """
- lead_number = self.get('from') if self.is_incoming_call() else self.get('to')
+ """Add lead(third party person) links to the document."""
+ lead_number = self.get("from") if self.is_incoming_call() else self.get("to")
lead_number = strip_number(lead_number)
contact = get_contact_with_phone_number(strip_number(lead_number))
if contact:
- self.add_link(link_type='Contact', link_name=contact)
+ self.add_link(link_type="Contact", link_name=contact)
lead = get_lead_with_phone_number(lead_number)
if lead:
- self.add_link(link_type='Lead', link_name=lead)
+ self.add_link(link_type="Lead", link_name=lead)
def after_insert(self):
self.trigger_call_popup()
@@ -39,29 +38,29 @@ class CallLog(Document):
def on_update(self):
def _is_call_missed(doc_before_save, doc_after_save):
# FIXME: This works for Exotel but not for all telepony providers
- return doc_before_save.to != doc_after_save.to and doc_after_save.status not in END_CALL_STATUSES
+ return (
+ doc_before_save.to != doc_after_save.to and doc_after_save.status not in END_CALL_STATUSES
+ )
def _is_call_ended(doc_before_save, doc_after_save):
return doc_before_save.status not in END_CALL_STATUSES and self.status in END_CALL_STATUSES
doc_before_save = self.get_doc_before_save()
- if not doc_before_save: return
+ if not doc_before_save:
+ return
if _is_call_missed(doc_before_save, self):
- frappe.publish_realtime('call_{id}_missed'.format(id=self.id), self)
+ frappe.publish_realtime("call_{id}_missed".format(id=self.id), self)
self.trigger_call_popup()
if _is_call_ended(doc_before_save, self):
- frappe.publish_realtime('call_{id}_ended'.format(id=self.id), self)
+ frappe.publish_realtime("call_{id}_ended".format(id=self.id), self)
def is_incoming_call(self):
- return self.type == 'Incoming'
+ return self.type == "Incoming"
def add_link(self, link_type, link_name):
- self.append('links', {
- 'link_doctype': link_type,
- 'link_name': link_name
- })
+ self.append("links", {"link_doctype": link_type, "link_name": link_name})
def trigger_call_popup(self):
if self.is_incoming_call():
@@ -72,53 +71,63 @@ class CallLog(Document):
emails = set(scheduled_employees).intersection(employee_emails)
if frappe.conf.developer_mode:
- self.add_comment(text=f"""
+ self.add_comment(
+ text=f"""
Scheduled Employees: {scheduled_employees}
Matching Employee: {employee_emails}
Show Popup To: {emails}
- """)
+ """
+ )
if employee_emails and not emails:
self.add_comment(text=_("No employee was scheduled for call popup"))
for email in emails:
- frappe.publish_realtime('show_call_popup', self, user=email)
+ frappe.publish_realtime("show_call_popup", self, user=email)
@frappe.whitelist()
def add_call_summary(call_log, summary):
- doc = frappe.get_doc('Call Log', call_log)
- doc.add_comment('Comment', frappe.bold(_('Call Summary')) + '
" + summary)
+
def get_employees_with_number(number):
number = strip_number(number)
- if not number: return []
+ if not number:
+ return []
- employee_emails = frappe.cache().hget('employees_with_number', number)
- if employee_emails: return employee_emails
+ employee_emails = frappe.cache().hget("employees_with_number", number)
+ if employee_emails:
+ return employee_emails
- employees = frappe.get_all('Employee', filters={
- 'cell_number': ['like', '%{}%'.format(number)],
- 'user_id': ['!=', '']
- }, fields=['user_id'])
+ employees = frappe.get_all(
+ "Employee",
+ filters={"cell_number": ["like", "%{}%".format(number)], "user_id": ["!=", ""]},
+ fields=["user_id"],
+ )
employee_emails = [employee.user_id for employee in employees]
- frappe.cache().hset('employees_with_number', number, employee_emails)
+ frappe.cache().hset("employees_with_number", number, employee_emails)
return employee_emails
+
def link_existing_conversations(doc, state):
"""
Called from hooks on creation of Contact or Lead to link all the existing conversations.
"""
- if doc.doctype != 'Contact': return
+ if doc.doctype != "Contact":
+ return
try:
numbers = [d.phone for d in doc.phone_nos]
for number in numbers:
number = strip_number(number)
- if not number: continue
- logs = frappe.db.sql_list("""
+ if not number:
+ continue
+ logs = frappe.db.sql_list(
+ """
SELECT cl.name FROM `tabCall Log` cl
LEFT JOIN `tabDynamic Link` dl
ON cl.name = dl.parent
@@ -131,44 +140,42 @@ def link_existing_conversations(doc, state):
ELSE 0
END
)=0
- """, dict(
- phone_number='%{}'.format(number),
- docname=doc.name,
- doctype = doc.doctype
- )
+ """,
+ dict(phone_number="%{}".format(number), docname=doc.name, doctype=doc.doctype),
)
for log in logs:
- call_log = frappe.get_doc('Call Log', log)
+ call_log = frappe.get_doc("Call Log", log)
call_log.add_link(link_type=doc.doctype, link_name=doc.name)
call_log.save(ignore_permissions=True)
frappe.db.commit()
except Exception:
- frappe.log_error(title=_('Error during caller information update'))
+ frappe.log_error(title=_("Error during caller information update"))
+
def get_linked_call_logs(doctype, docname):
# content will be shown in timeline
- logs = frappe.get_all('Dynamic Link', fields=['parent'], filters={
- 'parenttype': 'Call Log',
- 'link_doctype': doctype,
- 'link_name': docname
- })
+ logs = frappe.get_all(
+ "Dynamic Link",
+ fields=["parent"],
+ filters={"parenttype": "Call Log", "link_doctype": doctype, "link_name": docname},
+ )
logs = set([log.parent for log in logs])
- logs = frappe.get_all('Call Log', fields=['*'], filters={
- 'name': ['in', logs]
- })
+ logs = frappe.get_all("Call Log", fields=["*"], filters={"name": ["in", logs]})
timeline_contents = []
for log in logs:
log.show_call_button = 0
- timeline_contents.append({
- 'icon': 'call',
- 'is_card': True,
- 'creation': log.creation,
- 'template': 'call_link',
- 'template_data': log
- })
+ timeline_contents.append(
+ {
+ "icon": "call",
+ "is_card": True,
+ "creation": log.creation,
+ "template": "call_link",
+ "template_data": log,
+ }
+ )
return timeline_contents
diff --git a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py
index 08e244d8897..5edf81df736 100644
--- a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py
+++ b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py
@@ -20,35 +20,38 @@ class IncomingCallSettings(Document):
self.validate_call_schedule_overlaps(self.call_handling_schedule)
def validate_call_schedule_timeslot(self, schedule: list):
- """ Make sure that to time slot is ahead of from time slot.
- """
+ """Make sure that to time slot is ahead of from time slot."""
errors = []
for record in schedule:
from_time = self.time_to_seconds(record.from_time)
to_time = self.time_to_seconds(record.to_time)
if from_time >= to_time:
errors.append(
- _('Call Schedule Row {0}: To time slot should always be ahead of From time slot.').format(record.idx)
+ _("Call Schedule Row {0}: To time slot should always be ahead of From time slot.").format(
+ record.idx
+ )
)
if errors:
- frappe.throw(' '.join(errors))
+ frappe.throw(" ".join(errors))
def validate_call_schedule_overlaps(self, schedule: list):
- """Check if any time slots are overlapped in a day schedule.
- """
+ """Check if any time slots are overlapped in a day schedule."""
week_days = set([each.day_of_week for each in schedule])
for day in week_days:
- timeslots = [(record.from_time, record.to_time) for record in schedule if record.day_of_week==day]
+ timeslots = [
+ (record.from_time, record.to_time) for record in schedule if record.day_of_week == day
+ ]
# convert time in timeslot into an integer represents number of seconds
timeslots = sorted(map(lambda seq: tuple(map(self.time_to_seconds, seq)), timeslots))
- if len(timeslots) < 2: continue
+ if len(timeslots) < 2:
+ continue
for i in range(1, len(timeslots)):
- if self.check_timeslots_overlap(timeslots[i-1], timeslots[i]):
- frappe.throw(_('Please fix overlapping time slots for {0}.').format(day))
+ if self.check_timeslots_overlap(timeslots[i - 1], timeslots[i]):
+ frappe.throw(_("Please fix overlapping time slots for {0}.").format(day))
@staticmethod
def check_timeslots_overlap(ts1: Tuple[int, int], ts2: Tuple[int, int]) -> bool:
@@ -58,7 +61,6 @@ class IncomingCallSettings(Document):
@staticmethod
def time_to_seconds(time: str) -> int:
- """Convert time string of format HH:MM:SS into seconds
- """
+ """Convert time string of format HH:MM:SS into seconds"""
date_time = datetime.strptime(time, "%H:%M:%S")
return date_time - datetime(1900, 1, 1)
diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html
index 218481581c5..3926f77b1fa 100644
--- a/erpnext/templates/generators/item_group.html
+++ b/erpnext/templates/generators/item_group.html
@@ -52,24 +52,6 @@
-
diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html
index 892d62513e2..fb4cecf8266 100644
--- a/erpnext/templates/includes/macros.html
+++ b/erpnext/templates/includes/macros.html
@@ -300,13 +300,13 @@
{% if values | len > 20 %}
-
+
{% endif %}
{% if values %}