Merge branch 'develop' into patch-5
diff --git a/.eslintrc b/.eslintrc
index e40502a..cb45ce5 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -147,11 +147,14 @@
"Chart": true,
"Cypress": true,
"cy": true,
+ "describe": true,
+ "expect": true,
"it": true,
"context": true,
"before": true,
"beforeEach": true,
"onScan": true,
- "extend_cscript": true
+ "extend_cscript": true,
+ "localforage": true,
}
}
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index f7a7122..455ab86 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -42,6 +42,6 @@
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app erpnext "${GITHUB_WORKSPACE}"
-bench start &
+bench start &> bench_run_logs.txt &
bench --site test_site reinstall --yes
bench build --app frappe
diff --git a/.github/helper/semgrep_rules/frappe_correctness.yml b/.github/helper/semgrep_rules/frappe_correctness.yml
index faab334..d9603e8 100644
--- a/.github/helper/semgrep_rules/frappe_correctness.yml
+++ b/.github/helper/semgrep_rules/frappe_correctness.yml
@@ -98,8 +98,6 @@
languages: [python]
severity: WARNING
paths:
- exclude:
- - test_*.py
include:
- "*/**/doctype/*"
diff --git a/.github/helper/semgrep_rules/security.yml b/.github/helper/semgrep_rules/security.yml
index 5a5098b..8b21979 100644
--- a/.github/helper/semgrep_rules/security.yml
+++ b/.github/helper/semgrep_rules/security.yml
@@ -8,18 +8,3 @@
dynamic content. Avoid it or use safe_eval().
languages: [python]
severity: ERROR
-
-- id: frappe-sqli-format-strings
- patterns:
- - pattern-inside: |
- @frappe.whitelist()
- def $FUNC(...):
- ...
- - pattern-either:
- - pattern: frappe.db.sql("..." % ...)
- - pattern: frappe.db.sql(f"...", ...)
- - pattern: frappe.db.sql("...".format(...), ...)
- message: |
- Detected use of raw string formatting for SQL queries. This can lead to sql injection vulnerabilities. Refer security guidelines - https://github.com/frappe/erpnext/wiki/Code-Security-Guidelines
- languages: [python]
- severity: WARNING
diff --git a/.github/stale.yml b/.github/stale.yml
index dabc66eb..9322ae8 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -1,11 +1,11 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
-daysUntilStale: 30
+daysUntilStale: 15
# Number of days of inactivity before a stale Issue or Pull Request is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
-daysUntilClose: 7
+daysUntilClose: 3
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml
index 7c6b843..1d180f2 100644
--- a/.github/workflows/backport.yml
+++ b/.github/workflows/backport.yml
@@ -1,16 +1,25 @@
name: Backport
on:
- pull_request:
+ pull_request_target:
types:
- closed
- labeled
jobs:
- backport:
- runs-on: ubuntu-18.04
- name: Backport
+ main:
+ runs-on: ubuntu-latest
steps:
- - name: Backport
- uses: tibdex/backport@v1
+ - name: Checkout Actions
+ uses: actions/checkout@v2
with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
+ repository: "frappe/backport"
+ path: ./actions
+ ref: develop
+ - name: Install Actions
+ run: npm install --production --prefix ./actions
+ - name: Run backport
+ uses: ./actions/backport
+ with:
+ token: ${{secrets.BACKPORT_BOT_TOKEN}}
+ labelsToAdd: "backport"
+ title: "{{originalTitle}}"
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
index 389524e..e27b406 100644
--- a/.github/workflows/semgrep.yml
+++ b/.github/workflows/semgrep.yml
@@ -1,34 +1,18 @@
name: Semgrep
on:
- pull_request:
- branches:
- - develop
- - version-13-hotfix
- - version-13-pre-release
+ pull_request: { }
+
jobs:
semgrep:
name: Frappe Linter
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - name: Setup python3
- uses: actions/setup-python@v2
- with:
- python-version: 3.8
-
- - name: Setup semgrep
- run: |
- python -m pip install -q semgrep
- git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
-
- - name: Semgrep errors
- run: |
- files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
- [[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files
- semgrep --config="r/python.lang.correctness" --quiet --error $files
-
- - name: Semgrep warnings
- run: |
- files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
- [[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files
+ - uses: actions/checkout@v2
+ - uses: returntocorp/semgrep-action@v1
+ env:
+ SEMGREP_TIMEOUT: 120
+ with:
+ config: >-
+ r/python.lang.correctness
+ .github/helper/semgrep_rules
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
new file mode 100644
index 0000000..412a05b
--- /dev/null
+++ b/.github/workflows/ui-tests.yml
@@ -0,0 +1,108 @@
+name: UI
+
+on:
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ test:
+ runs-on: ubuntu-18.04
+
+ strategy:
+ fail-fast: false
+
+ name: UI Tests (Cypress)
+
+ services:
+ mysql:
+ image: mariadb:10.3
+ env:
+ MYSQL_ALLOW_EMPTY_PASSWORD: YES
+ ports:
+ - 3306:3306
+ options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
+
+ steps:
+ - name: Clone
+ uses: actions/checkout@v2
+
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.7
+
+ - uses: actions/setup-node@v2
+ with:
+ node-version: 14
+ check-latest: true
+
+ - name: Add to Hosts
+ run: |
+ echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
+
+ - name: Cache pip
+ uses: actions/cache@v2
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+ ${{ runner.os }}-
+
+ - name: Cache node modules
+ uses: actions/cache@v2
+ env:
+ cache-name: cache-node-modules
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-build-${{ env.cache-name }}-
+ ${{ runner.os }}-build-
+ ${{ runner.os }}-
+
+ - name: Get yarn cache directory path
+ id: yarn-cache-dir-path
+ run: echo "::set-output name=dir::$(yarn cache dir)"
+
+ - uses: actions/cache@v2
+ id: yarn-cache
+ with:
+ path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+ key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-yarn-
+
+ - name: Cache cypress binary
+ uses: actions/cache@v2
+ with:
+ path: ~/.cache
+ key: ${{ runner.os }}-cypress-
+ restore-keys: |
+ ${{ runner.os }}-cypress-
+ ${{ runner.os }}-
+
+ - name: Install
+ run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
+ env:
+ DB: mariadb
+ TYPE: ui
+
+ - name: Site Setup
+ run: cd ~/frappe-bench/ && bench --site test_site execute erpnext.setup.utils.before_tests
+
+ - name: cypress pre-requisites
+ run: cd ~/frappe-bench/apps/frappe && yarn add cypress-file-upload@^5 --no-lockfile
+
+
+ - name: Build Assets
+ run: cd ~/frappe-bench/ && bench build
+
+ - name: UI Tests
+ run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless
+ env:
+ CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+
+ - name: Show bench console if tests failed
+ if: ${{ failure() }}
+ run: cat ~/frappe-bench/bench_run_logs.txt
diff --git a/.gitignore b/.gitignore
index 63c51c4..89f5626 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@
.idea/
.vscode/
node_modules/
+.backportrc.json
\ No newline at end of file
diff --git a/CODEOWNERS b/CODEOWNERS
index 7cf65a7..a4a14de 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -3,16 +3,33 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
-manufacturing/ @rohitwaghchaure @marination
-accounts/ @deepeshgarg007 @nextchamp-saqib
-loan_management/ @deepeshgarg007 @rohitwaghchaure
-pos* @nextchamp-saqib @rohitwaghchaure
-assets/ @nextchamp-saqib @deepeshgarg007
-stock/ @marination @rohitwaghchaure
-buying/ @marination @deepeshgarg007
-hr/ @Anurag810 @rohitwaghchaure
-projects/ @hrwX @nextchamp-saqib
-support/ @hrwX @marination
-healthcare/ @ruchamahabal @marination
-erpnext_integrations/ @Mangesh-Khairnar @nextchamp-saqib
-requirements.txt @gavindsouza
+erpnext/accounts/ @nextchamp-saqib @deepeshgarg007
+erpnext/assets/ @nextchamp-saqib @deepeshgarg007
+erpnext/erpnext_integrations/ @nextchamp-saqib
+erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
+erpnext/regional @nextchamp-saqib @deepeshgarg007
+erpnext/selling @nextchamp-saqib @deepeshgarg007
+erpnext/support/ @nextchamp-saqib @deepeshgarg007
+pos* @nextchamp-saqib
+
+erpnext/buying/ @marination @rohitwaghchaure @ankush
+erpnext/e_commerce/ @marination
+erpnext/maintenance/ @marination @rohitwaghchaure
+erpnext/manufacturing/ @marination @rohitwaghchaure @ankush
+erpnext/portal/ @marination
+erpnext/quality_management/ @marination @rohitwaghchaure
+erpnext/shopping_cart/ @marination
+erpnext/stock/ @marination @rohitwaghchaure @ankush
+
+erpnext/crm/ @ruchamahabal @pateljannat
+erpnext/education/ @ruchamahabal @pateljannat
+erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand
+erpnext/hr/ @ruchamahabal @pateljannat
+erpnext/non_profit/ @ruchamahabal
+erpnext/payroll @ruchamahabal @pateljannat
+erpnext/projects/ @ruchamahabal @pateljannat
+
+erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
+
+.github/ @surajshetty3416 @ankush
+requirements.txt @gavindsouza
diff --git a/cypress.json b/cypress.json
new file mode 100644
index 0000000..02b10d8
--- /dev/null
+++ b/cypress.json
@@ -0,0 +1,11 @@
+{
+ "baseUrl": "http://test_site:8000/",
+ "projectId": "da59y9",
+ "adminPassword": "admin",
+ "defaultCommandTimeout": 20000,
+ "pageLoadTimeout": 15000,
+ "retries": {
+ "runMode": 2,
+ "openMode": 2
+ }
+}
diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json
new file mode 100644
index 0000000..da18d93
--- /dev/null
+++ b/cypress/fixtures/example.json
@@ -0,0 +1,5 @@
+{
+ "name": "Using fixtures to represent data",
+ "email": "hello@cypress.io",
+ "body": "Fixtures are a great way to mock data for responses to routes"
+}
\ No newline at end of file
diff --git a/cypress/integration/test_customer.js b/cypress/integration/test_customer.js
new file mode 100644
index 0000000..3d6ed5d
--- /dev/null
+++ b/cypress/integration/test_customer.js
@@ -0,0 +1,13 @@
+
+context('Customer', () => {
+ before(() => {
+ cy.login();
+ });
+ it('Check Customer Group', () => {
+ cy.visit(`app/customer/`);
+ cy.get('.primary-action').click();
+ cy.wait(500);
+ cy.get('.custom-actions > .btn').click();
+ cy.get_field('customer_group', 'Link').should('have.value', 'All Customer Groups');
+ });
+});
diff --git a/cypress/integration/test_item.js b/cypress/integration/test_item.js
new file mode 100644
index 0000000..fcb7533
--- /dev/null
+++ b/cypress/integration/test_item.js
@@ -0,0 +1,44 @@
+describe("Test Item Dashboard", () => {
+ before(() => {
+ cy.login();
+ cy.visit("/app/item");
+ cy.insert_doc(
+ "Item",
+ {
+ item_code: "e2e_test_item",
+ item_group: "All Item Groups",
+ opening_stock: 42,
+ valuation_rate: 100,
+ },
+ true
+ );
+ cy.go_to_doc("item", "e2e_test_item");
+ });
+
+ it("should show dashboard with correct data on first load", () => {
+ cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
+ cy.get(".stock-levels").contains("e2e_test_item").should("exist");
+
+ // reserved and available qty
+ cy.get(".stock-levels .inline-graph-count")
+ .eq(0)
+ .contains("0")
+ .should("exist");
+ cy.get(".stock-levels .inline-graph-count")
+ .eq(1)
+ .contains("42")
+ .should("exist");
+ });
+
+ it("should persist on field change", () => {
+ cy.get('input[data-fieldname="disabled"]').check();
+ cy.wait(500);
+ cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
+ cy.get(".stock-levels").should("have.length", 1);
+ });
+
+ it("should persist on reload", () => {
+ cy.reload();
+ cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
+ });
+});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
new file mode 100644
index 0000000..07d9804
--- /dev/null
+++ b/cypress/plugins/index.js
@@ -0,0 +1,17 @@
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+module.exports = () => {
+ // `on` is used to hook into various events Cypress emits
+ // `config` is the resolved Cypress config
+};
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
new file mode 100644
index 0000000..7ddc80a
--- /dev/null
+++ b/cypress/support/commands.js
@@ -0,0 +1,31 @@
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add("login", (email, password) => { ... });
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... });
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... });
+//
+//
+// -- This is will overwrite an existing command --
+// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... });
+
+const slug = (name) => name.toLowerCase().replace(" ", "-");
+
+Cypress.Commands.add("go_to_doc", (doctype, name) => {
+ cy.visit(`/app/${slug(doctype)}/${encodeURIComponent(name)}`);
+});
diff --git a/cypress/support/index.js b/cypress/support/index.js
new file mode 100644
index 0000000..72070cc
--- /dev/null
+++ b/cypress/support/index.js
@@ -0,0 +1,26 @@
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands';
+import '../../../frappe/cypress/support/commands' // eslint-disable-line
+
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
+
+Cypress.Cookies.defaults({
+ preserve: 'sid'
+});
diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json
new file mode 100644
index 0000000..d90ebf6
--- /dev/null
+++ b/cypress/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "baseUrl": "../node_modules",
+ "types": [
+ "cypress"
+ ]
+ },
+ "include": [
+ "**/*.*"
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 0c96d32..a181c2d 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -5,7 +5,7 @@
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
-__version__ = '13.6.0'
+__version__ = '13.7.1'
def get_default_company(user=None):
'''Get default company for user'''
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index 2f86c6c..335e8a1 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -301,17 +301,21 @@
start_date = add_months(today(), -1)
end_date = add_days(today(), -1)
- for record_type in ('Income', 'Expense'):
- doc = frappe.get_doc(dict(
- doctype='Process Deferred Accounting',
- posting_date=posting_date,
- start_date=start_date,
- end_date=end_date,
- type=record_type
- ))
+ companies = frappe.get_all('Company')
- doc.insert()
- doc.submit()
+ 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
+ ))
+
+ 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):
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 5f110e2..ffc9d1c 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
@@ -51,7 +51,7 @@
self.import_file, self.google_sheets_url
)
- if 'Bank Account' not in json.dumps(preview):
+ 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
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 3b764aa..4fd8413 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
@@ -13,7 +13,8 @@
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file, read_xls_file_from_attached_file
class ChartofAccountsImporter(Document):
- pass
+ def validate(self):
+ validate_accounts(self.import_file)
@frappe.whitelist()
def validate_company(company):
@@ -301,28 +302,27 @@
if account["parent_account"] and accounts_dict.get(account["parent_account"]):
accounts_dict[account["parent_account"]]["is_group"] = 1
- message = validate_root(accounts_dict)
- if message: return message
- message = validate_account_types(accounts_dict)
- if message: return message
+ validate_root(accounts_dict)
+
+ validate_account_types(accounts_dict)
return [True, len(accounts)]
def validate_root(accounts):
roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
if len(roots) < 4:
- return _("Number of root accounts cannot be less than 4")
+ frappe.throw(_("Number of root accounts cannot be less than 4"))
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")))
if error_messages:
- return "<br>".join(error_messages)
+ frappe.throw("<br>".join(error_messages))
def get_root_types():
return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
@@ -356,7 +356,7 @@
missing = list(set(account_types_for_ledger) - set(account_types))
if missing:
- return _("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing))
+ frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)))
account_types_for_group = ["Bank", "Cash", "Stock"]
# fix logic bug
@@ -364,7 +364,7 @@
missing = list(set(account_types_for_group) - set(account_groups))
if missing:
- return _("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing))
+ frappe.throw(_("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)))
def unset_existing_data(company):
linked = frappe.db.sql('''select fieldname from tabDocField
diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py
index c6c6892..1ef512a 100644
--- a/erpnext/accounts/doctype/dunning/dunning.py
+++ b/erpnext/accounts/doctype/dunning/dunning.py
@@ -25,7 +25,7 @@
def validate_amount(self):
amounts = calculate_interest_and_amount(
- self.posting_date, self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days)
+ 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'):
@@ -91,13 +91,13 @@
for dunning in dunnings:
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')
-def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
+def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
interest_amount = 0
- grand_total = 0
+ grand_total = flt(outstanding_amount) + flt(dunning_fee)
if rate_of_interest:
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
interest_amount = (interest_per_year * cint(overdue_days)) / 365
- grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee)
+ grand_total += flt(interest_amount)
dunning_amount = flt(interest_amount) + flt(dunning_fee)
return {
'interest_amount': interest_amount,
diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py
index e2d4d82..7fc2e4b 100644
--- a/erpnext/accounts/doctype/dunning/test_dunning.py
+++ b/erpnext/accounts/doctype/dunning/test_dunning.py
@@ -16,6 +16,7 @@
@classmethod
def setUpClass(self):
create_dunning_type()
+ create_dunning_type_with_zero_interest_rate()
unlink_payment_on_cancel_of_invoice()
@classmethod
@@ -25,11 +26,19 @@
def test_dunning(self):
dunning = create_dunning()
amounts = calculate_interest_and_amount(
- dunning.posting_date, dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
+ 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)
+
def test_gl_entries(self):
dunning = create_dunning()
dunning.submit()
@@ -83,6 +92,27 @@
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')
+ dunning = frappe.new_doc("Dunning")
+ dunning.sales_invoice = sales_invoice.name
+ dunning.customer_name = sales_invoice.customer_name
+ dunning.outstanding_amount = sales_invoice.outstanding_amount
+ dunning.debit_to = sales_invoice.debit_to
+ dunning.currency = sales_invoice.currency
+ 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.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'
@@ -98,3 +128,19 @@
}
)
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.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_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 5619321..f2b0a8c 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
@@ -27,6 +27,9 @@
if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries"))
+ def on_cancel(self):
+ self.ignore_linked_doctypes = ('GL Entry')
+
@frappe.whitelist()
def check_journal_entry_condition(self):
total_debit = frappe.db.get_value("Journal Entry Account", {
@@ -99,10 +102,12 @@
sum(debit) - sum(credit) as balance
from `tabGL Entry`
where account in (%s)
- group by account, party_type, party
+ and posting_date <= %s
+ and is_cancelled = 0
+ group by account, NULLIF(party_type,''), NULLIF(party,'')
having sum(debit) != sum(credit)
order by account
- """ % ', '.join(['%s']*len(accounts)), tuple(accounts), as_dict=1)
+ """ % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1)
return account_details
@@ -143,9 +148,9 @@
"party_type": d.get("party_type"),
"party": d.get("party"),
"account_currency": d.get("account_currency"),
- "balance": d.get("balance_in_account_currency"),
- dr_or_cr: abs(d.get("balance_in_account_currency")),
- "exchange_rate":d.get("new_exchange_rate"),
+ "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,
})
@@ -154,9 +159,9 @@
"party_type": d.get("party_type"),
"party": d.get("party"),
"account_currency": d.get("account_currency"),
- "balance": d.get("balance_in_account_currency"),
- reverse_dr_or_cr: abs(d.get("balance_in_account_currency")),
- "exchange_rate": d.get("current_exchange_rate"),
+ "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
})
@@ -185,9 +190,9 @@
account_details = {}
company_currency = erpnext.get_company_currency(company)
- balance = get_balance_on(account, party_type=party_type, party=party, in_account_currency=False)
+ balance = get_balance_on(account, date=posting_date, party_type=party_type, party=party, in_account_currency=False)
if balance:
- balance_in_account_currency = get_balance_on(account, party_type=party_type, party=party)
+ balance_in_account_currency = get_balance_on(account, date=posting_date, party_type=party_type, party=party)
current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0
new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index 51f18a5..6f362c1 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -667,6 +667,7 @@
{
"fieldname": "base_paid_amount_after_tax",
"fieldtype": "Currency",
+ "hidden": 1,
"label": "Paid Amount After Tax (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
@@ -693,21 +694,25 @@
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
"fieldname": "received_amount_after_tax",
"fieldtype": "Currency",
+ "hidden": 1,
"label": "Received Amount After Tax",
- "options": "paid_to_account_currency"
+ "options": "paid_to_account_currency",
+ "read_only": 1
},
{
"depends_on": "doc.received_amount",
"fieldname": "base_received_amount_after_tax",
"fieldtype": "Currency",
+ "hidden": 1,
"label": "Received Amount After Tax (Company Currency)",
- "options": "Company:company:default_currency"
+ "options": "Company:company:default_currency",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-06-22 20:37:06.154206",
+ "modified": "2021-07-09 08:58:15.008761",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index adaf99a..46904f7 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -183,6 +183,13 @@
d.reference_name, self.party_account_currency)
for field, value in iteritems(ref_details):
+ if d.exchange_gain_loss:
+ # for cases where gain/loss is booked into invoice
+ # exchange_gain_loss is calculated from invoice & populated
+ # and row.exchange_rate is already set to payment entry's exchange rate
+ # refer -> `update_reference_in_payment_entry()` in utils.py
+ continue
+
if field == 'exchange_rate' or not d.get(field) or force:
d.db_set(field, value)
@@ -404,9 +411,15 @@
if not self.advance_tax_account:
frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction"))
- reference_doclist = []
net_total = self.paid_amount
- included_in_paid_amount = 0
+
+ for reference in self.get("references"):
+ net_total_for_tds = 0
+ if reference.reference_doctype == 'Purchase Order':
+ net_total_for_tds += flt(frappe.db.get_value('Purchase Order', reference.reference_name, 'net_total'))
+
+ if net_total_for_tds:
+ net_total = net_total_for_tds
# Adding args as purchase invoice to get TDS amount
args = frappe._dict({
@@ -423,7 +436,7 @@
return
tax_withholding_details.update({
- 'included_in_paid_amount': included_in_paid_amount,
+ 'add_deduct_tax': 'Add',
'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company)
})
@@ -512,16 +525,19 @@
self.unallocated_amount = 0
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_after_tax + total_deductions \
- and self.total_allocated_amount < self.paid_amount_after_tax + (total_deductions / self.source_exchange_rate):
- self.unallocated_amount = (self.received_amount_after_tax + total_deductions -
+ 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.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_after_tax - total_deductions) \
- and self.total_allocated_amount < self.received_amount_after_tax + (total_deductions / self.target_exchange_rate):
- self.unallocated_amount = (self.base_paid_amount_after_tax - (total_deductions +
+ 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)
@@ -530,17 +546,29 @@
base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount)
if self.payment_type == "Receive":
- self.difference_amount = base_party_amount - self.base_received_amount_after_tax
+ self.difference_amount = base_party_amount - self.base_received_amount
elif self.payment_type == "Pay":
- self.difference_amount = self.base_paid_amount_after_tax - base_party_amount
+ self.difference_amount = self.base_paid_amount - base_party_amount
else:
- self.difference_amount = self.base_paid_amount_after_tax - flt(self.base_received_amount_after_tax)
+ self.difference_amount = self.base_paid_amount - flt(self.base_received_amount)
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,
+ 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'):
+ if tax.included_in_paid_amount:
+ if tax.add_deduct_tax == 'Add':
+ included_taxes += tax.base_tax_amount
+ else:
+ included_taxes -= tax.base_tax_amount
+
+ return included_taxes
+
# Paid amount is auto allocated in the reference document by default.
# 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):
@@ -664,8 +692,8 @@
gl_entries.append(gle)
if self.unallocated_amount:
- base_unallocated_amount = self.unallocated_amount * \
- (self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate)
+ exchange_rate = self.get_exchange_rate()
+ base_unallocated_amount = (self.unallocated_amount * exchange_rate)
gle = party_gl_dict.copy()
@@ -683,8 +711,8 @@
"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_after_tax,
- "credit": self.base_paid_amount_after_tax,
+ "credit_in_account_currency": self.paid_amount,
+ "credit": self.base_paid_amount,
"cost_center": self.cost_center
}, item=self)
)
@@ -694,8 +722,8 @@
"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_after_tax,
- "debit": self.base_received_amount_after_tax,
+ "debit_in_account_currency": self.received_amount,
+ "debit": self.base_received_amount,
"cost_center": self.cost_center
}, item=self)
)
@@ -708,35 +736,42 @@
if self.payment_type in ('Pay', 'Internal Transfer'):
dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit"
+ against = self.party or self.paid_from
elif self.payment_type == 'Receive':
dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit"
+ against = self.party or self.paid_to
payment_or_advance_account = self.get_party_account_for_taxes()
+ tax_amount = d.tax_amount
+ base_tax_amount = d.base_tax_amount
+
+ if self.advance_tax_account:
+ tax_amount = -1 * tax_amount
+ base_tax_amount = -1 * base_tax_amount
gl_entries.append(
self.get_gl_dict({
"account": d.account_head,
- "against": self.party if self.payment_type=="Receive" else self.paid_from,
- dr_or_cr: d.base_tax_amount,
- dr_or_cr + "_in_account_currency": d.base_tax_amount
+ "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
}, account_currency, item=d))
#Intentionally use -1 to get net values in party account
- gl_entries.append(
- self.get_gl_dict({
- "account": payment_or_advance_account,
- "against": self.party if self.payment_type=="Receive" else self.paid_from,
- dr_or_cr: -1 * d.base_tax_amount,
- dr_or_cr + "_in_account_currency": -1*d.base_tax_amount
- if account_currency==self.company_currency
- else d.tax_amount,
- "cost_center": self.cost_center,
- "party_type": self.party_type,
- "party": self.party
- }, account_currency, item=d))
+ if not d.included_in_paid_amount or self.advance_tax_account:
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": payment_or_advance_account,
+ "against": against,
+ dr_or_cr: -1 * tax_amount,
+ dr_or_cr + "_in_account_currency": -1 * base_tax_amount
+ if account_currency==self.company_currency
+ else d.tax_amount,
+ "cost_center": self.cost_center,
+ }, account_currency, item=d))
def add_deductions_gl_entries(self, gl_entries):
for d in self.get("deductions"):
@@ -760,9 +795,9 @@
if self.advance_tax_account:
return self.advance_tax_account
elif self.payment_type == 'Receive':
- return self.paid_from
- elif self.payment_type in ('Pay', 'Internal Transfer'):
return self.paid_to
+ 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:
@@ -806,10 +841,17 @@
if account_details:
row.update(account_details)
+
+ if not row.get('amount'):
+ # if no difference amount
+ return
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
+
def initialize_taxes(self):
for tax in self.get("taxes"):
validate_taxes_and_charges(tax)
@@ -1318,9 +1360,9 @@
return frappe._dict({
"due_date": ref_doc.get("due_date"),
- "total_amount": total_amount,
- "outstanding_amount": outstanding_amount,
- "exchange_rate": exchange_rate,
+ "total_amount": flt(total_amount),
+ "outstanding_amount": flt(outstanding_amount),
+ "exchange_rate": flt(exchange_rate),
"bill_no": bill_no
})
@@ -1634,12 +1676,6 @@
if dt == "Employee Advance":
paid_amount = received_amount * doc.get('exchange_rate', 1)
- if dt == "Purchase Order" and doc.apply_tds:
- if party_account_currency == bank.account_currency:
- paid_amount = received_amount = doc.base_net_total
- else:
- paid_amount = received_amount = doc.base_net_total * doc.get('exchange_rate', 1)
-
return paid_amount, received_amount
def apply_early_payment_discount(paid_amount, received_amount, doc):
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 4641d6b..d1302f5 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -589,9 +589,9 @@
party_account_balance = get_balance_on(account=pe.paid_from, cost_center=pe.cost_center)
self.assertEqual(pe.cost_center, si.cost_center)
- self.assertEqual(expected_account_balance, account_balance)
- self.assertEqual(expected_party_balance, party_balance)
- self.assertEqual(expected_party_account_balance, party_account_balance)
+ self.assertEqual(flt(expected_account_balance), account_balance)
+ self.assertEqual(flt(expected_party_balance), party_balance)
+ self.assertEqual(flt(expected_party_account_balance), party_account_balance)
def create_payment_terms_template():
diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
index 912ad09..43eb0b6 100644
--- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
+++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
@@ -14,7 +14,8 @@
"total_amount",
"outstanding_amount",
"allocated_amount",
- "exchange_rate"
+ "exchange_rate",
+ "exchange_gain_loss"
],
"fields": [
{
@@ -90,12 +91,19 @@
"fieldtype": "Link",
"label": "Payment Term",
"options": "Payment Term"
+ },
+ {
+ "fieldname": "exchange_gain_loss",
+ "fieldtype": "Currency",
+ "label": "Exchange Gain/Loss",
+ "options": "Company:company:default_currency",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-10 11:25:47.144392",
+ "modified": "2021-04-21 13:30:11.605388",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",
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 0b0ee90..500952e 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
@@ -207,10 +207,9 @@
@frappe.whitelist()
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
billing_email = frappe.db.sql("""
- SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent \
- WHERE l.link_doctype='Customer' and l.link_name='""" + customer_name + """' and \
- c.is_billing_contact=1 \
- order by c.creation desc""")
+ SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent
+ WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1
+ order by c.creation desc""", customer_name)
if len(billing_email) == 0 or (billing_email[0][0] is None):
if billing_and_primary:
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index c1cc092..b99d75e 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -451,6 +451,7 @@
self.get_asset_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
+ self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
self.allocate_advance_taxes(gl_entries)
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index ec93314..db6f143 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -231,25 +231,25 @@
self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_purchase_invoice_with_exchange_rate_difference(self):
- pr = make_purchase_receipt(currency = "USD", conversion_rate = 70)
- pi = make_purchase_invoice(currency = "USD", conversion_rate = 80, do_not_save = "True")
+ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as create_purchase_invoice
- pi.items[0].purchase_receipt = pr.name
- pi.items[0].pr_detail = pr.items[0].name
+ pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse='Stores - TCP1',
+ currency = "USD", conversion_rate = 70)
+
+ pi = create_purchase_invoice(pr.name)
+ pi.conversion_rate = 80
pi.insert()
pi.submit()
- # fetching the latest GL Entry with 'Exchange Gain/Loss - _TC' account
- gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - _TC'})
- voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no')
+ # Get exchnage gain and loss account
+ exchange_gain_loss_account = frappe.db.get_value('Company', pi.company, 'exchange_gain_loss_account')
- self.assertEqual(pi.name, voucher_no)
-
- exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit')
+ # fetching the latest GL Entry with exchange gain and loss account account
+ amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pi.name}, 'debit')
discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount)
- self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff)
+ self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
def test_purchase_invoice_change_naming_series(self):
pi = frappe.copy_doc(test_records[1])
@@ -974,6 +974,120 @@
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")
+
+ frappe.db.set_value(
+ "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")
+
+ 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.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.save()
+ pi.submit()
+
+ expected_gle = [
+ ["_Test Account Cost for Goods Sold - _TC", 37500.0],
+ ["_Test Payable USD - _TC", -40000.0],
+ ["Exchange Gain/Loss - _TC", 2500.0]
+ ]
+
+ 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)
+
+ 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.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.save()
+ pi_2.submit()
+
+ expected_gle = [
+ ["_Test Account Cost for Goods Sold - _TC", 36500.0],
+ ["_Test Payable USD - _TC", -38000.0],
+ ["Exchange Gain/Loss - _TC", 1500.0]
+ ]
+
+ 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)
+
+ 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]
+ ]
+
+ 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)
+
+ for i, gle in enumerate(gl_entries):
+ self.assertEqual(expected_gle[i][0], gle.account)
+ self.assertEqual(expected_gle[i][1], gle.balance)
+
+ pi.reload()
+ pi.cancel()
+
+ pi_2.reload()
+ pi_2.cancel()
+
+ pay.reload()
+ pay.cancel()
+
+ 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.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@@ -1031,21 +1145,21 @@
# Check GLE for Purchase Invoice
# Zero net effect on final TDS Payable on invoice
expected_gle = [
- ['_Test Account Cost for Goods Sold - _TC', 30000, 0],
- ['_Test Account Excise Duty - _TC', 0, 3000],
- ['Creditors - _TC', 0, 27000],
- ['TDS Payable - _TC', 3000, 3000]
+ ['_Test Account Cost for Goods Sold - _TC', 30000],
+ ['_Test Account Excise Duty - _TC', -3000],
+ ['Creditors - _TC', -27000],
+ ['TDS Payable - _TC', 0]
]
- gl_entries = frappe.db.sql("""select account, debit, credit
+ 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)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
- self.assertEqual(expected_gle[i][1], gle.debit)
- self.assertEqual(expected_gle[i][2], gle.credit)
+ self.assertEqual(expected_gle[i][1], gle.amount)
def update_tax_witholding_category(company, account, date):
from erpnext.accounts.utils import get_fiscal_year
diff --git a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json
index 5801b17..63dfff8 100644
--- a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json
+++ b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json
@@ -1,235 +1,127 @@
{
- "allow_copy": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2013-03-08 15:36:46",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 1,
+ "actions": [],
+ "creation": "2013-03-08 15:36:46",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "reference_type",
+ "reference_name",
+ "remarks",
+ "reference_row",
+ "col_break1",
+ "advance_amount",
+ "allocated_amount",
+ "exchange_gain_loss",
+ "ref_exchange_rate"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reference_type",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Reference Type",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "journal_voucher",
- "oldfieldtype": "Link",
- "options": "DocType",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "180px",
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "reference_type",
+ "fieldtype": "Link",
+ "label": "Reference Type",
+ "no_copy": 1,
+ "oldfieldname": "journal_voucher",
+ "oldfieldtype": "Link",
+ "options": "DocType",
+ "print_width": "180px",
+ "read_only": 1,
"width": "180px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 3,
- "fieldname": "reference_name",
- "fieldtype": "Dynamic Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Reference Name",
- "length": 0,
- "no_copy": 1,
- "options": "reference_type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "columns": 3,
+ "fieldname": "reference_name",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Reference Name",
+ "no_copy": 1,
+ "options": "reference_type",
+ "read_only": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 3,
- "fieldname": "remarks",
- "fieldtype": "Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Remarks",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "remarks",
- "oldfieldtype": "Small Text",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "150px",
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "columns": 3,
+ "fieldname": "remarks",
+ "fieldtype": "Text",
+ "in_list_view": 1,
+ "label": "Remarks",
+ "no_copy": 1,
+ "oldfieldname": "remarks",
+ "oldfieldtype": "Small Text",
+ "print_width": "150px",
+ "read_only": 1,
"width": "150px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reference_row",
- "fieldtype": "Data",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Reference Row",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "jv_detail_no",
- "oldfieldtype": "Date",
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "print_width": "80px",
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "reference_row",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Reference Row",
+ "no_copy": 1,
+ "oldfieldname": "jv_detail_no",
+ "oldfieldtype": "Date",
+ "print_hide": 1,
+ "print_width": "80px",
+ "read_only": 1,
"width": "80px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "col_break1",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "col_break1",
+ "fieldtype": "Column Break"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "advance_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Advance Amount",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "advance_amount",
- "oldfieldtype": "Currency",
- "options": "party_account_currency",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "100px",
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "columns": 2,
+ "fieldname": "advance_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Advance Amount",
+ "no_copy": 1,
+ "oldfieldname": "advance_amount",
+ "oldfieldtype": "Currency",
+ "options": "party_account_currency",
+ "print_width": "100px",
+ "read_only": 1,
"width": "100px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "allocated_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Allocated Amount",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "allocated_amount",
- "oldfieldtype": "Currency",
- "options": "party_account_currency",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "100px",
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "columns": 2,
+ "fieldname": "allocated_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Allocated Amount",
+ "no_copy": 1,
+ "oldfieldname": "allocated_amount",
+ "oldfieldtype": "Currency",
+ "options": "party_account_currency",
+ "print_width": "100px",
"width": "100px"
+ },
+ {
+ "fieldname": "exchange_gain_loss",
+ "fieldtype": "Currency",
+ "label": "Exchange Gain/Loss",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "ref_exchange_rate",
+ "fieldtype": "Float",
+ "label": "Reference Exchange Rate",
+ "non_negative": 1,
+ "read_only": 1
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "menu_index": 0,
- "modified": "2016-08-26 02:30:54.407138",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Purchase Invoice Advance",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "sort_order": "DESC",
- "track_seen": 0
+ ],
+ "idx": 1,
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-04-20 16:26:53.820530",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Purchase Invoice Advance",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index e7dd6b8..0a9a105 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -48,6 +48,8 @@
"shipping_address",
"company_address",
"company_address_display",
+ "dispatch_address_name",
+ "dispatch_address",
"currency_and_price_list",
"currency",
"conversion_rate",
@@ -1966,6 +1968,21 @@
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
"label": "Disable Rounded Total"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "dispatch_address_name",
+ "fieldtype": "Link",
+ "label": "Dispatch Address Name",
+ "options": "Address",
+ "print_hide": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "dispatch_address",
+ "fieldtype": "Small Text",
+ "label": "Dispatch Address",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
@@ -1978,7 +1995,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2021-05-20 22:48:33.988881",
+ "modified": "2021-07-08 14:03:55.502522",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 55a5b99..8889913 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -13,7 +13,7 @@
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
from erpnext.assets.doctype.asset.depreciation \
- import get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal
+ import get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_delivery_note_serial_no
from erpnext.setup.doctype.company.company import update_company_current_month_sales
@@ -149,7 +149,7 @@
if self.update_stock:
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
- elif asset.status in ("Scrapped", "Cancelled", "Sold"):
+ 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):
@@ -840,6 +840,7 @@
self.make_customer_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
+ self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
self.allocate_advance_taxes(gl_entries)
@@ -917,22 +918,33 @@
for item in self.get("items"):
if flt(item.base_net_amount, item.precision("base_net_amount")):
if item.is_fixed_asset:
- asset = frappe.get_doc("Asset", item.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")
+ )
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))
- fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset,
- item.base_net_amount, item.finance_book)
+ if self.is_return:
+ fixed_asset_gl_entries = get_gl_entries_on_asset_regain(asset,
+ item.base_net_amount, item.finance_book)
+ asset.db_set("disposal_date", None)
+ else:
+ 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)
for gle in fixed_asset_gl_entries:
gle["against"] = self.customer
gl_entries.append(self.get_gl_dict(gle, item=item))
- asset.db_set("disposal_date", self.posting_date)
- asset.set_status("Sold" if self.docstatus==1 else None)
+ self.set_asset_status(asset)
+
else:
# Do not book income for transfer within same company
if not self.is_internal_transfer():
@@ -958,6 +970,12 @@
erpnext.is_perpetual_inventory_enabled(self.company):
gl_entries += super(SalesInvoice, self).get_gl_entries()
+ def set_asset_status(self, asset):
+ if self.is_return:
+ asset.set_status()
+ else:
+ 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(
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index fe531d3..c6e6e3d 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -10,6 +10,7 @@
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
+from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
from frappe.model.naming import make_autoname
@@ -1069,6 +1070,36 @@
self.assertFalse(si1.outstanding_amount)
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500)
+ def test_gle_made_when_asset_is_returned(self):
+ create_asset_data()
+ 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)
+
+ 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"]
+ )[0]
+
+ loss_for_return_si = frappe.get_all(
+ "GL Entry",
+ 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'])
def test_discount_on_net_total(self):
si = frappe.copy_doc(test_records[2])
@@ -1908,6 +1939,8 @@
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
@@ -2061,6 +2094,30 @@
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()
+
+ 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({
@@ -2087,9 +2144,9 @@
if not gst_account:
gst_settings.append("gst_accounts", {
"company": "_Test Company",
- "cgst_account": "CGST - _TC",
- "sgst_account": "SGST - _TC",
- "igst_account": "IGST - _TC",
+ "cgst_account": "Output Tax CGST - _TC",
+ "sgst_account": "Output Tax SGST - _TC",
+ "igst_account": "Output Tax IGST - _TC",
})
gst_settings.save()
@@ -2099,6 +2156,7 @@
si.distance = 2000
si.company_address = "_Test Address for Eway bill-Billing"
si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
+ 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'
@@ -2106,7 +2164,7 @@
si.append("taxes", {
"charge_type": "On Net Total",
- "account_head": "CGST - _TC",
+ "account_head": "Output Tax CGST - _TC",
"cost_center": "Main - _TC",
"description": "CGST @ 9.0",
"rate": 9
@@ -2114,7 +2172,7 @@
si.append("taxes", {
"charge_type": "On Net Total",
- "account_head": "SGST - _TC",
+ "account_head": "Output Tax SGST - _TC",
"cost_center": "Main - _TC",
"description": "SGST @ 9.0",
"rate": 9
@@ -2164,6 +2222,7 @@
"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",
+ "asset": args.asset or None,
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
"conversion_factor": 1
diff --git a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json
index 14bf4d8..29422d6 100644
--- a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json
+++ b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json
@@ -1,235 +1,128 @@
{
- "allow_copy": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2013-02-22 01:27:41",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 1,
+ "actions": [],
+ "creation": "2013-02-22 01:27:41",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "reference_type",
+ "reference_name",
+ "remarks",
+ "reference_row",
+ "col_break1",
+ "advance_amount",
+ "allocated_amount",
+ "exchange_gain_loss",
+ "ref_exchange_rate"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reference_type",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Reference Type",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "journal_voucher",
- "oldfieldtype": "Link",
- "options": "DocType",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "250px",
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "reference_type",
+ "fieldtype": "Link",
+ "label": "Reference Type",
+ "no_copy": 1,
+ "oldfieldname": "journal_voucher",
+ "oldfieldtype": "Link",
+ "options": "DocType",
+ "print_width": "250px",
+ "read_only": 1,
"width": "250px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 3,
- "fieldname": "reference_name",
- "fieldtype": "Dynamic Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Reference Name",
- "length": 0,
- "no_copy": 1,
- "options": "reference_type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "columns": 3,
+ "fieldname": "reference_name",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Reference Name",
+ "no_copy": 1,
+ "options": "reference_type",
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 3,
- "fieldname": "remarks",
- "fieldtype": "Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Remarks",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "remarks",
- "oldfieldtype": "Small Text",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "150px",
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "columns": 3,
+ "fieldname": "remarks",
+ "fieldtype": "Text",
+ "in_list_view": 1,
+ "label": "Remarks",
+ "no_copy": 1,
+ "oldfieldname": "remarks",
+ "oldfieldtype": "Small Text",
+ "print_width": "150px",
+ "read_only": 1,
"width": "150px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reference_row",
- "fieldtype": "Data",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Reference Row",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "jv_detail_no",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "print_width": "120px",
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "reference_row",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Reference Row",
+ "no_copy": 1,
+ "oldfieldname": "jv_detail_no",
+ "oldfieldtype": "Data",
+ "print_hide": 1,
+ "print_width": "120px",
+ "read_only": 1,
"width": "120px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "col_break1",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "col_break1",
+ "fieldtype": "Column Break"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "advance_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Advance amount",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "advance_amount",
- "oldfieldtype": "Currency",
- "options": "party_account_currency",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "120px",
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "columns": 2,
+ "fieldname": "advance_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Advance amount",
+ "no_copy": 1,
+ "oldfieldname": "advance_amount",
+ "oldfieldtype": "Currency",
+ "options": "party_account_currency",
+ "print_width": "120px",
+ "read_only": 1,
"width": "120px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "allocated_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Allocated amount",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "allocated_amount",
- "oldfieldtype": "Currency",
- "options": "party_account_currency",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "120px",
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "columns": 2,
+ "fieldname": "allocated_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Allocated amount",
+ "no_copy": 1,
+ "oldfieldname": "allocated_amount",
+ "oldfieldtype": "Currency",
+ "options": "party_account_currency",
+ "print_width": "120px",
"width": "120px"
+ },
+ {
+ "fieldname": "exchange_gain_loss",
+ "fieldtype": "Currency",
+ "label": "Exchange Gain/Loss",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "ref_exchange_rate",
+ "fieldtype": "Float",
+ "label": "Reference Exchange Rate",
+ "non_negative": 1,
+ "read_only": 1
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "menu_index": 0,
- "modified": "2016-08-26 02:36:10.718057",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Sales Invoice Advance",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "sort_order": "DESC",
- "track_seen": 0
+ ],
+ "idx": 1,
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-06-04 20:25:49.832052",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Sales Invoice Advance",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
index 8e6952a..6690bda 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -743,7 +743,6 @@
"fieldname": "asset",
"fieldtype": "Link",
"label": "Asset",
- "no_copy": 1,
"options": "Asset"
},
{
@@ -826,7 +825,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-23 01:05:22.123527",
+ "modified": "2021-06-21 23:03:11.599901",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json
index 1b7a0fe..cfdb167 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json
@@ -27,7 +27,8 @@
"base_tax_amount",
"base_total",
"base_tax_amount_after_discount_amount",
- "item_wise_tax_detail"
+ "item_wise_tax_detail",
+ "dont_recompute_tax"
],
"fields": [
{
@@ -200,13 +201,22 @@
"fieldname": "included_in_paid_amount",
"fieldtype": "Check",
"label": "Considered In Paid Amount"
+ },
+ {
+ "default": "0",
+ "fieldname": "dont_recompute_tax",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Dont Recompute tax",
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-06-14 01:44:36.899147",
+ "modified": "2021-07-27 12:40:59.051803",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges",
diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.js b/erpnext/accounts/doctype/tax_rule/tax_rule.js
index 370890e..bc49716 100644
--- a/erpnext/accounts/doctype/tax_rule/tax_rule.js
+++ b/erpnext/accounts/doctype/tax_rule/tax_rule.js
@@ -1,24 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-cur_frm.add_fetch("customer", "customer_group", "customer_group" );
-cur_frm.add_fetch("supplier", "supplier_group_name", "supplier_group" );
-
-frappe.ui.form.on("Tax Rule", "tax_type", function(frm) {
- frm.toggle_reqd("sales_tax_template", frm.doc.tax_type=="Sales");
- frm.toggle_reqd("purchase_tax_template", frm.doc.tax_type=="Purchase");
-})
-
-frappe.ui.form.on("Tax Rule", "onload", function(frm) {
- if(frm.doc.__islocal) {
- frm.set_value("use_for_shopping_cart", 1);
- }
-})
-
-frappe.ui.form.on("Tax Rule", "refresh", function(frm) {
- frappe.ui.form.trigger("Tax Rule", "tax_type");
-})
-
frappe.ui.form.on("Tax Rule", "customer", function(frm) {
if(frm.doc.customer) {
frappe.call({
diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.json b/erpnext/accounts/doctype/tax_rule/tax_rule.json
index ef15538..2746748 100644
--- a/erpnext/accounts/doctype/tax_rule/tax_rule.json
+++ b/erpnext/accounts/doctype/tax_rule/tax_rule.json
@@ -1,1103 +1,250 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 0,
- "autoname": "ACC-TAX-RULE-.YYYY.-.#####",
- "beta": 0,
- "creation": "2015-08-07 02:33:52.670866",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 0,
+ "actions": [],
+ "allow_import": 1,
+ "autoname": "ACC-TAX-RULE-.YYYY.-.#####",
+ "creation": "2015-08-07 02:33:52.670866",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "tax_type",
+ "use_for_shopping_cart",
+ "column_break_1",
+ "sales_tax_template",
+ "purchase_tax_template",
+ "filters",
+ "customer",
+ "supplier",
+ "item",
+ "billing_city",
+ "billing_county",
+ "billing_state",
+ "billing_zipcode",
+ "billing_country",
+ "tax_category",
+ "column_break_2",
+ "customer_group",
+ "supplier_group",
+ "item_group",
+ "shipping_city",
+ "shipping_county",
+ "shipping_state",
+ "shipping_zipcode",
+ "shipping_country",
+ "section_break_4",
+ "from_date",
+ "column_break_7",
+ "to_date",
+ "section_break_6",
+ "priority",
+ "column_break_20",
+ "company"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Sales",
- "fieldname": "tax_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": 1,
- "label": "Tax Type",
- "length": 0,
- "no_copy": 0,
- "options": "Sales\nPurchase",
- "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
- },
+ "default": "Sales",
+ "fieldname": "tax_type",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Tax Type",
+ "options": "Sales\nPurchase"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "use_for_shopping_cart",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Use for Shopping Cart",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "1",
+ "fieldname": "use_for_shopping_cart",
+ "fieldtype": "Check",
+ "label": "Use for Shopping Cart"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_1",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_1",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.tax_type==\"Sales\"",
- "fieldname": "sales_tax_template",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Sales Tax Template",
- "length": 0,
- "no_copy": 0,
- "options": "Sales Taxes and Charges Template",
- "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
- },
+ "depends_on": "eval:doc.tax_type==\"Sales\"",
+ "fieldname": "sales_tax_template",
+ "fieldtype": "Link",
+ "label": "Sales Tax Template",
+ "options": "Sales Taxes and Charges Template"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.tax_type==\"Purchase\"",
- "fieldname": "purchase_tax_template",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Purchase Tax Template",
- "length": 0,
- "no_copy": 0,
- "options": "Purchase Taxes and Charges Template",
- "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
- },
+ "depends_on": "eval:doc.tax_type==\"Purchase\"",
+ "fieldname": "purchase_tax_template",
+ "fieldtype": "Link",
+ "label": "Purchase Tax Template",
+ "options": "Purchase Taxes and Charges Template"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "filters",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Filters",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "filters",
+ "fieldtype": "Section Break",
+ "label": "Filters"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.tax_type==\"Sales\"",
- "fieldname": "customer",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Customer",
- "length": 0,
- "no_copy": 0,
- "options": "Customer",
- "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
- },
+ "depends_on": "eval:doc.tax_type==\"Sales\"",
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "label": "Customer",
+ "options": "Customer"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.tax_type==\"Purchase\"",
- "fieldname": "supplier",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Supplier",
- "length": 0,
- "no_copy": 0,
- "options": "Supplier",
- "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
- },
+ "depends_on": "eval:doc.tax_type==\"Purchase\"",
+ "fieldname": "supplier",
+ "fieldtype": "Link",
+ "label": "Supplier",
+ "options": "Supplier"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "item",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Item",
- "length": 0,
- "no_copy": 0,
- "options": "Item",
- "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": "item",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "billing_city",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Billing City",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "billing_city",
+ "fieldtype": "Data",
+ "label": "Billing City"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "billing_county",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Billing County",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "billing_county",
+ "fieldtype": "Data",
+ "label": "Billing County"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "billing_state",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Billing State",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "billing_state",
+ "fieldtype": "Data",
+ "label": "Billing State"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "billing_zipcode",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Billing Zipcode",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "billing_zipcode",
+ "fieldtype": "Data",
+ "label": "Billing Zipcode"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "billing_country",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Billing Country",
- "length": 0,
- "no_copy": 0,
- "options": "Country",
- "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": "billing_country",
+ "fieldtype": "Link",
+ "label": "Billing Country",
+ "options": "Country"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "tax_category",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Tax Category",
- "length": 0,
- "no_copy": 0,
- "options": "Tax Category",
- "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": "tax_category",
+ "fieldtype": "Link",
+ "label": "Tax Category",
+ "options": "Tax Category"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.tax_type==\"Sales\"",
- "fieldname": "customer_group",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Customer Group",
- "length": 0,
- "no_copy": 0,
- "options": "Customer Group",
- "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
- },
+ "depends_on": "eval:doc.tax_type==\"Sales\"",
+ "fetch_from": "customer.customer_group",
+ "fieldname": "customer_group",
+ "fieldtype": "Link",
+ "label": "Customer Group",
+ "options": "Customer Group"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.tax_type==\"Purchase\"",
- "fieldname": "supplier_group",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Supplier Group",
- "length": 0,
- "no_copy": 0,
- "options": "Supplier Group",
- "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
- },
+ "depends_on": "eval:doc.tax_type==\"Purchase\"",
+ "fetch_from": "supplier.supplier_group",
+ "fieldname": "supplier_group",
+ "fieldtype": "Link",
+ "label": "Supplier Group",
+ "options": "Supplier Group"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "item_group",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Item Group",
- "length": 0,
- "no_copy": 0,
- "options": "Item Group",
- "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": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "shipping_city",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Shipping City",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "shipping_city",
+ "fieldtype": "Data",
+ "label": "Shipping City"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "shipping_county",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Shipping County",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "shipping_county",
+ "fieldtype": "Data",
+ "label": "Shipping County"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "shipping_state",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Shipping State",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "shipping_state",
+ "fieldtype": "Data",
+ "label": "Shipping State"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "shipping_zipcode",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Shipping Zipcode",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "shipping_zipcode",
+ "fieldtype": "Data",
+ "label": "Shipping Zipcode"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "shipping_country",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Shipping Country",
- "length": 0,
- "no_copy": 0,
- "options": "Country",
- "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": "shipping_country",
+ "fieldtype": "Link",
+ "label": "Shipping Country",
+ "options": "Country"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_4",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Validity",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break",
+ "label": "Validity"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "from_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "From Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "from_date",
+ "fieldtype": "Date",
+ "label": "From Date"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_7",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "to_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "To Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "to_date",
+ "fieldtype": "Date",
+ "label": "To Date"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_6",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "1",
- "fieldname": "priority",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Priority",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "1",
+ "fieldname": "priority",
+ "fieldtype": "Int",
+ "label": "Priority"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_20",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_20",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "company",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Company",
- "length": 0,
- "no_copy": 0,
- "options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "remember_last_selected_value": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-12-27 01:22:17.721636",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Tax Rule",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "links": [],
+ "modified": "2021-06-04 23:14:27.186879",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Tax Rule",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py
index ac1ffd9..cf72268 100644
--- a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py
+++ b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py
@@ -50,7 +50,7 @@
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":0}),
+ 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):
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index e025fc6..b97dc40 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -542,6 +542,7 @@
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)))
for d in companies:
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index a11b77a..b54646f 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -99,7 +99,6 @@
voucher_no = gle.voucher_no,
party = gle.party,
posting_date = gle.posting_date,
- remarks = gle.remarks,
account_currency = gle.account_currency,
invoiced = 0.0,
paid = 0.0,
@@ -579,7 +578,7 @@
self.gl_entries = frappe.db.sql("""
select
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
- against_voucher_type, against_voucher, account_currency, remarks, {0}
+ against_voucher_type, against_voucher, account_currency, {0}
from
`tabGL Entry`
where
@@ -792,8 +791,6 @@
self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link',
options='Supplier Group')
- self.add_column(label=_('Remarks'), fieldname='remarks', fieldtype='Text', width=200)
-
def add_column(self, label, fieldname=None, fieldtype='Currency', options=None, width=120):
if not fieldname: fieldname = scrub(label)
if fieldtype=='Currency': options='currency'
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
index 9c9ada8..f1b231b 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
@@ -397,6 +397,7 @@
{'name': 'Budget', 'chartType': 'bar', 'values': budget_values},
{'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values}
]
- }
+ },
+ 'type' : 'bar'
}
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index 7793af7..56a67bb 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -380,7 +380,7 @@
gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company,
gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
acc.account_name, acc.account_number
- from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s
+ from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0
{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions),
{
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 744ada9..1759fa3 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -48,17 +48,18 @@
if not filters.get("from_date") and not filters.get("to_date"):
frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))))
-
- for account in filters.account:
- if not account_details.get(account):
- frappe.throw(_("Account {0} does not exists").format(account))
if filters.get('account'):
filters.account = frappe.parse_json(filters.get('account'))
+ for account in filters.account:
+ if not account_details.get(account):
+ frappe.throw(_("Account {0} does not exists").format(account))
- if (filters.get("account") and filters.get("group_by") == _('Group by Account')
- and account_details[filters.account].is_group == 0):
- frappe.throw(_("Can not filter based on Account, if grouped by Account"))
+ if (filters.get("account") and filters.get("group_by") == _('Group by Account')):
+ filters.account = frappe.parse_json(filters.get('account'))
+ for account in filters.account:
+ if account_details[account].is_group == 0:
+ frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
if (filters.get("voucher_no")
and filters.get("group_by") in [_('Group by Voucher')]):
diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
index e15715d..6b9df41 100644
--- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
+++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
@@ -75,7 +75,8 @@
select voucher_no, credit
from `tabGL Entry`
where party in (%s) and credit > 0
- and company=%s and posting_date between %s and %s
+ and company=%s and is_cancelled = 0
+ and posting_date between %s and %s
""", (supplier, company, from_date, to_date), as_dict=1)
supplier_credit_amount = flt(sum(d.credit for d in entries))
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 66a9b60..1cdbd8d 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -472,7 +472,8 @@
"total_amount": d.grand_total,
"outstanding_amount": d.outstanding_amount,
"allocated_amount": d.allocated_amount,
- "exchange_rate": d.exchange_rate
+ "exchange_rate": d.exchange_rate if not d.exchange_gain_loss else payment_entry.get_exchange_rate(),
+ "exchange_gain_loss": d.exchange_gain_loss # only populated from invoice in case of advance allocation
}
if d.voucher_detail_no:
@@ -498,12 +499,15 @@
payment_entry.set_amounts()
if d.difference_amount and d.difference_account:
- payment_entry.set_gain_or_loss(account_details={
+ account_details = {
'account': d.difference_account,
'cost_center': payment_entry.cost_center or frappe.get_cached_value('Company',
- payment_entry.company, "cost_center"),
- 'amount': d.difference_amount
- })
+ payment_entry.company, "cost_center")
+ }
+ if d.difference_amount:
+ account_details['amount'] = d.difference_amount
+
+ payment_entry.set_gain_or_loss(account_details=account_details)
if not do_not_save:
payment_entry.save(ignore_permissions=True)
@@ -784,7 +788,7 @@
return acc
def create_payment_gateway_account(gateway, payment_channel="Email"):
- from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account
+ from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
company = frappe.db.get_value("Global Defaults", None, "default_company")
if not company:
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 6f1bb28..922cc4a 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -82,24 +82,46 @@
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
frm.add_custom_button("Transfer Asset", function() {
erpnext.asset.transfer_asset(frm);
- });
+ }, __("Manage"));
frm.add_custom_button("Scrap Asset", function() {
erpnext.asset.scrap_asset(frm);
- });
+ }, __("Manage"));
frm.add_custom_button("Sell Asset", function() {
frm.trigger("make_sales_invoice");
- });
+ }, __("Manage"));
} else if (frm.doc.status=='Scrapped') {
frm.add_custom_button("Restore Asset", function() {
erpnext.asset.restore_asset(frm);
- });
+ }, __("Manage"));
+ }
+
+ if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
+ frm.add_custom_button(__("Maintain Asset"), function() {
+ frm.trigger("create_asset_maintenance");
+ }, __("Manage"));
+ }
+
+ frm.add_custom_button(__("Repair Asset"), function() {
+ frm.trigger("create_asset_repair");
+ }, __("Manage"));
+
+ if (frm.doc.status != 'Fully Depreciated') {
+ frm.add_custom_button(__("Adjust Asset Value"), function() {
+ frm.trigger("create_asset_adjustment");
+ }, __("Manage"));
+ }
+
+ if (!frm.doc.calculate_depreciation) {
+ frm.add_custom_button(__("Create Depreciation Entry"), function() {
+ frm.trigger("make_journal_entry");
+ }, __("Manage"));
}
if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) {
- frm.add_custom_button("General Ledger", function() {
+ frm.add_custom_button("View General Ledger", function() {
frappe.route_options = {
"voucher_no": frm.doc.name,
"from_date": frm.doc.available_for_use_date,
@@ -107,27 +129,9 @@
"company": frm.doc.company
};
frappe.set_route("query-report", "General Ledger");
- });
+ }, __("Manage"));
}
- if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
- frm.add_custom_button(__("Asset Maintenance"), function() {
- frm.trigger("create_asset_maintenance");
- }, __('Create'));
- }
- if (frm.doc.status != 'Fully Depreciated') {
- frm.add_custom_button(__("Asset Value Adjustment"), function() {
- frm.trigger("create_asset_adjustment");
- }, __('Create'));
- }
-
- if (!frm.doc.calculate_depreciation) {
- frm.add_custom_button(__("Depreciation Entry"), function() {
- frm.trigger("make_journal_entry");
- }, __('Create'));
- }
-
- frm.page.set_inner_btn_group_as_primary(__('Create'));
frm.trigger("setup_chart");
}
@@ -304,6 +308,20 @@
})
},
+ create_asset_repair: function(frm) {
+ frappe.call({
+ args: {
+ "asset": frm.doc.name,
+ "asset_name": frm.doc.asset_name
+ },
+ method: "erpnext.assets.doctype.asset.asset.create_asset_repair",
+ callback: function(r) {
+ var doclist = frappe.model.sync(r.message);
+ frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
+ }
+ });
+ },
+
create_asset_adjustment: function(frm) {
frappe.call({
args: {
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index 421b9a6..de06075 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -502,7 +502,7 @@
"link_fieldname": "asset"
}
],
- "modified": "2021-01-22 12:38:59.091510",
+ "modified": "2021-06-24 14:58:51.097908",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 8799275..66f0bdc 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -168,17 +168,24 @@
d.precision("rate_of_depreciation"))
def make_depreciation_schedule(self):
- if 'Manual' not in [d.depreciation_method for d in self.finance_books]:
+ if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules:
self.schedules = []
- if self.get("schedules") or not self.available_for_use_date:
+ if not self.available_for_use_date:
return
for d in self.get('finance_books'):
self.validate_asset_finance_books(d)
+
+ start = self.clear_depreciation_schedule()
- value_after_depreciation = (flt(self.gross_purchase_amount) -
- flt(self.opening_accumulated_depreciation))
+ # value_after_depreciation - current Asset value
+ if d.value_after_depreciation:
+ value_after_depreciation = (flt(d.value_after_depreciation) -
+ flt(self.opening_accumulated_depreciation))
+ else:
+ value_after_depreciation = (flt(self.gross_purchase_amount) -
+ flt(self.opening_accumulated_depreciation))
d.value_after_depreciation = value_after_depreciation
@@ -191,7 +198,7 @@
number_of_pending_depreciations += 1
skip_row = False
- for n in range(number_of_pending_depreciations):
+ for n in range(start, number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row: continue
@@ -216,11 +223,13 @@
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
- to_date = add_months(self.available_for_use_date,
- n * cint(d.frequency_of_depreciation))
+ if not self.flags.increase_in_asset_life:
+ # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
+ self.to_date = add_months(self.available_for_use_date,
+ n * cint(d.frequency_of_depreciation))
depreciation_amount, days, months = self.get_pro_rata_amt(d,
- depreciation_amount, schedule_date, to_date)
+ depreciation_amount, schedule_date, self.to_date)
monthly_schedule_date = add_months(schedule_date, 1)
@@ -284,10 +293,23 @@
"finance_book_id": d.idx
})
+ # used when depreciation schedule needs to be modified due to increase in asset life
+ def clear_depreciation_schedule(self):
+ start = 0
+ for n in range(len(self.schedules)):
+ if not self.schedules[n].journal_entry:
+ del self.schedules[n:]
+ start = n
+ break
+ return start
+
+
+ # if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row):
has_pro_rata = False
-
days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1
+
+ # if frequency_of_depreciation is 12 months, total_days = 365
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
if days < total_days:
@@ -346,11 +368,12 @@
if d.finance_book_id not in finance_books:
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id))
- finance_books.append(d.finance_book_id)
+ finance_books.append(int(d.finance_book_id))
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
value_after_depreciation -= flt(depreciation_amount)
+ # for the last row, if depreciation method = Straight Line
if straight_line_idx and i == max(straight_line_idx) - 1:
book = self.get('finance_books')[cint(d.finance_book_id) - 1]
depreciation_amount += flt(value_after_depreciation -
@@ -626,8 +649,17 @@
return asset_maintenance
@frappe.whitelist()
+def create_asset_repair(asset, asset_name):
+ asset_repair = frappe.new_doc("Asset Repair")
+ asset_repair.update({
+ "asset": asset,
+ "asset_name": asset_name
+ })
+ return asset_repair
+
+@frappe.whitelist()
def create_asset_adjustment(asset, asset_category, company):
- asset_maintenance = frappe.new_doc("Asset Value Adjustment")
+ asset_maintenance = frappe.get_doc("Asset Value Adjustment")
asset_maintenance.update({
"asset": asset,
"company": company,
@@ -757,8 +789,15 @@
depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
if row.depreciation_method in ("Straight Line", "Manual"):
- depreciation_amount = (flt(row.value_after_depreciation) -
- flt(row.expected_value_after_useful_life)) / depreciation_left
+ # if the Depreciation Schedule is being prepared for the first time
+ if not asset.flags.increase_in_asset_life:
+ depreciation_amount = (flt(row.value_after_depreciation) -
+ flt(row.expected_value_after_useful_life)) / depreciation_left
+
+ # if the Depreciation Schedule is being modified after Asset Repair
+ else:
+ depreciation_amount = (flt(row.value_after_depreciation) -
+ flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
else:
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index 8f0afb4..8fdbbf9 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -176,22 +176,34 @@
asset.set_status()
-@frappe.whitelist()
+def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None):
+ fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation = \
+ get_asset_details(asset, finance_book)
+
+ gl_entries = [
+ {
+ "account": fixed_asset_account,
+ "debit_in_account_currency": asset.gross_purchase_amount,
+ "debit": asset.gross_purchase_amount,
+ "cost_center": depreciation_cost_center
+ },
+ {
+ "account": accumulated_depr_account,
+ "credit_in_account_currency": accumulated_depr_amount,
+ "credit": accumulated_depr_amount,
+ "cost_center": depreciation_cost_center
+ }
+ ]
+
+ profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount))
+ if profit_amount:
+ get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center)
+
+ return gl_entries
+
def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None):
- fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(asset)
- disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
- depreciation_cost_center = asset.cost_center or depreciation_cost_center
-
- idx = 1
- if finance_book:
- for d in asset.finance_books:
- if d.finance_book == finance_book:
- idx = d.idx
- break
-
- value_after_depreciation = (asset.finance_books[idx - 1].value_after_depreciation
- if asset.calculate_depreciation else asset.value_after_depreciation)
- accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
+ fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation = \
+ get_asset_details(asset, finance_book)
gl_entries = [
{
@@ -210,16 +222,37 @@
profit_amount = flt(selling_amount) - flt(value_after_depreciation)
if profit_amount:
- debit_or_credit = "debit" if profit_amount < 0 else "credit"
- gl_entries.append({
- "account": disposal_account,
- "cost_center": depreciation_cost_center,
- debit_or_credit: abs(profit_amount),
- debit_or_credit + "_in_account_currency": abs(profit_amount)
- })
+ get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center)
return gl_entries
+def get_asset_details(asset, finance_book=None):
+ fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(asset)
+ disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
+ depreciation_cost_center = asset.cost_center or depreciation_cost_center
+
+ idx = 1
+ if finance_book:
+ for d in asset.finance_books:
+ if d.finance_book == finance_book:
+ idx = d.idx
+ break
+
+ value_after_depreciation = (asset.finance_books[idx - 1].value_after_depreciation
+ if asset.calculate_depreciation else asset.value_after_depreciation)
+ accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
+
+ return fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation
+
+def get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center):
+ debit_or_credit = "debit" if profit_amount < 0 else "credit"
+ gl_entries.append({
+ "account": disposal_account,
+ "cost_center": depreciation_cost_center,
+ debit_or_credit: abs(profit_amount),
+ debit_or_credit + "_in_account_currency": abs(profit_amount)
+ })
+
@frappe.whitelist()
def get_disposal_account_and_cost_center(company):
disposal_account, depreciation_cost_center = frappe.get_cached_value('Company', company,
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 8845f24..59fbe3b 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -125,7 +125,6 @@
"frequency_of_depreciation": 12,
"depreciation_start_date": "2030-12-31"
})
- asset.insert()
self.assertEqual(asset.status, "Draft")
asset.save()
expected_schedules = [
@@ -154,9 +153,8 @@
"frequency_of_depreciation": 12,
"depreciation_start_date": '2030-12-31'
})
- asset.insert()
- self.assertEqual(asset.status, "Draft")
asset.save()
+ self.assertEqual(asset.status, "Draft")
expected_schedules = [
['2030-12-31', 66667.00, 66667.00],
@@ -185,7 +183,7 @@
"frequency_of_depreciation": 12,
"depreciation_start_date": "2030-12-31"
})
- asset.insert()
+ asset.save()
self.assertEqual(asset.status, "Draft")
expected_schedules = [
@@ -216,7 +214,6 @@
"depreciation_start_date": "2030-12-31"
})
- asset.insert()
asset.save()
expected_schedules = [
@@ -247,7 +244,6 @@
"frequency_of_depreciation": 10,
"depreciation_start_date": "2020-12-31"
})
- asset.insert()
asset.submit()
asset.load_from_db()
self.assertEqual(asset.status, "Submitted")
@@ -350,7 +346,6 @@
"frequency_of_depreciation": 10,
"depreciation_start_date": "2020-12-31"
})
- asset.insert()
asset.submit()
post_depreciation_entries(date="2021-01-01")
@@ -380,7 +375,6 @@
"total_number_of_depreciations": 10,
"frequency_of_depreciation": 1
})
- asset.insert()
asset.submit()
post_depreciation_entries(date=add_months('2020-01-01', 4))
@@ -424,7 +418,6 @@
"frequency_of_depreciation": 10,
"depreciation_start_date": "2020-12-31"
})
- asset.insert()
asset.submit()
post_depreciation_entries(date="2021-01-01")
@@ -468,7 +461,7 @@
"total_number_of_depreciations": 3,
"frequency_of_depreciation": 10
})
- asset.insert()
+ asset.save()
accumulated_depreciation_after_full_schedule = \
max(d.accumulated_depreciation_amount for d in asset.get("schedules"))
@@ -699,7 +692,7 @@
"item_code": args.item_code or "Macbook Pro",
"company": args.company or"_Test Company",
"purchase_date": "2015-01-01",
- "calculate_depreciation": 0,
+ "calculate_depreciation": args.calculate_depreciation or 0,
"gross_purchase_amount": 100000,
"purchase_receipt_amount": 100000,
"expected_value_after_useful_life": 10000,
@@ -707,9 +700,16 @@
"available_for_use_date": "2020-06-06",
"location": "Test Location",
"asset_owner": "Company",
- "is_existing_asset": args.is_existing_asset or 0
+ "is_existing_asset": 1
})
+ if asset.calculate_depreciation:
+ asset.append("finance_books", {
+ "depreciation_method": "Straight Line",
+ "frequency_of_depreciation": 12,
+ "total_number_of_depreciations": 5
+ })
+
try:
asset.save()
except frappe.DuplicateEntryError:
diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
index d9b7b69..e5a5f19 100644
--- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
+++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
@@ -67,7 +67,6 @@
{
"fieldname": "value_after_depreciation",
"fieldtype": "Currency",
- "hidden": 1,
"label": "Value After Depreciation",
"no_copy": 1,
"options": "Company:company:default_currency",
@@ -85,7 +84,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-05 16:30:09.213479",
+ "modified": "2021-06-17 12:59:05.743683",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index 4ba2b44..1cebfff 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -2,6 +2,45 @@
// For license information, please see license.txt
frappe.ui.form.on('Asset Repair', {
+ setup: function(frm) {
+ frm.fields_dict.cost_center.get_query = function(doc) {
+ return {
+ filters: {
+ 'is_group': 0,
+ 'company': doc.company
+ }
+ };
+ };
+
+ frm.fields_dict.project.get_query = function(doc) {
+ return {
+ filters: {
+ 'company': doc.company
+ }
+ };
+ };
+
+ frm.fields_dict.warehouse.get_query = function(doc) {
+ return {
+ filters: {
+ 'is_group': 0,
+ 'company': doc.company
+ }
+ };
+ };
+ },
+
+ refresh: function(frm) {
+ if (frm.doc.docstatus) {
+ frm.add_custom_button("View General Ledger", function() {
+ frappe.route_options = {
+ "voucher_no": frm.doc.name
+ };
+ frappe.set_route("query-report", "General Ledger");
+ });
+ }
+ },
+
repair_status: (frm) => {
if (frm.doc.completion_date && frm.doc.repair_status == "Completed") {
frappe.call ({
@@ -17,5 +56,16 @@
}
});
}
+
+ if (frm.doc.repair_status == "Completed") {
+ frm.set_value('completion_date', frappe.datetime.now_datetime());
+ }
}
});
+
+frappe.ui.form.on('Asset Repair Consumed Item', {
+ consumed_quantity: function(frm, cdt, cdn) {
+ var row = locals[cdt][cdn];
+ frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate);
+ },
+});
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json
index d338fc0..ba31898 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.json
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.json
@@ -7,39 +7,44 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "naming_series",
- "asset_name",
+ "asset",
+ "company",
"column_break_2",
- "item_code",
- "item_name",
+ "asset_name",
+ "naming_series",
"section_break_5",
"failure_date",
- "assign_to",
- "assign_to_name",
+ "repair_status",
"column_break_6",
"completion_date",
- "repair_status",
+ "accounting_dimensions_section",
+ "cost_center",
+ "column_break_14",
+ "project",
+ "accounting_details",
"repair_cost",
+ "capitalize_repair_cost",
+ "stock_consumption",
+ "column_break_8",
+ "purchase_invoice",
+ "stock_consumption_details_section",
+ "warehouse",
+ "stock_items",
+ "total_repair_cost",
+ "stock_entry",
+ "asset_depreciation_details_section",
+ "increase_in_asset_life",
"section_break_9",
"description",
"column_break_9",
"actions_performed",
- "section_break_17",
+ "section_break_23",
"downtime",
"column_break_19",
"amended_from"
],
"fields": [
{
- "columns": 1,
- "fieldname": "asset_name",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Asset",
- "options": "Asset",
- "reqd": 1
- },
- {
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
@@ -51,18 +56,6 @@
"fieldtype": "Column Break"
},
{
- "fetch_from": "asset_name.item_code",
- "fieldname": "item_code",
- "fieldtype": "Read Only",
- "label": "Item Code"
- },
- {
- "fetch_from": "asset_name.item_name",
- "fieldname": "item_name",
- "fieldtype": "Read Only",
- "label": "Item Name"
- },
- {
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"label": "Repair Details"
@@ -75,32 +68,19 @@
"reqd": 1
},
{
- "allow_on_submit": 1,
- "fieldname": "assign_to",
- "fieldtype": "Link",
- "label": "Assign To",
- "options": "User"
- },
- {
- "allow_on_submit": 1,
- "fetch_from": "assign_to.full_name",
- "fieldname": "assign_to_name",
- "fieldtype": "Read Only",
- "label": "Assign To Name"
- },
- {
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
- "allow_on_submit": 1,
+ "depends_on": "eval:!doc.__islocal",
"fieldname": "completion_date",
"fieldtype": "Datetime",
- "label": "Completion Date"
+ "label": "Completion Date",
+ "no_copy": 1
},
{
- "allow_on_submit": 1,
"default": "Pending",
+ "depends_on": "eval:!doc.__islocal",
"fieldname": "repair_status",
"fieldtype": "Select",
"label": "Repair Status",
@@ -116,25 +96,18 @@
{
"fieldname": "description",
"fieldtype": "Long Text",
- "label": "Error Description",
- "reqd": 1
+ "label": "Error Description"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
- "allow_on_submit": 1,
"fieldname": "actions_performed",
"fieldtype": "Long Text",
"label": "Actions performed"
},
{
- "fieldname": "section_break_17",
- "fieldtype": "Section Break"
- },
- {
- "allow_on_submit": 1,
"fieldname": "downtime",
"fieldtype": "Data",
"in_list_view": 1,
@@ -146,7 +119,7 @@
"fieldtype": "Column Break"
},
{
- "allow_on_submit": 1,
+ "default": "0",
"fieldname": "repair_cost",
"fieldtype": "Currency",
"label": "Repair Cost"
@@ -159,12 +132,139 @@
"options": "Asset Repair",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "columns": 1,
+ "fieldname": "asset",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Asset",
+ "options": "Asset",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "asset.asset_name",
+ "fieldname": "asset_name",
+ "fieldtype": "Read Only",
+ "label": "Asset Name"
+ },
+ {
+ "fieldname": "column_break_8",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "capitalize_repair_cost",
+ "fieldtype": "Check",
+ "label": "Capitalize Repair Cost"
+ },
+ {
+ "fieldname": "accounting_details",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fieldname": "stock_items",
+ "fieldtype": "Table",
+ "label": "Stock Items",
+ "mandatory_depends_on": "stock_consumption",
+ "options": "Asset Repair Consumed Item"
+ },
+ {
+ "fieldname": "section_break_23",
+ "fieldtype": "Section Break"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "accounting_dimensions_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Dimensions"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
+ },
+ {
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "label": "Project",
+ "options": "Project"
+ },
+ {
+ "fieldname": "column_break_14",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "stock_consumption",
+ "fieldtype": "Check",
+ "label": "Stock Consumed During Repair"
+ },
+ {
+ "depends_on": "stock_consumption",
+ "fieldname": "stock_consumption_details_section",
+ "fieldtype": "Section Break",
+ "label": "Stock Consumption Details"
+ },
+ {
+ "depends_on": "eval: doc.stock_consumption && doc.total_repair_cost > 0",
+ "description": "Sum of Repair Cost and Value of Consumed Stock Items.",
+ "fieldname": "total_repair_cost",
+ "fieldtype": "Currency",
+ "label": "Total Repair Cost",
+ "read_only": 1
+ },
+ {
+ "depends_on": "stock_consumption",
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "depends_on": "capitalize_repair_cost",
+ "fieldname": "asset_depreciation_details_section",
+ "fieldtype": "Section Break",
+ "label": "Asset Depreciation Details"
+ },
+ {
+ "fieldname": "increase_in_asset_life",
+ "fieldtype": "Int",
+ "label": "Increase In Asset Life(Months)",
+ "no_copy": 1
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "purchase_invoice",
+ "fieldtype": "Link",
+ "label": "Purchase Invoice",
+ "mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0",
+ "no_copy": 1,
+ "options": "Purchase Invoice"
+ },
+ {
+ "fetch_from": "asset.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "fieldname": "stock_entry",
+ "fieldtype": "Link",
+ "label": "Stock Entry",
+ "options": "Stock Entry",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-01-22 15:08:12.495850",
+ "modified": "2021-06-25 13:14:38.307723",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",
@@ -203,6 +303,7 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "title_field": "asset_name",
"track_changes": 1,
"track_seen": 1
}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index 049b931..d32fdf7 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -5,16 +5,252 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import time_diff_in_hours
-from frappe.model.document import Document
+from frappe.utils import time_diff_in_hours, getdate, add_months, flt, cint
+from erpnext.accounts.general_ledger import make_gl_entries
+from erpnext.assets.doctype.asset.asset import get_asset_account
+from erpnext.controllers.accounts_controller import AccountsController
-class AssetRepair(Document):
+class AssetRepair(AccountsController):
def validate(self):
- if self.repair_status == "Completed" and not self.completion_date:
- frappe.throw(_("Please select Completion Date for Completed Repair"))
+ self.asset_doc = frappe.get_doc('Asset', self.asset)
+ self.update_status()
+ if self.get('stock_items'):
+ self.set_total_value()
+ self.calculate_total_repair_cost()
+
+ def update_status(self):
+ if self.repair_status == 'Pending':
+ frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order')
+ else:
+ self.asset_doc.set_status()
+
+ def set_total_value(self):
+ for item in self.get('stock_items'):
+ item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity)
+
+ def calculate_total_repair_cost(self):
+ self.total_repair_cost = flt(self.repair_cost)
+
+ total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
+ self.total_repair_cost += total_value_of_stock_consumed
+
+ def before_submit(self):
+ self.check_repair_status()
+
+ if self.get('stock_consumption') or self.get('capitalize_repair_cost'):
+ self.increase_asset_value()
+ if self.get('stock_consumption'):
+ self.check_for_stock_items_and_warehouse()
+ self.decrease_stock_quantity()
+ if self.get('capitalize_repair_cost'):
+ self.make_gl_entries()
+ if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life:
+ self.modify_depreciation_schedule()
+
+ self.asset_doc.flags.ignore_validate_update_after_submit = True
+ self.asset_doc.prepare_depreciation_data()
+ self.asset_doc.save()
+
+ def before_cancel(self):
+ self.asset_doc = frappe.get_doc('Asset', self.asset)
+
+ if self.get('stock_consumption') or self.get('capitalize_repair_cost'):
+ self.decrease_asset_value()
+ if self.get('stock_consumption'):
+ self.increase_stock_quantity()
+ if self.get('capitalize_repair_cost'):
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.make_gl_entries(cancel=True)
+ if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life:
+ self.revert_depreciation_schedule_on_cancellation()
+
+ self.asset_doc.flags.ignore_validate_update_after_submit = True
+ self.asset_doc.prepare_depreciation_data()
+ self.asset_doc.save()
+
+ def check_repair_status(self):
+ if self.repair_status == "Pending":
+ frappe.throw(_("Please update Repair Status."))
+
+ def check_for_stock_items_and_warehouse(self):
+ if not self.get('stock_items'):
+ frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items"))
+ if not self.warehouse:
+ frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse"))
+
+ def increase_asset_value(self):
+ total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
+
+ if self.asset_doc.calculate_depreciation:
+ for row in self.asset_doc.finance_books:
+ row.value_after_depreciation += total_value_of_stock_consumed
+
+ if self.capitalize_repair_cost:
+ row.value_after_depreciation += self.repair_cost
+
+ def decrease_asset_value(self):
+ total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
+
+ if self.asset_doc.calculate_depreciation:
+ for row in self.asset_doc.finance_books:
+ row.value_after_depreciation -= total_value_of_stock_consumed
+
+ if self.capitalize_repair_cost:
+ row.value_after_depreciation -= self.repair_cost
+
+ def get_total_value_of_stock_consumed(self):
+ total_value_of_stock_consumed = 0
+ if self.get('stock_consumption'):
+ for item in self.get('stock_items'):
+ total_value_of_stock_consumed += item.total_value
+
+ return total_value_of_stock_consumed
+
+ def decrease_stock_quantity(self):
+ stock_entry = frappe.get_doc({
+ "doctype": "Stock Entry",
+ "stock_entry_type": "Material Issue",
+ "company": self.company
+ })
+
+ for stock_item in self.get('stock_items'):
+ stock_entry.append('items', {
+ "s_warehouse": self.warehouse,
+ "item_code": stock_item.item,
+ "qty": stock_item.consumed_quantity,
+ "basic_rate": stock_item.valuation_rate
+ })
+
+ stock_entry.insert()
+ stock_entry.submit()
+
+ self.db_set('stock_entry', stock_entry.name)
+
+ def increase_stock_quantity(self):
+ stock_entry = frappe.get_doc('Stock Entry', self.stock_entry)
+ stock_entry.flags.ignore_links = True
+ stock_entry.cancel()
+
+ def make_gl_entries(self, cancel=False):
+ if flt(self.repair_cost) > 0:
+ gl_entries = self.get_gl_entries()
+ make_gl_entries(gl_entries, cancel)
+
+ def get_gl_entries(self):
+ gl_entries = []
+ repair_and_maintenance_account = frappe.db.get_value('Company', self.company, 'repair_and_maintenance_account')
+ fixed_asset_account = get_asset_account("fixed_asset_account", asset=self.asset, company=self.company)
+ expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": expense_account,
+ "credit": self.repair_cost,
+ "credit_in_account_currency": self.repair_cost,
+ "against": repair_and_maintenance_account,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(),
+ "company": self.company
+ }, item=self)
+ )
+
+ if self.get('stock_consumption'):
+ # creating GL Entries for each row in Stock Items based on the Stock Entry created for it
+ stock_entry = frappe.get_doc('Stock Entry', self.stock_entry)
+ for item in stock_entry.items:
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": item.expense_account,
+ "credit": item.amount,
+ "credit_in_account_currency": item.amount,
+ "against": repair_and_maintenance_account,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(),
+ "company": self.company
+ }, item=self)
+ )
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": fixed_asset_account,
+ "debit": self.total_repair_cost,
+ "debit_in_account_currency": self.total_repair_cost,
+ "against": expense_account,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(),
+ "against_voucher_type": "Purchase Invoice",
+ "against_voucher": self.purchase_invoice,
+ "company": self.company
+ }, item=self)
+ )
+
+ return gl_entries
+
+ def modify_depreciation_schedule(self):
+ for row in self.asset_doc.finance_books:
+ row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation
+
+ self.asset_doc.flags.increase_in_asset_life = False
+ extra_months = self.increase_in_asset_life % row.frequency_of_depreciation
+ if extra_months != 0:
+ self.calculate_last_schedule_date(self.asset_doc, row, extra_months)
+
+ # to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation
+ def calculate_last_schedule_date(self, asset, row, extra_months):
+ asset.flags.increase_in_asset_life = True
+ number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \
+ cint(asset.number_of_depreciations_booked)
+
+ # the Schedule Date in the final row of the old Depreciation Schedule
+ last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date
+
+ # the Schedule Date in the final row of the new Depreciation Schedule
+ asset.to_date = add_months(last_schedule_date, extra_months)
+
+ # the latest possible date at which the depreciation can occur, without increasing the Total Number of Depreciations
+ # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022...
+ schedule_date = add_months(row.depreciation_start_date,
+ number_of_pending_depreciations * cint(row.frequency_of_depreciation))
+
+ if asset.to_date > schedule_date:
+ row.total_number_of_depreciations += 1
+
+ def revert_depreciation_schedule_on_cancellation(self):
+ for row in self.asset_doc.finance_books:
+ row.total_number_of_depreciations -= self.increase_in_asset_life/row.frequency_of_depreciation
+
+ self.asset_doc.flags.increase_in_asset_life = False
+ extra_months = self.increase_in_asset_life % row.frequency_of_depreciation
+ if extra_months != 0:
+ self.calculate_last_schedule_date_before_modification(self.asset_doc, row, extra_months)
+
+ def calculate_last_schedule_date_before_modification(self, asset, row, extra_months):
+ asset.flags.increase_in_asset_life = True
+ number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \
+ cint(asset.number_of_depreciations_booked)
+
+ # the Schedule Date in the final row of the modified Depreciation Schedule
+ last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date
+
+ # the Schedule Date in the final row of the original Depreciation Schedule
+ asset.to_date = add_months(last_schedule_date, -extra_months)
+
+ # the latest possible date at which the depreciation can occur, without decreasing the Total Number of Depreciations
+ # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022...
+ schedule_date = add_months(row.depreciation_start_date,
+ (number_of_pending_depreciations - 1) * cint(row.frequency_of_depreciation))
+
+ if asset.to_date < schedule_date:
+ row.total_number_of_depreciations -= 1
@frappe.whitelist()
def get_downtime(failure_date, completion_date):
downtime = time_diff_in_hours(completion_date, failure_date)
- return round(downtime, 2)
\ No newline at end of file
+ return round(downtime, 2)
diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
index 3d325a9..30bbb37 100644
--- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
@@ -2,8 +2,167 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
-
+import frappe
+from frappe.utils import nowdate, flt
import unittest
+from erpnext.assets.doctype.asset.test_asset import create_asset_data, create_asset, set_depreciation_settings_in_company
class TestAssetRepair(unittest.TestCase):
- pass
+ def setUp(self):
+ set_depreciation_settings_in_company()
+ create_asset_data()
+ frappe.db.sql("delete from `tabTax Rule`")
+
+ def test_update_status(self):
+ asset = create_asset()
+ initial_status = asset.status
+ asset_repair = create_asset_repair(asset = asset)
+
+ if asset_repair.repair_status == "Pending":
+ asset.reload()
+ self.assertEqual(asset.status, "Out of Order")
+
+ asset_repair.repair_status = "Completed"
+ asset_repair.save()
+ asset_status = frappe.db.get_value("Asset", asset_repair.asset, "status")
+ self.assertEqual(asset_status, initial_status)
+
+ def test_stock_item_total_value(self):
+ asset_repair = create_asset_repair(stock_consumption = 1)
+
+ for item in asset_repair.stock_items:
+ total_value = flt(item.valuation_rate) * flt(item.consumed_quantity)
+ self.assertEqual(item.total_value, total_value)
+
+ def test_total_repair_cost(self):
+ asset_repair = create_asset_repair(stock_consumption = 1)
+
+ total_repair_cost = asset_repair.repair_cost
+ self.assertEqual(total_repair_cost, asset_repair.repair_cost)
+ for item in asset_repair.stock_items:
+ total_repair_cost += item.total_value
+
+ self.assertEqual(total_repair_cost, asset_repair.total_repair_cost)
+
+ def test_repair_status_after_submit(self):
+ asset_repair = create_asset_repair(submit = 1)
+ self.assertNotEqual(asset_repair.repair_status, "Pending")
+
+ def test_stock_items(self):
+ asset_repair = create_asset_repair(stock_consumption = 1)
+ self.assertTrue(asset_repair.stock_consumption)
+ self.assertTrue(asset_repair.stock_items)
+
+ def test_warehouse(self):
+ asset_repair = create_asset_repair(stock_consumption = 1)
+ self.assertTrue(asset_repair.stock_consumption)
+ self.assertTrue(asset_repair.warehouse)
+
+ def test_decrease_stock_quantity(self):
+ asset_repair = create_asset_repair(stock_consumption = 1, submit = 1)
+ stock_entry = frappe.get_last_doc('Stock Entry')
+
+ self.assertEqual(stock_entry.stock_entry_type, "Material Issue")
+ self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse)
+ self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item)
+ self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
+
+ def test_increase_in_asset_value_due_to_stock_consumption(self):
+ asset = create_asset(calculate_depreciation = 1)
+ initial_asset_value = get_asset_value(asset)
+ asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1)
+ asset.reload()
+
+ increase_in_asset_value = get_asset_value(asset) - initial_asset_value
+ self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value)
+
+ def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self):
+ asset = create_asset(calculate_depreciation = 1)
+ initial_asset_value = get_asset_value(asset)
+ asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
+ asset.reload()
+
+ increase_in_asset_value = get_asset_value(asset) - initial_asset_value
+ self.assertEqual(asset_repair.repair_cost, increase_in_asset_value)
+
+ def test_purchase_invoice(self):
+ asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1)
+ self.assertTrue(asset_repair.purchase_invoice)
+
+ def test_gl_entries(self):
+ asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1)
+ gl_entry = frappe.get_last_doc('GL Entry')
+ self.assertEqual(asset_repair.name, gl_entry.voucher_no)
+
+ def test_increase_in_asset_life(self):
+ asset = create_asset(calculate_depreciation = 1)
+ initial_num_of_depreciations = num_of_depreciations(asset)
+ create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
+ asset.reload()
+
+ self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset))
+ self.assertEqual(asset.schedules[-1].accumulated_depreciation_amount, asset.finance_books[0].value_after_depreciation)
+
+def get_asset_value(asset):
+ return asset.finance_books[0].value_after_depreciation
+
+def num_of_depreciations(asset):
+ return asset.finance_books[0].total_number_of_depreciations
+
+def create_asset_repair(**args):
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+
+ args = frappe._dict(args)
+
+ if args.asset:
+ asset = args.asset
+ else:
+ asset = create_asset(is_existing_asset = 1)
+ asset_repair = frappe.new_doc("Asset Repair")
+ asset_repair.update({
+ "asset": asset.name,
+ "asset_name": asset.asset_name,
+ "failure_date": nowdate(),
+ "description": "Test Description",
+ "repair_cost": 0,
+ "company": asset.company
+ })
+
+ if args.stock_consumption:
+ asset_repair.stock_consumption = 1
+ asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company)
+ asset_repair.append("stock_items", {
+ "item": args.item or args.item_code or "_Test Item",
+ "valuation_rate": args.rate if args.get("rate") is not None else 100,
+ "consumed_quantity": args.qty or 1
+ })
+
+ asset_repair.insert(ignore_if_duplicate=True)
+
+ if args.submit:
+ asset_repair.repair_status = "Completed"
+ asset_repair.cost_center = "_Test Cost Center - _TC"
+
+ if args.stock_consumption:
+ stock_entry = frappe.get_doc({
+ "doctype": "Stock Entry",
+ "stock_entry_type": "Material Receipt",
+ "company": asset.company
+ })
+ stock_entry.append('items', {
+ "t_warehouse": asset_repair.warehouse,
+ "item_code": asset_repair.stock_items[0].item,
+ "qty": asset_repair.stock_items[0].consumed_quantity
+ })
+ stock_entry.submit()
+
+ if args.capitalize_repair_cost:
+ asset_repair.capitalize_repair_cost = 1
+ asset_repair.repair_cost = 1000
+ if asset.calculate_depreciation:
+ asset_repair.increase_in_asset_life = 12
+ asset_repair.purchase_invoice = make_purchase_invoice().name
+
+ asset_repair.submit()
+ return asset_repair
\ No newline at end of file
diff --git a/erpnext/accounts/accounts b/erpnext/assets/doctype/asset_repair_consumed_item/__init__.py
similarity index 100%
copy from erpnext/accounts/accounts
copy to erpnext/assets/doctype/asset_repair_consumed_item/__init__.py
diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
new file mode 100644
index 0000000..528f0ec
--- /dev/null
+++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
@@ -0,0 +1,55 @@
+{
+ "actions": [],
+ "creation": "2021-05-12 02:41:54.161024",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item",
+ "valuation_rate",
+ "consumed_quantity",
+ "total_value"
+ ],
+ "fields": [
+ {
+ "fieldname": "item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item",
+ "options": "Item"
+ },
+ {
+ "fetch_from": "item.valuation_rate",
+ "fieldname": "valuation_rate",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Valuation Rate",
+ "read_only": 1
+ },
+ {
+ "fieldname": "consumed_quantity",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Consumed Quantity"
+ },
+ {
+ "fieldname": "total_value",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Total Value",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-05-12 03:19:55.006300",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Repair Consumed Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py
new file mode 100644
index 0000000..fa22a57
--- /dev/null
+++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+class AssetRepairConsumedItem(Document):
+ pass
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index 1dbd7c6..132dd17 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -97,6 +97,9 @@
"is_fixed_asset",
"item_tax_rate",
"section_break_72",
+ "production_plan",
+ "production_plan_item",
+ "production_plan_sub_assembly_item",
"page_break"
],
"fields": [
@@ -803,13 +806,37 @@
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "production_plan",
+ "fieldtype": "Link",
+ "label": "Production Plan",
+ "options": "Production Plan",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "production_plan_item",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Production Plan Item",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "production_plan_sub_assembly_item",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Production Plan Sub Assembly Item",
+ "no_copy": 1,
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-03-22 11:46:12.357435",
+ "modified": "2021-06-28 19:22:22.715365",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js
index 4ddc458..1766c2c 100644
--- a/erpnext/buying/doctype/supplier/supplier.js
+++ b/erpnext/buying/doctype/supplier/supplier.js
@@ -60,10 +60,23 @@
erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name);
}, __('Create'));
+ frm.add_custom_button(__('Get Supplier Group Details'), function () {
+ frm.trigger("get_supplier_group_details");
+ }, __('Actions'));
+
// indicators
erpnext.utils.set_party_dashboard_indicators(frm);
}
},
+ get_supplier_group_details: function(frm) {
+ frappe.call({
+ method: "get_supplier_group_details",
+ doc: frm.doc,
+ callback: function() {
+ frm.refresh();
+ }
+ });
+ },
is_internal_supplier: function(frm) {
if (frm.doc.is_internal_supplier == 1) {
diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py
index edeb135..fd16b23 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -51,6 +51,23 @@
validate_party_accounts(self)
self.validate_internal_supplier()
+ @frappe.whitelist()
+ def get_supplier_group_details(self):
+ doc = frappe.get_doc('Supplier Group', self.supplier_group)
+ self.payment_terms = ""
+ self.accounts = []
+
+ if doc.accounts:
+ for account in doc.accounts:
+ child = self.append('accounts')
+ child.company = account.company
+ child.account = account.account
+
+ if doc.payment_terms:
+ self.payment_terms = doc.payment_terms
+
+ self.save()
+
def validate_internal_supplier(self):
internal_supplier = frappe.db.get_value("Supplier",
{"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name")
@@ -86,4 +103,4 @@
create_contact(supplier, 'Supplier',
doc.name, args.get('supplier_email_' + str(i)))
except frappe.NameError:
- pass
\ No newline at end of file
+ pass
diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py
index f9c8d35..8980466 100644
--- a/erpnext/buying/doctype/supplier/test_supplier.py
+++ b/erpnext/buying/doctype/supplier/test_supplier.py
@@ -13,6 +13,30 @@
class TestSupplier(unittest.TestCase):
+ def test_get_supplier_group_details(self):
+ doc = frappe.new_doc("Supplier Group")
+ doc.supplier_group_name = "_Testing Supplier Group"
+ doc.payment_terms = "_Test Payment Term Template 3"
+ doc.accounts = []
+ test_account_details = {
+ "company": "_Test Company",
+ "account": "Creditors - _TC",
+ }
+ doc.append("accounts", test_account_details)
+ doc.save()
+ s_doc = frappe.new_doc("Supplier")
+ s_doc.supplier_name = "Testing Supplier"
+ s_doc.supplier_group = "_Testing Supplier Group"
+ s_doc.payment_terms = ""
+ s_doc.accounts = []
+ s_doc.insert()
+ s_doc.get_supplier_group_details()
+ self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3")
+ self.assertEqual(s_doc.accounts[0].company, "_Test Company")
+ self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC")
+ s_doc.delete()
+ doc.delete()
+
def test_supplier_default_payment_terms(self):
# Payment Term based on Days after invoice date
frappe.db.set_value(
@@ -136,4 +160,4 @@
return doc
except frappe.DuplicateEntryError:
- return frappe.get_doc("Supplier", args.supplier_name)
\ No newline at end of file
+ return frappe.get_doc("Supplier", args.supplier_name)
diff --git a/erpnext/change_log/v13/v13_7_0.md b/erpnext/change_log/v13/v13_7_0.md
new file mode 100644
index 0000000..589f610
--- /dev/null
+++ b/erpnext/change_log/v13/v13_7_0.md
@@ -0,0 +1,69 @@
+# Version 13.7.0 Release Notes
+
+### Features & Enhancements
+- Optionally allow rejected quality inspection on submission ([#26133](https://github.com/frappe/erpnext/pull/26133))
+- Bootstrapped GST Setup for India ([#25415](https://github.com/frappe/erpnext/pull/25415))
+- Fetching details from supplier/customer groups ([#26454](https://github.com/frappe/erpnext/pull/26454))
+- Provision to make subcontracted purchase order from the production plan ([#26240](https://github.com/frappe/erpnext/pull/26240))
+- Optimized code for reposting item valuation ([#26432](https://github.com/frappe/erpnext/pull/26432))
+
+### Fixes
+- Auto process deferred accounting for multi-company setup ([#26277](https://github.com/frappe/erpnext/pull/26277))
+- Error while fetching item taxes ([#26218](https://github.com/frappe/erpnext/pull/26218))
+- Validation check for batch for stock reconciliation type in stock entry(bp #26370 ) ([#26488](https://github.com/frappe/erpnext/pull/26488))
+- Error popup for COA errors ([#26358](https://github.com/frappe/erpnext/pull/26358))
+- Precision for expected values in payment entry test ([#26394](https://github.com/frappe/erpnext/pull/26394))
+- Bank statement import ([#26287](https://github.com/frappe/erpnext/pull/26287))
+- LMS progress issue ([#26253](https://github.com/frappe/erpnext/pull/26253))
+- Paging buttons not working on item group portal page ([#26497](https://github.com/frappe/erpnext/pull/26497))
+- Omit item discount amount for e-invoicing ([#26353](https://github.com/frappe/erpnext/pull/26353))
+- Validate LCV for Invoices without Update Stock ([#26333](https://github.com/frappe/erpnext/pull/26333))
+- Remove cancelled entries in consolidated financial statements ([#26331](https://github.com/frappe/erpnext/pull/26331))
+- Fetching employee in payroll entry ([#26271](https://github.com/frappe/erpnext/pull/26271))
+- To fetch the correct field in Tax Rule ([#25927](https://github.com/frappe/erpnext/pull/25927))
+- Order and time of operations in multilevel BOM work order ([#25886](https://github.com/frappe/erpnext/pull/25886))
+- Fixed Budget Variance Graph color from all black to default ([#26368](https://github.com/frappe/erpnext/pull/26368))
+- TDS computation summary shows cancelled invoices (#26456) ([#26486](https://github.com/frappe/erpnext/pull/26486))
+- Do not consider cancelled entries in party dashboard ([#26231](https://github.com/frappe/erpnext/pull/26231))
+- Add validation for 'for_qty' else throws errors ([#25829](https://github.com/frappe/erpnext/pull/25829))
+- Move the rename abbreviation job to long queue (#26434) ([#26462](https://github.com/frappe/erpnext/pull/26462))
+- Query for Training Event ([#26388](https://github.com/frappe/erpnext/pull/26388))
+- Item group portal issues (backport) ([#26493](https://github.com/frappe/erpnext/pull/26493))
+- When lead is created with mobile_no, mobile_no value gets lost ([#26298](https://github.com/frappe/erpnext/pull/26298))
+- WIP needs to be set before submit on skip_transfer (bp #26499) ([#26507](https://github.com/frappe/erpnext/pull/26507))
+- Incorrect valuation rate in stock reconciliation ([#26259](https://github.com/frappe/erpnext/pull/26259))
+- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046))
+- Changed profitability analysis report width ([#26165](https://github.com/frappe/erpnext/pull/26165))
+- Unable to download GSTR-1 json ([#26468](https://github.com/frappe/erpnext/pull/26468))
+- Unallocated amount in Payment Entry after taxes ([#26472](https://github.com/frappe/erpnext/pull/26472))
+- Include Stock Reco logic in `update_qty_in_future_sle` ([#26158](https://github.com/frappe/erpnext/pull/26158))
+- Update cost not working in the draft BOM ([#26279](https://github.com/frappe/erpnext/pull/26279))
+- Cancellation of Loan Security Pledges ([#26252](https://github.com/frappe/erpnext/pull/26252))
+- fix(e-invoicing): allow export invoice even if no taxes applied (#26363) ([#26405](https://github.com/frappe/erpnext/pull/26405))
+- Delete accounts (an empty file) ([#25323](https://github.com/frappe/erpnext/pull/25323))
+- Errors on parallel requests creation of company for India ([#26470](https://github.com/frappe/erpnext/pull/26470))
+- Incorrect bom no added for non-variant items on variant boms ([#26320](https://github.com/frappe/erpnext/pull/26320))
+- Incorrect discount amount on amended document ([#26466](https://github.com/frappe/erpnext/pull/26466))
+- Added a message to enable appointment booking if disabled ([#26334](https://github.com/frappe/erpnext/pull/26334))
+- fix(pos): taxes amount in pos item cart ([#26411](https://github.com/frappe/erpnext/pull/26411))
+- Track changes on batch ([#26382](https://github.com/frappe/erpnext/pull/26382))
+- Stock entry with putaway rule not working ([#26350](https://github.com/frappe/erpnext/pull/26350))
+- Only "Tax" type accounts should be shown for selection in GST Settings ([#26300](https://github.com/frappe/erpnext/pull/26300))
+- Added permission for employee to book appointment ([#26255](https://github.com/frappe/erpnext/pull/26255))
+- Allow to make job card without employee ([#26312](https://github.com/frappe/erpnext/pull/26312))
+- Project Portal Enhancements ([#26290](https://github.com/frappe/erpnext/pull/26290))
+- BOM stock report not working ([#26332](https://github.com/frappe/erpnext/pull/26332))
+- Order Items by weightage in the web items query ([#26284](https://github.com/frappe/erpnext/pull/26284))
+- Removed values out of sync validation from stock transactions ([#26226](https://github.com/frappe/erpnext/pull/26226))
+- Payroll-entry minor fix ([#26349](https://github.com/frappe/erpnext/pull/26349))
+- Allow user to change the To Date in the blanket order even after submit of order ([#26241](https://github.com/frappe/erpnext/pull/26241))
+- Value fetching for custom field in POS ([#26367](https://github.com/frappe/erpnext/pull/26367))
+- Iteration through accounts only when accounts exist ([#26391](https://github.com/frappe/erpnext/pull/26391))
+- Employee Inactive status implications ([#26244](https://github.com/frappe/erpnext/pull/26244))
+- Multi-currency issue ([#26458](https://github.com/frappe/erpnext/pull/26458))
+- FG item not fetched in manufacture entry ([#26509](https://github.com/frappe/erpnext/pull/26509))
+- Set query for training events ([#26303](https://github.com/frappe/erpnext/pull/26303))
+- Fetch batch items in stock reconciliation ([#26213](https://github.com/frappe/erpnext/pull/26213))
+- Employee selection not working in payroll entry ([#26278](https://github.com/frappe/erpnext/pull/26278))
+- POS item cart dom updates (#26459) ([#26461](https://github.com/frappe/erpnext/pull/26461))
+- dunning calculation of grand total when rate of interest is 0% ([#26285](https://github.com/frappe/erpnext/pull/26285))
\ No newline at end of file
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 1c086e9..cdd865a 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -124,6 +124,8 @@
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
self.set_advances()
+ self.set_advance_gain_or_loss()
+
if self.is_return:
self.validate_qty()
else:
@@ -584,15 +586,18 @@
allocated_amount = min(amount - advance_allocated, d.amount)
advance_allocated += flt(allocated_amount)
- self.append("advances", {
+ advance_row = {
"doctype": self.doctype + " Advance",
"reference_type": d.reference_type,
"reference_name": d.reference_name,
"reference_row": d.reference_row,
"remarks": d.remarks,
"advance_amount": flt(d.amount),
- "allocated_amount": allocated_amount
- })
+ "allocated_amount": allocated_amount,
+ "ref_exchange_rate": flt(d.exchange_rate) # exchange_rate of advance entry
+ }
+
+ self.append("advances", advance_row)
def get_advance_entries(self, include_unallocated=True):
if self.doctype == "Sales Invoice":
@@ -650,6 +655,66 @@
"Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice.")
.format(d.reference_name, d.against_order))
+ def set_advance_gain_or_loss(self):
+ if not self.get("advances"):
+ return
+
+ for d in self.get("advances"):
+ advance_exchange_rate = d.ref_exchange_rate
+ if (d.allocated_amount and self.conversion_rate != 1
+ and self.conversion_rate != advance_exchange_rate):
+
+ base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount
+ base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount
+ difference = base_allocated_amount_in_ref_rate - base_allocated_amount_in_inv_rate
+
+ d.exchange_gain_loss = difference
+
+ def make_exchange_gain_loss_gl_entries(self, gl_entries):
+ if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']:
+ for d in self.get("advances"):
+ if d.exchange_gain_loss:
+ party = self.supplier if self.get('doctype') == 'Purchase Invoice' else self.customer
+ party_account = self.credit_to if self.get('doctype') == 'Purchase Invoice' else self.debit_to
+ party_type = "Supplier" if self.get('doctype') == 'Purchase Invoice' else "Customer"
+
+ gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account')
+ account_currency = get_account_currency(gain_loss_account)
+ if account_currency != self.company_currency:
+ frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency))
+
+ # for purchase
+ dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit'
+ # just reverse for sales?
+ dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit'
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": gain_loss_account,
+ "account_currency": account_currency,
+ "against": party,
+ dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss),
+ dr_or_cr: abs(d.exchange_gain_loss),
+ "cost_center": self.cost_center,
+ "project": self.project
+ }, item=d)
+ )
+
+ dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit'
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": party_account,
+ "party_type": party_type,
+ "party": party,
+ "against": gain_loss_account,
+ dr_or_cr + "_in_account_currency": flt(abs(d.exchange_gain_loss) / self.conversion_rate),
+ dr_or_cr: abs(d.exchange_gain_loss),
+ "cost_center": self.cost_center,
+ "project": self.project
+ }, self.party_account_currency, item=self)
+ )
+
def update_against_document_in_jv(self):
"""
Links invoice and advance voucher:
@@ -690,7 +755,9 @@
if self.party_account_currency != self.company_currency else 1),
'grand_total': (self.base_grand_total
if self.party_account_currency == self.company_currency else self.grand_total),
- 'outstanding_amount': self.outstanding_amount
+ 'outstanding_amount': self.outstanding_amount,
+ 'difference_account': frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account'),
+ 'exchange_gain_loss': flt(d.get('exchange_gain_loss'))
})
lst.append(args)
@@ -751,11 +818,11 @@
account_currency = get_account_currency(tax.account_head)
if self.doctype == "Purchase Invoice":
- dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
- rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
- else:
dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
+ else:
+ dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
+ rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
party = self.supplier if self.doctype == "Purchase Invoice" else self.customer
unallocated_amount = tax.tax_amount - tax.allocated_amount
@@ -1045,8 +1112,11 @@
for d in self.get("payment_schedule"):
if d.invoice_portion:
d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
- d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
+ d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('base_payment_amount'))
d.outstanding = d.payment_amount
+ elif not d.invoice_portion:
+ d.base_payment_amount = flt(base_grand_total * self.get("conversion_rate"), d.precision('base_payment_amount'))
+
def set_due_date(self):
due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date]
@@ -1289,6 +1359,8 @@
party_account_field = "paid_from" if party_type == "Customer" else "paid_to"
currency_field = "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency"
payment_type = "Receive" if party_type == "Customer" else "Pay"
+ exchange_rate_field = "source_exchange_rate" if payment_type == "Receive" else "target_exchange_rate"
+
payment_entries_against_order, unallocated_payment_entries = [], []
limit_cond = "limit %s" % limit if limit else ""
@@ -1305,27 +1377,28 @@
"Payment Entry" as reference_type, t1.name as reference_name,
t1.remarks, t2.allocated_amount as amount, t2.name as reference_row,
t2.reference_name as against_order, t1.posting_date,
- t1.{0} as currency
+ t1.{0} as currency, t1.{4} as exchange_rate
from `tabPayment Entry` t1, `tabPayment Entry Reference` t2
where
t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s
and t1.party_type = %s and t1.party = %s and t1.docstatus = 1
and t2.reference_doctype = %s {2}
order by t1.posting_date {3}
- """.format(currency_field, party_account_field, reference_condition, limit_cond),
+ """.format(currency_field, party_account_field, reference_condition, limit_cond, exchange_rate_field),
[party_account, payment_type, party_type, party,
order_doctype] + order_list, as_dict=1)
if include_unallocated:
unallocated_payment_entries = frappe.db.sql("""
select "Payment Entry" as reference_type, name as reference_name,
- remarks, unallocated_amount as amount
+ remarks, unallocated_amount as amount, {2} as exchange_rate
from `tabPayment Entry`
where
{0} = %s and party_type = %s and party = %s and payment_type = %s
and docstatus = 1 and unallocated_amount > 0
order by posting_date {1}
- """.format(party_account_field, limit_cond), (party_account, party_type, party, payment_type), as_dict=1)
+ """.format(party_account_field, limit_cond, exchange_rate_field),
+ (party_account, party_type, party, payment_type), as_dict=1)
return list(payment_entries_against_order) + list(unallocated_payment_entries)
diff --git a/erpnext/controllers/employee_boarding_controller.py b/erpnext/controllers/employee_boarding_controller.py
new file mode 100644
index 0000000..1898222
--- /dev/null
+++ b/erpnext/controllers/employee_boarding_controller.py
@@ -0,0 +1,127 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from frappe import _
+from frappe.desk.form import assign_to
+from frappe.model.document import Document
+from frappe.utils import flt, unique
+
+class EmployeeBoardingController(Document):
+ '''
+ Create the project and the task for the boarding process
+ Assign to the concerned person and roles as per the onboarding/separation template
+ '''
+ def validate(self):
+ # remove the task if linked before submitting the form
+ if self.amended_from:
+ for activity in self.activities:
+ activity.task = ''
+
+ def on_submit(self):
+ # create the project for the given employee onboarding
+ project_name = _(self.doctype) + ' : '
+ if self.doctype == 'Employee Onboarding':
+ project_name += self.job_applicant
+ else:
+ project_name += self.employee
+
+ project = frappe.get_doc({
+ 'doctype': 'Project',
+ 'project_name': project_name,
+ 'expected_start_date': self.date_of_joining if self.doctype == 'Employee Onboarding' else self.resignation_letter_date,
+ 'department': self.department,
+ 'company': self.company
+ }).insert(ignore_permissions=True, ignore_mandatory=True)
+
+ self.db_set('project', project.name)
+ self.db_set('boarding_status', 'Pending')
+ self.reload()
+ self.create_task_and_notify_user()
+
+ def create_task_and_notify_user(self):
+ # create the task for the given project and assign to the concerned person
+ for activity in self.activities:
+ if activity.task:
+ continue
+
+ task = frappe.get_doc({
+ 'doctype': 'Task',
+ 'project': self.project,
+ 'subject': activity.activity_name + ' : ' + self.employee_name,
+ 'description': activity.description,
+ 'department': self.department,
+ 'company': self.company,
+ 'task_weight': activity.task_weight
+ }).insert(ignore_permissions=True)
+ activity.db_set('task', task.name)
+
+ users = [activity.user] if activity.user else []
+ if activity.role:
+ user_list = frappe.db.sql_list('''
+ SELECT
+ DISTINCT(has_role.parent)
+ FROM
+ `tabHas Role` has_role
+ LEFT JOIN `tabUser` user
+ ON has_role.parent = user.name
+ WHERE
+ has_role.parenttype = 'User'
+ AND user.enabled = 1
+ AND has_role.role = %s
+ ''', activity.role)
+ users = unique(users + user_list)
+
+ if 'Administrator' in users:
+ users.remove('Administrator')
+
+ # assign the task the users
+ if users:
+ self.assign_task_to_users(task, users)
+
+ def assign_task_to_users(self, task, users):
+ for user in users:
+ args = {
+ 'assign_to': [user],
+ 'doctype': task.doctype,
+ 'name': task.name,
+ 'description': task.description or task.subject,
+ 'notify': self.notify_users_by_email
+ }
+ assign_to.add(args)
+
+ def on_cancel(self):
+ # delete task project
+ for task in frappe.get_all('Task', filters={'project': self.project}):
+ frappe.delete_doc('Task', task.name, force=1)
+ frappe.delete_doc('Project', self.project, force=1)
+ self.db_set('project', '')
+ for activity in self.activities:
+ activity.db_set('task', '')
+
+
+@frappe.whitelist()
+def get_onboarding_details(parent, parenttype):
+ return frappe.get_all('Employee Boarding Activity',
+ fields=['activity_name', 'role', 'user', 'required_for_employee_creation', 'description', 'task_weight'],
+ filters={'parent': parent, 'parenttype': parenttype},
+ order_by= 'idx')
+
+
+def update_employee_boarding_status(project):
+ employee_onboarding = frappe.db.exists('Employee Onboarding', {'project': project.name})
+ employee_separation = frappe.db.exists('Employee Separation', {'project': project.name})
+
+ if not (employee_onboarding or employee_separation):
+ return
+
+ status = 'Pending'
+ if flt(project.percent_complete) > 0.0 and flt(project.percent_complete) < 100.0:
+ status = 'In Process'
+ elif flt(project.percent_complete) == 100.0:
+ status = 'Completed'
+
+ if employee_onboarding:
+ frappe.db.set_value('Employee Onboarding', employee_onboarding, 'boarding_status', status)
+ elif employee_separation:
+ frappe.db.set_value('Employee Separation', employee_separation, 'boarding_status', status)
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 8196cff..17bd735 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -53,12 +53,17 @@
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
for d in self.get("items"):
if hasattr(d, 'serial_no') and hasattr(d, 'batch_no') and d.serial_no and d.batch_no:
- serial_nos = get_serial_nos(d.serial_no)
- for serial_no_data in frappe.get_all("Serial No",
- filters={"name": ("in", serial_nos)}, fields=["batch_no", "name"]):
- if serial_no_data.batch_no != d.batch_no:
+ serial_nos = frappe.get_all("Serial No",
+ fields=["batch_no", "name", "warehouse"],
+ filters={
+ "name": ("in", get_serial_nos(d.serial_no))
+ }
+ )
+
+ for row in serial_nos:
+ if row.warehouse and row.batch_no != d.batch_no:
frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}")
- .format(d.idx, serial_no_data.name, d.batch_no))
+ .format(d.idx, row.name, d.batch_no))
if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2:
expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date")
@@ -356,42 +361,68 @@
}, update_modified)
def validate_inspection(self):
- '''Checks if quality inspection is set for Items that require inspection.
- On submit, throw an exception'''
- inspection_required_fieldname = None
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
- inspection_required_fieldname = "inspection_required_before_purchase"
- elif self.doctype in ["Delivery Note", "Sales Invoice"]:
- inspection_required_fieldname = "inspection_required_before_delivery"
+ """Checks if quality inspection is set/ is valid for Items that require inspection."""
+ inspection_fieldname_map = {
+ "Purchase Receipt": "inspection_required_before_purchase",
+ "Purchase Invoice": "inspection_required_before_purchase",
+ "Sales Invoice": "inspection_required_before_delivery",
+ "Delivery Note": "inspection_required_before_delivery"
+ }
+ inspection_required_fieldname = inspection_fieldname_map.get(self.doctype)
+ # return if inspection is not required on document level
if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or
(self.doctype == "Stock Entry" and not self.inspection_required) or
(self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)):
return
- for d in self.get('items'):
- qa_required = False
- if (inspection_required_fieldname and not d.quality_inspection and
- frappe.db.get_value("Item", d.item_code, inspection_required_fieldname)):
- qa_required = True
- elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse:
- qa_required = True
- if self.docstatus == 1 and d.quality_inspection:
- qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection)
- if qa_doc.docstatus == 0:
- link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection)
- frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError)
+ for row in self.get('items'):
+ qi_required = False
+ if (inspection_required_fieldname and frappe.db.get_value("Item", row.item_code, inspection_required_fieldname)):
+ qi_required = True
+ elif self.doctype == "Stock Entry" and row.t_warehouse:
+ qi_required = True # inward stock needs inspection
- if qa_doc.status != 'Accepted':
- frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}")
- .format(d.idx, d.item_code), QualityInspectionRejectedError)
- elif qa_required :
- action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted
- if self.docstatus==1 and action == 'Stop':
- frappe.throw(_("Quality Inspection required for Item {0} to submit").format(frappe.bold(d.item_code)),
- exc=QualityInspectionRequiredError)
- else:
- frappe.msgprint(_("Create Quality Inspection for Item {0}").format(frappe.bold(d.item_code)))
+ if qi_required: # validate row only if inspection is required on item level
+ self.validate_qi_presence(row)
+ if self.docstatus == 1:
+ self.validate_qi_submission(row)
+ self.validate_qi_rejection(row)
+
+ def validate_qi_presence(self, row):
+ """Check if QI is present on row level. Warn on save and stop on submit if missing."""
+ if not row.quality_inspection:
+ msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}"
+ if self.docstatus == 1:
+ frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError)
+ else:
+ frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue")
+
+ def validate_qi_submission(self, row):
+ """Check if QI is submitted on row level, during submission"""
+ action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted")
+ qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus")
+
+ if not qa_docstatus == 1:
+ link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection)
+ msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}"
+ if action == "Stop":
+ frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError)
+ else:
+ frappe.msgprint(_(msg), alert=True, indicator="orange")
+
+ def validate_qi_rejection(self, row):
+ """Check if QI is rejected on row level, during submission"""
+ action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_rejected")
+ qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status")
+
+ if qa_status == "Rejected":
+ link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection)
+ msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}"
+ if action == "Stop":
+ frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError)
+ else:
+ frappe.msgprint(_(msg), alert=True, indicator="orange")
def update_blanket_order(self):
blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order]))
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 56da5b7..099c7d4 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -152,7 +152,7 @@
validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, self.doc)
- if not self.doc.get('is_consolidated'):
+ if not (self.doc.get('is_consolidated') or tax.get("dont_recompute_tax")):
tax.item_wise_tax_detail = {}
tax_fields = ["total", "tax_amount_after_discount_amount",
@@ -347,7 +347,7 @@
elif tax.charge_type == "On Item Quantity":
current_tax_amount = tax_rate * item.qty
- if not self.doc.get("is_consolidated"):
+ if not (self.doc.get("is_consolidated") or tax.get("dont_recompute_tax")):
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
return current_tax_amount
@@ -455,7 +455,8 @@
def _cleanup(self):
if not self.doc.get('is_consolidated'):
for tax in self.doc.get("taxes"):
- tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
+ if not tax.get("dont_recompute_tax"):
+ tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
def set_discount_amount(self):
if self.doc.additional_discount_percentage:
diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json
index 8517dde..fe7b4e1 100644
--- a/erpnext/crm/doctype/appointment/appointment.json
+++ b/erpnext/crm/doctype/appointment/appointment.json
@@ -102,7 +102,7 @@
}
],
"links": [],
- "modified": "2020-01-28 16:16:45.447213",
+ "modified": "2021-06-30 13:09:14.228756",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment",
@@ -153,6 +153,18 @@
"role": "Sales User",
"share": 1,
"write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Employee",
+ "share": 1,
+ "write": 1
}
],
"quick_entry": 1,
diff --git a/erpnext/accounts/accounts b/erpnext/crm/doctype/campaign/__init__.py
similarity index 100%
rename from erpnext/accounts/accounts
rename to erpnext/crm/doctype/campaign/__init__.py
diff --git a/erpnext/crm/doctype/campaign/campaign.js b/erpnext/crm/doctype/campaign/campaign.js
new file mode 100644
index 0000000..11bfa74
--- /dev/null
+++ b/erpnext/crm/doctype/campaign/campaign.js
@@ -0,0 +1,17 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Campaign', {
+ refresh: function(frm) {
+ erpnext.toggle_naming_series();
+
+ if (frm.doc.__islocal) {
+ frm.toggle_display("naming_series", frappe.boot.sysdefaults.campaign_naming_by=="Naming Series");
+ } else {
+ cur_frm.add_custom_button(__("View Leads"), function() {
+ frappe.route_options = {"source": "Campaign", "campaign_name": frm.doc.name};
+ frappe.set_route("List", "Lead");
+ }, "fa fa-list", true);
+ }
+ }
+});
diff --git a/erpnext/selling/doctype/campaign/campaign.json b/erpnext/crm/doctype/campaign/campaign.json
similarity index 95%
rename from erpnext/selling/doctype/campaign/campaign.json
rename to erpnext/crm/doctype/campaign/campaign.json
index 986ac13..f833f4c 100644
--- a/erpnext/selling/doctype/campaign/campaign.json
+++ b/erpnext/crm/doctype/campaign/campaign.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "naming_series:",
@@ -39,17 +40,9 @@
"set_only_once": 1
},
{
- "fieldname": "description",
- "fieldtype": "Text",
- "in_list_view": 1,
- "label": "Description",
- "oldfieldname": "description",
- "oldfieldtype": "Text",
- "width": "300px"
- },
- {
- "fieldname": "description_section",
- "fieldtype": "Section Break"
+ "fieldname": "campaign_schedules_section",
+ "fieldtype": "Section Break",
+ "label": "Campaign Schedules"
},
{
"fieldname": "campaign_schedules",
@@ -58,16 +51,25 @@
"options": "Campaign Email Schedule"
},
{
- "fieldname": "campaign_schedules_section",
- "fieldtype": "Section Break",
- "label": "Campaign Schedules"
+ "fieldname": "description_section",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "in_list_view": 1,
+ "label": "Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text",
+ "width": "300px"
}
],
"icon": "fa fa-bullhorn",
"idx": 1,
- "modified": "2019-07-22 12:03:39.832342",
+ "links": [],
+ "modified": "2021-06-30 18:05:06.412712",
"modified_by": "Administrator",
- "module": "Selling",
+ "module": "CRM",
"name": "Campaign",
"owner": "Administrator",
"permissions": [
diff --git a/erpnext/selling/doctype/campaign/campaign.py b/erpnext/crm/doctype/campaign/campaign.py
similarity index 66%
rename from erpnext/selling/doctype/campaign/campaign.py
rename to erpnext/crm/doctype/campaign/campaign.py
index 1094542..e32799f 100644
--- a/erpnext/selling/doctype/campaign/campaign.py
+++ b/erpnext/crm/doctype/campaign/campaign.py
@@ -1,9 +1,7 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
-
from frappe.model.document import Document
from frappe.model.naming import set_name_by_naming_series
diff --git a/erpnext/crm/doctype/campaign/test_campaign.py b/erpnext/crm/doctype/campaign/test_campaign.py
new file mode 100644
index 0000000..7124b8c
--- /dev/null
+++ b/erpnext/crm/doctype/campaign/test_campaign.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestCampaign(unittest.TestCase):
+ pass
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index d1d0968..ce3de40 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -168,12 +168,13 @@
if self.phone:
contact.append("phone_nos", {
"phone": self.phone,
- "is_primary": 1
+ "is_primary_phone": 1
})
if self.mobile_no:
contact.append("phone_nos", {
- "phone": self.mobile_no
+ "phone": self.mobile_no,
+ "is_primary_mobile_no":1
})
contact.insert(ignore_permissions=True)
diff --git a/erpnext/education/utils.py b/erpnext/education/utils.py
index 9db8a4a..3070e6a 100644
--- a/erpnext/education/utils.py
+++ b/erpnext/education/utils.py
@@ -355,11 +355,11 @@
student = get_current_student()
course_enrollment = get_enrollment("course", course, student.name)
if not course_enrollment:
- program_enrollment = get_enrollment('program', program, student.name)
+ program_enrollment = get_enrollment('program', program.name, student.name)
if not program_enrollment:
frappe.throw(_("You are not enrolled in program {0}").format(program))
return
- return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program, student.name))
+ return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program.name, student.name))
else:
return frappe.get_doc('Course Enrollment', course_enrollment)
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py
index 3c2e59a..b0e662d 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py
@@ -7,16 +7,21 @@
import unittest
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import process_balance_info, verify_transaction
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
+from erpnext.erpnext_integrations.utils import create_mode_of_payment
class TestMpesaSettings(unittest.TestCase):
+ def setUp(self):
+ # create payment gateway in setup
+ create_mpesa_settings(payment_gateway_name="_Test")
+ create_mpesa_settings(payment_gateway_name="_Account Balance")
+ create_mpesa_settings(payment_gateway_name="Payment")
+
def tearDown(self):
frappe.db.sql('delete from `tabMpesa Settings`')
frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"')
def test_creation_of_payment_gateway(self):
- create_mpesa_settings(payment_gateway_name="_Test")
-
- mode_of_payment = frappe.get_doc("Mode of Payment", "Mpesa-_Test")
+ mode_of_payment = create_mode_of_payment('Mpesa-_Test', payment_type="Phone")
self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"}))
self.assertTrue(mode_of_payment.name)
self.assertEqual(mode_of_payment.type, "Phone")
@@ -47,7 +52,6 @@
integration_request.delete()
def test_processing_of_callback_payload(self):
- create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
@@ -90,7 +94,6 @@
pos_invoice.delete()
def test_processing_of_multiple_callback_payload(self):
- create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
@@ -141,7 +144,6 @@
pos_invoice.delete()
def test_processing_of_only_one_succes_callback_payload(self):
- create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
@@ -202,6 +204,7 @@
doc = frappe.get_doc(dict( #nosec
doctype="Mpesa Settings",
+ sandbox=1,
payment_gateway_name=payment_gateway_name,
consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn",
consumer_secret="VI1oS3oBGPJfh3JyvLHw",
diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py
index 3840e78..a5e162f 100644
--- a/erpnext/erpnext_integrations/utils.py
+++ b/erpnext/erpnext_integrations/utils.py
@@ -52,7 +52,8 @@
"payment_gateway": gateway
}, ['payment_account'])
- if not frappe.db.exists("Mode of Payment", gateway) and payment_gateway_account:
+ mode_of_payment = frappe.db.exists("Mode of Payment", gateway)
+ if not mode_of_payment and payment_gateway_account:
mode_of_payment = frappe.get_doc({
"doctype": "Mode of Payment",
"mode_of_payment": gateway,
@@ -66,6 +67,10 @@
})
mode_of_payment.insert(ignore_permissions=True)
+ return mode_of_payment
+ elif mode_of_payment:
+ return frappe.get_doc("Mode of Payment", mode_of_payment)
+
def get_tracking_url(carrier, tracking_number):
# Return the formatted Tracking URL.
tracking_url = ''
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index ba10b58..59b011d 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -25,7 +25,8 @@
"Address": "public/js/address.js",
"Communication": "public/js/communication.js",
"Event": "public/js/event.js",
- "Newsletter": "public/js/newsletter.js"
+ "Newsletter": "public/js/newsletter.js",
+ "Contact": "public/js/contact.js"
}
override_doctype_class = {
@@ -245,7 +246,10 @@
"erpnext.portal.utils.set_default_role"]
},
"Communication": {
- "on_update": "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time"
+ "on_update": [
+ "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time",
+ "erpnext.support.doctype.issue.issue.set_first_response_time"
+ ]
},
("Sales Taxes and Charges Template", 'Price List'): {
"on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings"
diff --git a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.js b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.js
index d6047e1..5d1a024 100644
--- a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.js
+++ b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.js
@@ -50,28 +50,13 @@
}, __('Create'));
frm.page.set_inner_btn_group_as_primary(__('Create'));
}
- if (frm.doc.docstatus === 1 && frm.doc.project) {
- frappe.call({
- method: "erpnext.hr.utils.get_boarding_status",
- args: {
- "project": frm.doc.project
- },
- callback: function(r) {
- if (r.message) {
- frm.set_value('boarding_status', r.message);
- }
- refresh_field("boarding_status");
- }
- });
- }
-
},
employee_onboarding_template: function(frm) {
frm.set_value("activities" ,"");
if (frm.doc.employee_onboarding_template) {
frappe.call({
- method: "erpnext.hr.utils.get_onboarding_details",
+ method: "erpnext.controllers.employee_boarding_controller.get_onboarding_details",
args: {
"parent": frm.doc.employee_onboarding_template,
"parenttype": "Employee Onboarding Template"
diff --git a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.json b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.json
index 783c757..673e228 100644
--- a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.json
+++ b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.json
@@ -30,18 +30,14 @@
"fieldtype": "Link",
"label": "Job Applicant",
"options": "Job Applicant",
- "reqd": 1,
- "show_days": 1,
- "show_seconds": 1
+ "reqd": 1
},
{
"fieldname": "job_offer",
"fieldtype": "Link",
"label": "Job Offer",
"options": "Job Offer",
- "reqd": 1,
- "show_days": 1,
- "show_seconds": 1
+ "reqd": 1
},
{
"fetch_from": "job_applicant.applicant_name",
@@ -49,116 +45,90 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "Employee Name",
- "reqd": 1,
- "show_days": 1,
- "show_seconds": 1
+ "reqd": 1
},
{
"fieldname": "employee",
"fieldtype": "Link",
"label": "Employee",
"options": "Employee",
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "date_of_joining",
"fieldtype": "Date",
"in_list_view": 1,
- "label": "Date of Joining",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Date of Joining"
},
{
"allow_on_submit": 1,
+ "default": "Pending",
"fieldname": "boarding_status",
"fieldtype": "Select",
"label": "Status",
- "options": "\nPending\nIn Process\nCompleted",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Pending\nIn Process\nCompleted",
+ "read_only": 1
},
{
"allow_on_submit": 1,
"default": "0",
"fieldname": "notify_users_by_email",
"fieldtype": "Check",
- "label": "Notify users by email",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Notify users by email"
},
{
"fieldname": "column_break_7",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"fieldname": "employee_onboarding_template",
"fieldtype": "Link",
"label": "Employee Onboarding Template",
- "options": "Employee Onboarding Template",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Employee Onboarding Template"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
- "options": "Company",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Company"
},
{
"fieldname": "department",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Department",
- "options": "Department",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Department"
},
{
"fieldname": "designation",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Designation",
- "options": "Designation",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Designation"
},
{
"fieldname": "employee_grade",
"fieldtype": "Link",
"label": "Employee Grade",
- "options": "Employee Grade",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Employee Grade"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project",
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "table_for_activity",
- "fieldtype": "Section Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "activities",
"fieldtype": "Table",
"label": "Activities",
- "options": "Employee Boarding Activity",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Employee Boarding Activity"
},
{
"fieldname": "amended_from",
@@ -167,14 +137,12 @@
"no_copy": 1,
"options": "Employee Onboarding",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
}
],
"is_submittable": 1,
"links": [],
- "modified": "2020-06-25 15:22:24.923835",
+ "modified": "2021-06-03 18:01:51.097927",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Onboarding",
diff --git a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py
index 6cc2bf5..55fe317 100644
--- a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from erpnext.hr.utils import EmployeeBoardingController
+from erpnext.controllers.employee_boarding_controller import EmployeeBoardingController
from frappe.model.mapper import get_mapped_doc
class IncompleteTaskError(frappe.ValidationError): pass
@@ -16,9 +16,9 @@
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} already exists for Job Applicant: {1}").format(frappe.bold(emp_onboarding), frappe.bold(self.job_applicant)))
def validate_employee_creation(self):
if self.docstatus != 1:
@@ -30,7 +30,7 @@
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 tasks for employee creation are not completed yet."), IncompleteTaskError)
def on_submit(self):
super(EmployeeOnboarding, self).on_submit()
diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
index 336e13c..5f7756b 100644
--- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
@@ -11,39 +11,26 @@
from erpnext.hr.doctype.job_offer.test_job_offer import create_job_offer
class TestEmployeeOnboarding(unittest.TestCase):
- def test_employee_onboarding_incomplete_task(self):
+ def setUp(self):
if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}):
frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'})
- _set_up()
- applicant = get_job_applicant()
- job_offer = create_job_offer(job_applicant=applicant.name)
- job_offer.submit()
+ project = "Employee Onboarding : Test Researcher - test@researcher.com"
+ frappe.db.sql("delete from tabProject where name=%s", project)
+ frappe.db.sql("delete from tabTask where project=%s", project)
- onboarding = frappe.new_doc('Employee Onboarding')
- onboarding.job_applicant = applicant.name
- onboarding.job_offer = job_offer.name
- onboarding.company = '_Test Company'
- onboarding.designation = 'Researcher'
- onboarding.append('activities', {
- 'activity_name': 'Assign ID Card',
- 'role': 'HR User',
- 'required_for_employee_creation': 1
- })
- onboarding.append('activities', {
- 'activity_name': 'Assign a laptop',
- 'role': 'HR User'
- })
- onboarding.status = 'Pending'
- onboarding.insert()
- onboarding.submit()
+ def test_employee_onboarding_incomplete_task(self):
+ onboarding = create_employee_onboarding()
- project_name = frappe.db.get_value("Project", onboarding.project, "project_name")
+ project_name = frappe.db.get_value('Project', onboarding.project, 'project_name')
self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com')
# don't allow making employee if onboarding is not complete
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
+ # boarding status
+ self.assertEqual(onboarding.boarding_status, 'Pending')
+
# complete the task
project = frappe.get_doc('Project', onboarding.project)
for task in frappe.get_all('Task', dict(project=project.name)):
@@ -51,6 +38,10 @@
task.status = 'Completed'
task.save()
+ # boarding status
+ onboarding.reload()
+ self.assertEqual(onboarding.boarding_status, 'Completed')
+
# make employee
onboarding.reload()
employee = make_employee(onboarding.name)
@@ -61,6 +52,13 @@
employee.insert()
self.assertEqual(employee.employee_name, 'Test Researcher')
+ def tearDown(self):
+ for entry in frappe.get_all('Employee Onboarding'):
+ doc = frappe.get_doc('Employee Onboarding', entry.name)
+ doc.cancel()
+ doc.delete()
+
+
def get_job_applicant():
if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'):
return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com')
@@ -72,10 +70,35 @@
applicant.insert()
return applicant
-def _set_up():
- for doctype in ["Employee Onboarding"]:
- frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype))
+def get_job_offer(applicant_name):
+ job_offer = frappe.db.exists('Job Offer', {'job_applicant': applicant_name})
+ if job_offer:
+ return frappe.get_doc('Job Offer', job_offer)
- project = "Employee Onboarding : Test Researcher - test@researcher.com"
- frappe.db.sql("delete from tabProject where name=%s", project)
- frappe.db.sql("delete from tabTask where project=%s", project)
+ job_offer = create_job_offer(job_applicant=applicant_name)
+ job_offer.submit()
+ return job_offer
+
+def create_employee_onboarding():
+ applicant = get_job_applicant()
+ job_offer = get_job_offer(applicant.name)
+
+ onboarding = frappe.new_doc('Employee Onboarding')
+ onboarding.job_applicant = applicant.name
+ onboarding.job_offer = job_offer.name
+ onboarding.company = '_Test Company'
+ onboarding.designation = 'Researcher'
+ onboarding.append('activities', {
+ 'activity_name': 'Assign ID Card',
+ 'role': 'HR User',
+ 'required_for_employee_creation': 1
+ })
+ onboarding.append('activities', {
+ 'activity_name': 'Assign a laptop',
+ 'role': 'HR User'
+ })
+ onboarding.status = 'Pending'
+ onboarding.insert()
+ onboarding.submit()
+
+ return onboarding
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_separation/employee_separation.js b/erpnext/hr/doctype/employee_separation/employee_separation.js
index 9a75c16..d9011b2 100644
--- a/erpnext/hr/doctype/employee_separation/employee_separation.js
+++ b/erpnext/hr/doctype/employee_separation/employee_separation.js
@@ -23,27 +23,13 @@
frappe.set_route('List', 'Task', {project: frm.doc.project});
},__("View"));
}
- if (frm.doc.docstatus === 1 && frm.doc.project) {
- frappe.call({
- method: "erpnext.hr.utils.get_boarding_status",
- args: {
- "project": frm.doc.project
- },
- callback: function(r) {
- if (r.message) {
- frm.set_value('boarding_status', r.message);
- }
- refresh_field("boarding_status");
- }
- });
- }
},
employee_separation_template: function(frm) {
frm.set_value("activities" ,"");
if (frm.doc.employee_separation_template) {
frappe.call({
- method: "erpnext.hr.utils.get_onboarding_details",
+ method: "erpnext.controllers.employee_boarding_controller.get_onboarding_details",
args: {
"parent": frm.doc.employee_separation_template,
"parenttype": "Employee Separation Template"
diff --git a/erpnext/hr/doctype/employee_separation/employee_separation.json b/erpnext/hr/doctype/employee_separation/employee_separation.json
index 7af20988..c10da5c 100644
--- a/erpnext/hr/doctype/employee_separation/employee_separation.json
+++ b/erpnext/hr/doctype/employee_separation/employee_separation.json
@@ -50,11 +50,12 @@
},
{
"allow_on_submit": 1,
+ "default": "Pending",
"fieldname": "boarding_status",
"fieldtype": "Select",
"label": "Status",
- "options": "\nPending\nIn Process\nCompleted",
- "reqd": 1
+ "options": "Pending\nIn Process\nCompleted",
+ "read_only": 1
},
{
"allow_on_submit": 1,
@@ -147,7 +148,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-04-28 15:58:36.020196",
+ "modified": "2021-06-03 18:02:54.007313",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Separation",
diff --git a/erpnext/hr/doctype/employee_separation/employee_separation.py b/erpnext/hr/doctype/employee_separation/employee_separation.py
index b646681..8afee25 100644
--- a/erpnext/hr/doctype/employee_separation/employee_separation.py
+++ b/erpnext/hr/doctype/employee_separation/employee_separation.py
@@ -3,7 +3,7 @@
# For license information, please see license.txt
from __future__ import unicode_literals
-from erpnext.hr.utils import EmployeeBoardingController
+from erpnext.controllers.employee_boarding_controller import EmployeeBoardingController
class EmployeeSeparation(EmployeeBoardingController):
def validate(self):
diff --git a/erpnext/hr/doctype/employee_separation/test_employee_separation.py b/erpnext/hr/doctype/employee_separation/test_employee_separation.py
index 713fcf5..f787d9c 100644
--- a/erpnext/hr/doctype/employee_separation/test_employee_separation.py
+++ b/erpnext/hr/doctype/employee_separation/test_employee_separation.py
@@ -6,21 +6,43 @@
import frappe
import unittest
-test_dependencies = ["Employee Onboarding"]
+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.employee = employee
- separation.company = '_Test Company'
- separation.append('activities', {
- 'activity_name': 'Deactivate Employee',
- 'role': 'HR User'
- })
- separation.boarding_status = 'Pending'
- separation.insert()
- separation.submit()
+ separation = create_employee_separation()
+
self.assertEqual(separation.docstatus, 1)
+ self.assertEqual(separation.boarding_status, 'Pending')
+
+ project = frappe.get_doc('Project', separation.project)
+ project.percent_complete_method = 'Manual'
+ project.status = 'Completed'
+ project.save()
+
+ separation.reload()
+ self.assertEqual(separation.boarding_status, 'Completed')
+
separation.cancel()
- self.assertEqual(separation.project, "")
\ No newline at end of file
+ self.assertEqual(separation.project, '')
+
+ def tearDown(self):
+ for entry in frappe.get_all('Employee Separation'):
+ doc = frappe.get_doc('Employee Separation', entry.name)
+ if doc.docstatus == 1:
+ doc.cancel()
+ doc.delete()
+
+def create_employee_separation():
+ employee = frappe.db.get_value('Employee', {'status': 'Active'})
+ separation = frappe.new_doc('Employee Separation')
+ separation.employee = employee
+ separation.company = '_Test Company'
+ separation.append('activities', {
+ 'activity_name': 'Deactivate Employee',
+ 'role': 'HR User'
+ })
+ separation.boarding_status = 'Pending'
+ separation.insert()
+ separation.submit()
+ return separation
\ No newline at end of file
diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
index 578eccf..96ea686 100644
--- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
@@ -72,7 +72,8 @@
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 - _TC4", do_not_submit=True, taxes=taxes)
+ expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4",
+ do_not_submit=True, taxes=taxes)
expense_claim.submit()
gl_entries = frappe.db.sql("""select account, debit, credit
@@ -82,7 +83,7 @@
self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [
- ['CGST - _TC4',18.0, 0.0],
+ ['Output Tax CGST - _TC4',18.0, 0.0],
[payable_account, 0.0, 218.0],
["Travel Expenses - _TC4", 200.0, 0.0]
])
@@ -145,7 +146,7 @@
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="CGST", account_type="Tax", parent_account=parent_account)
+ account = create_account(company=company_name, account_name="Output Tax CGST", account_type="Tax", parent_account=parent_account)
return {'taxes':[{
"account_head": account,
"rate": 0,
diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js
index b7d34b1..d5f6e5f 100644
--- a/erpnext/hr/doctype/training_event/training_event.js
+++ b/erpnext/hr/doctype/training_event/training_event.js
@@ -20,11 +20,10 @@
frappe.set_route("List", "Training Feedback");
});
}
- }
-});
+ frm.events.set_employee_query(frm);
+ },
-frappe.ui.form.on("Training Event Employee", {
- employee: function (frm) {
+ set_employee_query: function(frm) {
let emp = [];
for (let d in frm.doc.employees) {
if (frm.doc.employees[d].employee) {
@@ -34,9 +33,17 @@
frm.set_query("employee", "employees", function () {
return {
filters: {
- name: ["NOT IN", emp]
+ name: ["NOT IN", emp],
+ status: "Active"
}
};
});
}
});
+
+frappe.ui.form.on("Training Event Employee", {
+ employee: function(frm) {
+ frm.events.set_employee_query(frm);
+ }
+});
+
diff --git a/erpnext/hr/doctype/training_event_employee/training_event_employee.json b/erpnext/hr/doctype/training_event_employee/training_event_employee.json
index 2d313e9..bcb7d5e 100644
--- a/erpnext/hr/doctype/training_event_employee/training_event_employee.json
+++ b/erpnext/hr/doctype/training_event_employee/training_event_employee.json
@@ -19,6 +19,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Employee",
+ "no_copy": 1,
"options": "Employee"
},
{
@@ -68,7 +69,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-05-21 12:41:59.336237",
+ "modified": "2021-07-02 17:20:27.630176",
"modified_by": "Administrator",
"module": "HR",
"name": "Training Event Employee",
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index ebb1734..3cc1a01 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -13,118 +13,6 @@
class DuplicateDeclarationError(frappe.ValidationError): pass
-
-class EmployeeBoardingController(Document):
- '''
- Create the project and the task for the boarding process
- Assign to the concerned person and roles as per the onboarding/separation template
- '''
- def validate(self):
- # remove the task if linked before submitting the form
- if self.amended_from:
- for activity in self.activities:
- activity.task = ''
-
- def on_submit(self):
- # create the project for the given employee onboarding
- project_name = _(self.doctype) + " : "
- if self.doctype == "Employee Onboarding":
- project_name += self.job_applicant
- else:
- project_name += self.employee
-
- project = frappe.get_doc({
- "doctype": "Project",
- "project_name": project_name,
- "expected_start_date": self.date_of_joining if self.doctype == "Employee Onboarding" else self.resignation_letter_date,
- "department": self.department,
- "company": self.company
- }).insert(ignore_permissions=True, ignore_mandatory=True)
-
- self.db_set("project", project.name)
- self.db_set("boarding_status", "Pending")
- self.reload()
- self.create_task_and_notify_user()
-
- def create_task_and_notify_user(self):
- # create the task for the given project and assign to the concerned person
- for activity in self.activities:
- if activity.task:
- continue
-
- task = frappe.get_doc({
- "doctype": "Task",
- "project": self.project,
- "subject": activity.activity_name + " : " + self.employee_name,
- "description": activity.description,
- "department": self.department,
- "company": self.company,
- "task_weight": activity.task_weight
- }).insert(ignore_permissions=True)
- activity.db_set("task", task.name)
-
- users = [activity.user] if activity.user else []
- if activity.role:
- user_list = frappe.db.sql_list('''
- SELECT
- DISTINCT(has_role.parent)
- FROM
- `tabHas Role` has_role
- LEFT JOIN `tabUser` user
- ON has_role.parent = user.name
- WHERE
- has_role.parenttype = 'User'
- AND user.enabled = 1
- AND has_role.role = %s
- ''', activity.role)
- users = unique(users + user_list)
-
- if "Administrator" in users:
- users.remove("Administrator")
-
- # assign the task the users
- if users:
- self.assign_task_to_users(task, users)
-
- def assign_task_to_users(self, task, users):
- for user in users:
- args = {
- 'assign_to': [user],
- 'doctype': task.doctype,
- 'name': task.name,
- 'description': task.description or task.subject,
- 'notify': self.notify_users_by_email
- }
- assign_to.add(args)
-
- def on_cancel(self):
- # delete task project
- for task in frappe.get_all("Task", filters={"project": self.project}):
- frappe.delete_doc("Task", task.name, force=1)
- frappe.delete_doc("Project", self.project, force=1)
- self.db_set('project', '')
- for activity in self.activities:
- activity.db_set("task", "")
-
-
-@frappe.whitelist()
-def get_onboarding_details(parent, parenttype):
- return frappe.get_all("Employee Boarding Activity",
- fields=["activity_name", "role", "user", "required_for_employee_creation", "description", "task_weight"],
- filters={"parent": parent, "parenttype": parenttype},
- order_by= "idx")
-
-@frappe.whitelist()
-def get_boarding_status(project):
- status = 'Pending'
- if project:
- doc = frappe.get_doc('Project', project)
- if flt(doc.percent_complete) > 0.0 and flt(doc.percent_complete) < 100.0:
- status = 'In Process'
- elif flt(doc.percent_complete) == 100.0:
- status = 'Completed'
- return status
-
def set_employee_name(doc):
if doc.employee and not doc.employee_name:
doc.employee_name = frappe.db.get_value("Employee", doc.employee, "employee_name")
diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js
index 28af3a9..f9c201a 100644
--- a/erpnext/loan_management/doctype/loan/loan.js
+++ b/erpnext/loan_management/doctype/loan/loan.js
@@ -28,7 +28,8 @@
frm.set_query("loan_type", function () {
return {
"filters": {
- "docstatus": 1
+ "docstatus": 1,
+ "company": frm.doc.company
}
};
});
diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.js b/erpnext/loan_management/doctype/loan_application/loan_application.js
index 1365274..017026c 100644
--- a/erpnext/loan_management/doctype/loan_application/loan_application.js
+++ b/erpnext/loan_management/doctype/loan_application/loan_application.js
@@ -14,11 +14,18 @@
refresh: function(frm) {
frm.trigger("toggle_fields");
frm.trigger("add_toolbar_buttons");
+ frm.set_query("loan_type", () => {
+ return {
+ filters: {
+ company: frm.doc.company
+ }
+ };
+ });
},
repayment_method: function(frm) {
- frm.doc.repayment_amount = frm.doc.repayment_periods = ""
- frm.trigger("toggle_fields")
- frm.trigger("toggle_required")
+ frm.doc.repayment_amount = frm.doc.repayment_periods = "";
+ frm.trigger("toggle_fields");
+ frm.trigger("toggle_required");
},
toggle_fields: function(frm) {
frm.toggle_enable("repayment_amount", frm.doc.repayment_method=="Repay Fixed Amount per Period")
diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json
index 18bd4ae..68bac8e 100644
--- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json
+++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json
@@ -35,7 +35,9 @@
"no_copy": 1,
"options": "Loan Security Pledge",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fetch_from": "loan_application.applicant",
@@ -45,47 +47,63 @@
"in_standard_filter": 1,
"label": "Applicant",
"options": "applicant_type",
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "loan_security_details_section",
"fieldtype": "Section Break",
- "label": "Loan Security Details"
+ "label": "Loan Security Details",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_3",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "loan",
"fieldtype": "Link",
"label": "Loan",
- "options": "Loan"
+ "options": "Loan",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "loan_application",
"fieldtype": "Link",
"label": "Loan Application",
- "options": "Loan Application"
+ "options": "Loan Application",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "total_security_value",
"fieldtype": "Currency",
"label": "Total Security Value",
"options": "Company:company:default_currency",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "maximum_loan_value",
"fieldtype": "Currency",
"label": "Maximum Loan Value",
"options": "Company:company:default_currency",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "loan_details_section",
"fieldtype": "Section Break",
- "label": "Loan Details"
+ "label": "Loan Details",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "Requested",
@@ -94,37 +112,49 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
- "options": "Requested\nUnpledged\nPledged\nPartially Pledged",
- "read_only": 1
+ "options": "Requested\nUnpledged\nPledged\nPartially Pledged\nCancelled",
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "pledge_time",
"fieldtype": "Datetime",
"label": "Pledge Time",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "securities",
"fieldtype": "Table",
"label": "Securities",
"options": "Pledge",
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_11",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break",
- "label": "Totals"
+ "label": "Totals",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fetch_from": "loan.applicant_type",
@@ -132,35 +162,45 @@
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "more_information_section",
"fieldtype": "Section Break",
- "label": "More Information"
+ "label": "More Information",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
"fieldname": "reference_no",
"fieldtype": "Data",
- "label": "Reference No"
+ "label": "Reference No",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_18",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
"fieldname": "description",
"fieldtype": "Text",
- "label": "Description"
+ "label": "Description",
+ "show_days": 1,
+ "show_seconds": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-19 18:23:16.953305",
+ "modified": "2021-06-29 17:15:16.082256",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Pledge",
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 cbc8376..c390b6c 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
@@ -23,6 +23,12 @@
update_shortfall_status(self.loan, self.total_security_value)
update_loan(self.loan, self.maximum_loan_value)
+ def on_cancel(self):
+ if self.loan:
+ self.db_set("status", "Cancelled")
+ self.db_set("pledge_time", None)
+ update_loan(self.loan, self.maximum_loan_value, cancel=1)
+
def validate_duplicate_securities(self):
security_list = []
for security in self.securities:
@@ -36,7 +42,7 @@
existing_pledge = ''
if self.loan:
- existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan}, ['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'])
@@ -77,8 +83,12 @@
self.total_security_value = total_security_value
self.maximum_loan_value = maximum_loan_value
-def update_loan(loan, maximum_value_against_pledge):
+def update_loan(loan, maximum_value_against_pledge, cancel=0):
maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount'])
- 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))
+ if cancel:
+ 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))
diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js
index 4c31bd0..f19a1b0 100644
--- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js
+++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js
@@ -13,7 +13,7 @@
refresh: function(frm) {
erpnext.hide_company();
- if (frm.doc.customer && frm.doc.docstatus === 1) {
+ if (frm.doc.customer && frm.doc.docstatus === 1 && frm.doc.to_date > frappe.datetime.get_today()) {
frm.add_custom_button(__("Sales Order"), function() {
frappe.model.open_mapped_doc({
method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order",
diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.json b/erpnext/manufacturing/doctype/blanket_order/blanket_order.json
index 0330e5c..a63fc4d 100644
--- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.json
+++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "naming_series:",
"creation": "2018-05-24 07:18:08.256060",
"doctype": "DocType",
@@ -79,6 +80,7 @@
"reqd": 1
},
{
+ "allow_on_submit": 1,
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date",
@@ -129,8 +131,10 @@
"label": "Terms and Conditions Details"
}
],
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
- "modified": "2019-11-18 19:37:37.151686",
+ "links": [],
+ "modified": "2021-06-29 00:30:30.621636",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Blanket Order",
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index c566688..3f50b41 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -83,7 +83,7 @@
if (!frm.doc.__islocal && frm.doc.docstatus<2) {
frm.add_custom_button(__("Update Cost"), function() {
- frm.events.update_cost(frm);
+ frm.events.update_cost(frm, true);
});
frm.add_custom_button(__("Browse BOM"), function() {
frappe.route_options = {
@@ -318,14 +318,15 @@
})
},
- update_cost: function(frm) {
+ update_cost: function(frm, save_doc=false) {
return frappe.call({
doc: frm.doc,
method: "update_cost",
freeze: true,
args: {
update_parent: true,
- from_child_bom:false
+ save: save_doc,
+ from_child_bom: false
},
callback: function(r) {
refresh_field("items");
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index f38d1b9..7e53918 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -36,6 +36,9 @@
"materials_section",
"inspection_required",
"quality_inspection_template",
+ "column_break_31",
+ "bom_level",
+ "section_break_33",
"items",
"scrap_section",
"scrap_items",
@@ -513,6 +516,22 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "column_break_31",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "bom_level",
+ "fieldtype": "Int",
+ "label": "BOM Level",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_33",
+ "fieldtype": "Section Break",
+ "hide_border": 1
}
],
"icon": "fa fa-sitemap",
@@ -520,7 +539,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2021-03-16 12:25:09.081968",
+ "modified": "2021-05-16 12:25:09.081968",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index c58f017..c68198b 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -154,6 +154,7 @@
self.calculate_cost()
self.update_stock_qty()
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
+ self.set_bom_level()
def get_context(self, context):
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
@@ -329,7 +330,7 @@
frappe.get_doc("BOM", bom).update_cost(from_child_bom=True)
if not from_child_bom:
- frappe.msgprint(_("Cost Updated"))
+ frappe.msgprint(_("Cost Updated"), alert=True)
def update_parent_cost(self):
if self.total_cost:
@@ -676,6 +677,19 @@
"""Get a complete tree representation preserving order of child items."""
return BOMTree(self.name)
+ def set_bom_level(self, update=False):
+ levels = []
+
+ self.bom_level = 0
+ for row in self.items:
+ if row.bom_no:
+ levels.append(frappe.get_cached_value("BOM", row.bom_no, "bom_level") or 0)
+
+ if levels:
+ self.bom_level = max(levels) + 1
+
+ if update:
+ self.db_set("bom_level", self.bom_level)
def get_bom_item_rate(args, bom_doc):
if bom_doc.rm_cost_as_per == 'Valuation Rate':
@@ -699,7 +713,8 @@
"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_party": True,
+ "ignore_conversion_rate": True
})
item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
out = frappe._dict()
@@ -759,7 +774,7 @@
item.image,
bom.project,
bom_item.rate,
- bom_item.amount,
+ sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * bom_item.rate * %(qty)s as amount,
item.stock_uom,
item.item_group,
item.allow_alternative_item,
@@ -860,7 +875,7 @@
frappe.form_dict.parent = parent
if frappe.form_dict.parent:
- bom_doc = frappe.get_doc("BOM", frappe.form_dict.parent)
+ 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',
@@ -871,7 +886,7 @@
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'],
+ 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:
@@ -884,6 +899,7 @@
bom_item.parent_bom_qty = bom_doc.quantity
bom_item.expandable = 0 if bom_item.value in ('', None) else 1
+ bom_item.image = frappe.db.escape(bom_item.image)
return bom_items
@@ -1053,13 +1069,6 @@
if barcodes:
or_cond_filters["name"] = ("in", barcodes)
- for cond in get_match_cond(doctype, as_condition=False):
- for key, value in cond.items():
- if key == doctype:
- key = "name"
-
- query_filters[key] = ("in", value)
-
if filters and filters.get("item_code"):
has_variants = frappe.get_cached_value("Item", filters.get("item_code"), "has_variants")
if not has_variants:
@@ -1068,7 +1077,7 @@
if filters and filters.get("is_stock_item"):
query_filters["is_stock_item"] = 1
- return frappe.get_all("Item",
+ 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)
@@ -1100,6 +1109,8 @@
},
'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)
diff --git a/erpnext/manufacturing/doctype/bom/bom_item_preview.html b/erpnext/manufacturing/doctype/bom/bom_item_preview.html
index 6cd5f8c..6088e46 100644
--- a/erpnext/manufacturing/doctype/bom/bom_item_preview.html
+++ b/erpnext/manufacturing/doctype/bom/bom_item_preview.html
@@ -1,13 +1,31 @@
<div style="padding: 15px;">
- {% if data.image %}
- <img class="responsive" src={{ data.image }}>
- <hr style="margin: 15px -15px;">
- {% endif %}
- <h4>
- {{ __("Description") }}
- </h4>
- <div style="padding-top: 10px;">
- {{ data.description }}
+ <div class="row mb-5">
+ <div class="col-md-5" style="max-height: 500px">
+ {% if data.image %}
+ <div class="border image-field " style="overflow: hidden;border-color:#e6e6e6">
+ <img class="responsive" src={{ data.image }}>
+ </div>
+ {% endif %}
+ </div>
+ <div class="col-md-7 h-500">
+ <h4>
+ {{ __("Description") }}
+ </h4>
+ <div style="padding-top: 10px;">
+ {{ data.description }}
+ </div>
+ <hr style="margin: 15px -15px;">
+ <p>
+ {% if data.value %}
+ <a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="#Form/BOM/{{ data.value }}">
+ {{ __("Open BOM {0}", [data.value.bold()]) }}</a>
+ {% endif %}
+ {% if data.item_code %}
+ <a class="btn btn-default btn-xs" href="#Form/Item/{{ data.item_code }}">
+ {{ __("Open Item {0}", [data.item_code.bold()]) }}</a>
+ {% endif %}
+ </p>
+ </div>
</div>
<hr style="margin: 15px -15px;">
<p>
diff --git a/erpnext/manufacturing/doctype/bom/bom_tree.js b/erpnext/manufacturing/doctype/bom/bom_tree.js
index 185b9ed..60fb377 100644
--- a/erpnext/manufacturing/doctype/bom/bom_tree.js
+++ b/erpnext/manufacturing/doctype/bom/bom_tree.js
@@ -64,7 +64,7 @@
if(node.is_root && node.data.value!="BOM") {
frappe.model.with_doc("BOM", node.data.value, function() {
var bom = frappe.model.get_doc("BOM", node.data.value);
- node.data.image = bom.image || "";
+ node.data.image = escape(bom.image) || "";
node.data.description = bom.description || "";
});
}
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 57a5458..c89f7d6 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -8,6 +8,7 @@
from frappe.utils import cstr, flt
from frappe.test_runner import make_test_records
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
+from erpnext.manufacturing.doctype.bom.bom import make_variant_bom
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -248,6 +249,37 @@
for reqd_item, created_item in zip(reqd_order, created_order):
self.assertEqual(reqd_item, created_item.item_code)
+ def test_generated_variant_bom(self):
+ from erpnext.controllers.item_variant import create_variant
+
+ template_item = make_item(
+ "_TestTemplateItem", {"has_variants": 1, "attributes": [{"attribute": "Test Size"},]}
+ )
+ variant = create_variant(template_item.item_code, {"Test Size": "Large"})
+ variant.insert(ignore_if_duplicate=True)
+
+ bom_tree = {
+ template_item.item_code: {
+ "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
+ "ChildPart5": {},
+ }
+ }
+ template_bom = create_nested_bom(bom_tree, prefix="")
+ variant_bom = make_variant_bom(
+ template_bom.name, template_bom.name, variant.item_code, variant_items=[]
+ )
+ variant_bom.save()
+
+ reqd_order = template_bom.get_tree_representation().level_order_traversal()
+ created_order = variant_bom.get_tree_representation().level_order_traversal()
+
+ self.assertEqual(len(reqd_order), len(created_order))
+
+ for reqd_item, created_item in zip(reqd_order, created_order):
+ self.assertEqual(reqd_item.item_code, created_item.item_code)
+ self.assertEqual(reqd_item.qty, created_item.qty)
+ self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty)
+
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})
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 7f8f2ef..69c7f5c 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -192,15 +192,20 @@
"completed_qty": args.get("completed_qty") or 0.0
})
elif args.get("start_time"):
- for name in employees:
- self.append("time_logs", {
- "from_time": get_datetime(args.get("start_time")),
- "employee": name.get('employee'),
- "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 not self.employee:
+ if employees:
+ for name in employees:
+ new_args.employee = name.get('employee')
+ self.add_start_time_log(new_args)
+ else:
+ self.add_start_time_log(new_args)
+
+ if not self.employee and employees:
self.set_employees(employees)
if self.status == "On Hold":
@@ -208,6 +213,9 @@
self.save()
+ def add_start_time_log(self, args):
+ self.append("time_logs", args)
+
def set_employees(self, employees):
for name in employees:
self.append('employee', {
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 450aa04..d198a69 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -4,7 +4,7 @@
frappe.ui.form.on('Production Plan', {
setup: function(frm) {
frm.custom_make_buttons = {
- 'Work Order': 'Work Order',
+ 'Work Order': 'Work Order / Subcontract PO',
'Material Request': 'Material Request',
};
@@ -68,17 +68,13 @@
frm.trigger("show_progress");
if (frm.doc.status !== "Completed") {
- if (frm.doc.po_items && frm.doc.status !== "Closed") {
- frm.add_custom_button(__("Work Order"), ()=> {
- frm.trigger("make_work_order");
- }, __('Create'));
- }
+ frm.add_custom_button(__("Work Order Tree"), ()=> {
+ frappe.set_route('Tree', 'Work Order', {production_plan: frm.doc.name});
+ }, __('View'));
- if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
- frm.add_custom_button(__("Material Request"), ()=> {
- frm.trigger("make_material_request");
- }, __('Create'));
- }
+ frm.add_custom_button(__("Production Plan Summary"), ()=> {
+ frappe.set_route('query-report', 'Production Plan Summary', {production_plan: frm.doc.name});
+ }, __('View'));
if (frm.doc.status === "Closed") {
frm.add_custom_button(__("Re-open"), function() {
@@ -89,6 +85,18 @@
frm.events.close_open_production_plan(frm, true);
}, __("Status"));
}
+
+ if (frm.doc.po_items && frm.doc.status !== "Closed") {
+ frm.add_custom_button(__("Work Order / Subcontract PO"), ()=> {
+ frm.trigger("make_work_order");
+ }, __('Create'));
+ }
+
+ if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
+ frm.add_custom_button(__("Material Request"), ()=> {
+ frm.trigger("make_material_request");
+ }, __('Create'));
+ }
}
}
@@ -233,6 +241,17 @@
});
},
+ get_sub_assembly_items: function(frm) {
+ frappe.call({
+ method: "get_sub_assembly_items",
+ freeze: true,
+ doc: frm.doc,
+ callback: function() {
+ refresh_field("sub_assembly_items");
+ }
+ });
+ },
+
get_items_for_mr: function(frm) {
if (!frm.doc.for_warehouse) {
frappe.throw(__("Select warehouse for material requests"));
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 1c0dde2..8437895 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -32,6 +32,9 @@
"po_items",
"section_break_25",
"prod_plan_references",
+ "section_break_24",
+ "get_sub_assembly_items",
+ "sub_assembly_items",
"material_request_planning",
"include_non_stock_items",
"include_subcontracted_items",
@@ -187,7 +190,7 @@
"depends_on": "get_items_from",
"fieldname": "get_items",
"fieldtype": "Button",
- "label": "Get Items For Work Order"
+ "label": "Get Finished Goods for Manufacture"
},
{
"fieldname": "po_items",
@@ -199,7 +202,7 @@
{
"fieldname": "material_request_planning",
"fieldtype": "Section Break",
- "label": "Material Request Planning"
+ "label": "Material Requirement Planning"
},
{
"default": "1",
@@ -237,12 +240,13 @@
},
{
"fieldname": "section_break_27",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "hide_border": 1
},
{
"fieldname": "mr_items",
"fieldtype": "Table",
- "label": "Material Request Plan Item",
+ "label": "Raw Materials",
"no_copy": 1,
"options": "Material Request Plan Item"
},
@@ -337,13 +341,30 @@
"hidden": 1,
"label": "Production Plan Item Reference",
"options": "Production Plan Item Reference"
+ },
+ {
+ "fieldname": "section_break_24",
+ "fieldtype": "Section Break",
+ "hide_border": 1
+ },
+ {
+ "fieldname": "sub_assembly_items",
+ "fieldtype": "Table",
+ "label": "Sub Assembly Items",
+ "no_copy": 1,
+ "options": "Production Plan Sub Assembly Item"
+ },
+ {
+ "fieldname": "get_sub_assembly_items",
+ "fieldtype": "Button",
+ "label": "Get Sub Assembly Items"
}
],
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-05-24 16:59:03.643211",
+ "modified": "2021-06-28 20:00:33.905114",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 0ede1bd..6a024f2 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -5,10 +5,11 @@
from __future__ import unicode_literals
import frappe, json, copy
from frappe import msgprint, _
-from six import string_types, iteritems
+from six import iteritems
from frappe.model.document import Document
-from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and, now_datetime, ceil
+from frappe.utils import (flt, cint, nowdate, add_days, comma_and, now_datetime,
+ ceil, get_link_to_form, getdate)
from frappe.utils.csvutils import build_csv_response
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_children
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
@@ -349,49 +350,88 @@
@frappe.whitelist()
def make_work_order(self):
- wo_list = []
+ wo_list, po_list = [], []
+ subcontracted_po = {}
+
self.validate_data()
+ self.make_work_order_for_finished_goods(wo_list)
+ self.make_work_order_for_subassembly_items(wo_list, subcontracted_po)
+ 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)
+
+ def make_work_order_for_finished_goods(self, wo_list):
items_data = self.get_production_items()
for key, item in items_data.items():
+ if self.sub_assembly_items:
+ item['use_multi_level_bom'] = 0
+
work_order = self.create_work_order(item)
if work_order:
wo_list.append(work_order)
- if item.get("make_work_order_for_sub_assembly_items"):
- work_orders = self.make_work_order_for_sub_assembly_items(item)
- wo_list.extend(work_orders)
+ def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po):
+ for row in self.sub_assembly_items:
+ if row.type_of_manufacturing == 'Subcontract':
+ subcontracted_po.setdefault(row.supplier, []).append(row)
+ continue
+
+ args = {}
+ self.prepare_args_for_sub_assembly_items(row, args)
+ work_order = self.create_work_order(args)
+ if work_order:
+ wo_list.append(work_order)
+
+ 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.supplier = supplier
+ po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
+ po.is_subcontracted_item = 'Yes'
+ for row in po_list:
+ args = {
+ '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']:
+ args[field] = row.get(field)
+
+ po.append('items', args)
+
+ po.set_missing_values()
+ po.flags.ignore_mandatory = True
+ po.flags.ignore_validate = True
+ po.insert()
+ purchase_orders.append(po.name)
+
+ def show_list_created_message(self, doctype, doc_list=None):
+ if not doc_list:
+ return
frappe.flags.mute_messages = False
+ if doc_list:
+ doc_list = [get_link_to_form(doctype, p) for p in doc_list]
+ msgprint(_("{0} created").format(comma_and(doc_list)))
- if wo_list:
- wo_list = ["""<a href="/app/Form/Work Order/%s" target="_blank">%s</a>""" % \
- (p, p) for p in wo_list]
- msgprint(_("{0} created").format(comma_and(wo_list)))
- else :
- msgprint(_("No Work Orders created"))
+ def prepare_args_for_sub_assembly_items(self, row, args):
+ for field in ["production_item", "item_name", "qty", "fg_warehouse",
+ "description", "bom_no", "stock_uom", "bom_level", "production_plan_item"]:
+ args[field] = row.get(field)
- def make_work_order_for_sub_assembly_items(self, item):
- work_orders = []
- bom_data = {}
-
- get_sub_assembly_items(item.get("bom_no"), bom_data, item.get("qty"))
-
- for key, data in bom_data.items():
- data.update({
- 'qty': data.get("stock_qty"),
- 'production_plan': self.name,
- 'use_multi_level_bom': item.get("use_multi_level_bom"),
- 'company': self.company,
- 'fg_warehouse': item.get("fg_warehouse"),
- 'update_consumed_material_cost_in_project': 0
- })
-
- work_order = self.create_work_order(data)
- if work_order:
- work_orders.append(work_order)
-
- return work_orders
+ args.update({
+ "use_multi_level_bom": 0,
+ "production_plan": self.name,
+ "production_plan_sub_assembly_item": row.name
+ })
def create_work_order(self, item):
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError, get_default_warehouse
@@ -476,9 +516,32 @@
else :
msgprint(_("No material request created"))
+ @frappe.whitelist()
+ def get_sub_assembly_items(self, manufacturing_type=None):
+ self.sub_assembly_items = []
+ for row in self.po_items:
+ bom_data = []
+ 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.save()
+
+ def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
+ bom_data = sorted(bom_data, key = lambda i: i.bom_level)
+
+ for data in bom_data:
+ data.qty = data.stock_qty
+ 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")
+
+ self.append("sub_assembly_items", data)
+
@frappe.whitelist()
def download_raw_materials(doc, warehouses=None):
- if isinstance(doc, string_types):
+ if isinstance(doc, str):
doc = frappe._dict(json.loads(doc))
item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
@@ -660,7 +723,7 @@
@frappe.whitelist()
def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
- if isinstance(row, string_types):
+ if isinstance(row, str):
row = frappe._dict(json.loads(row))
company = frappe.db.escape(company)
@@ -684,8 +747,10 @@
group by item_code, warehouse
""".format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1)
-def get_warehouse_list(warehouses, warehouse_list=[]):
- if isinstance(warehouses, string_types):
+def get_warehouse_list(warehouses):
+ warehouse_list = []
+
+ if isinstance(warehouses, str):
warehouses = json.loads(warehouses)
for row in warehouses:
@@ -695,23 +760,19 @@
else:
warehouse_list.append(row.get("warehouse"))
+ return warehouse_list
+
@frappe.whitelist()
def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None):
- if isinstance(doc, string_types):
+ if isinstance(doc, str):
doc = frappe._dict(json.loads(doc))
- warehouse_list = []
if warehouses:
- get_warehouse_list(warehouses, warehouse_list)
-
- if warehouse_list:
- warehouses = list(set(warehouse_list))
+ 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:
warehouses.remove(doc.get("for_warehouse"))
- warehouse_list = None
-
doc['mr_items'] = []
po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items')
@@ -726,6 +787,9 @@
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')
@@ -857,23 +921,28 @@
# "description": item_details.get("description")
}
-def get_sub_assembly_items(bom_no, bom_data, to_produce_qty):
+def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
data = get_children('BOM', parent = bom_no)
for d in data:
if d.expandable:
- key = (d.name, d.value)
- if key not in bom_data:
- bom_data.setdefault(key, {
- 'stock_qty': 0,
- 'description': d.description,
- 'production_item': d.item_code,
- 'item_name': d.item_name,
- 'stock_uom': d.stock_uom,
- 'uom': d.stock_uom,
- 'bom_no': d.value
- })
+ parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
+ bom_level = (frappe.get_cached_value("BOM", d.value, "bom_level")
+ if d.value else 0)
- bom_item = bom_data.get(key)
- bom_item["stock_qty"] += (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
+ stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
+ bom_data.append(frappe._dict({
+ 'parent_item_code': parent_item_code,
+ 'description': d.description,
+ 'production_item': d.item_code,
+ 'item_name': d.item_name,
+ 'stock_uom': d.stock_uom,
+ 'uom': d.stock_uom,
+ 'bom_no': d.value,
+ 'is_sub_contracted_item': d.is_sub_contracted_item,
+ 'bom_level': bom_level,
+ 'indent': indent,
+ 'stock_qty': stock_qty
+ }))
- get_sub_assembly_items(bom_item.get("bom_no"), bom_data, bom_item["stock_qty"])
+ if d.value:
+ get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent+1)
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py
index 09ec24a..ca597f6 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py
@@ -9,5 +9,9 @@
'label': _('Transactions'),
'items': ['Work Order', 'Material Request']
},
+ {
+ 'label': _('Subcontract'),
+ 'items': ['Purchase Order']
+ },
]
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 768f99e..93e6d7a 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -10,7 +10,7 @@
from erpnext.manufacturing.doctype.production_plan.production_plan import get_sales_orders
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
-from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests
+from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests, get_warehouse_list
class TestProductionPlan(unittest.TestCase):
def setUp(self):
@@ -169,7 +169,7 @@
pln.get_items()
pln.submit()
- self.assertTrue(pln.po_items[0].planned_qty, 3)
+ self.assertTrue(pln.po_items[0].planned_qty, 3)
pln.make_work_order()
work_order = frappe.db.get_value('Work Order', {
@@ -193,10 +193,10 @@
for so_item in so_items:
so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty')
self.assertEqual(so_wo_qty, 0.0)
-
+
latest_plan = frappe.get_doc('Production Plan', pln.name)
latest_plan.cancel()
-
+
def test_pp_to_mr_customer_provided(self):
#Material Request from Production Plan for Customer Provided
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
@@ -236,10 +236,10 @@
pln.append("po_items", {
"item_code": item_code,
"bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}),
- "planned_qty": 3,
- "make_work_order_for_sub_assembly_items": 1
+ "planned_qty": 3
})
+ pln.get_sub_assembly_items('In House')
pln.submit()
pln.make_work_order()
@@ -251,6 +251,27 @@
pln.cancel()
frappe.delete_doc("Production Plan", pln.name)
+ def test_get_warehouse_list_group(self):
+ """Check if required warehouses are returned"""
+ warehouse_json = '[{\"warehouse\":\"_Test Warehouse Group - _TC\"}]'
+
+ warehouses = set(get_warehouse_list(warehouse_json))
+ expected_warehouses = {"_Test Warehouse Group-C1 - _TC", "_Test Warehouse Group-C2 - _TC"}
+
+ missing_warehouse = expected_warehouses - warehouses
+
+ self.assertTrue(len(missing_warehouse) == 0,
+ msg=f"Following warehouses were expected {', '.join(missing_warehouse)}")
+
+ def test_get_warehouse_list_single(self):
+ warehouse_json = '[{\"warehouse\":\"_Test Scrap Warehouse - _TC\"}]'
+
+ warehouses = set(get_warehouse_list(warehouse_json))
+ expected_warehouses = {"_Test Scrap Warehouse - _TC", }
+
+ self.assertEqual(warehouses, expected_warehouses)
+
+
def create_production_plan(**args):
args = frappe._dict(args)
diff --git a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
index 89ab7aa..f829d57 100644
--- a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
+++ b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
@@ -9,18 +9,17 @@
"include_exploded_items",
"item_code",
"bom_no",
- "planned_qty",
"column_break_6",
- "make_work_order_for_sub_assembly_items",
+ "planned_qty",
"warehouse",
"planned_start_date",
"section_break_9",
"pending_qty",
"ordered_qty",
- "produced_qty",
"column_break_17",
"description",
"stock_uom",
+ "produced_qty",
"reference_section",
"sales_order",
"sales_order_item",
@@ -32,11 +31,10 @@
],
"fields": [
{
- "columns": 2,
- "default": "0",
+ "columns": 1,
+ "default": "1",
"fieldname": "include_exploded_items",
"fieldtype": "Check",
- "in_list_view": 1,
"label": "Include Exploded Items"
},
{
@@ -81,13 +79,6 @@
"fieldtype": "Column Break"
},
{
- "default": "0",
- "description": "If enabled, system will create the work order for the exploded items against which BOM is available.",
- "fieldname": "make_work_order_for_sub_assembly_items",
- "fieldtype": "Check",
- "label": "Make Work Order for Sub Assembly Items"
- },
- {
"fieldname": "warehouse",
"fieldtype": "Link",
"in_list_view": 1,
@@ -218,7 +209,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-04-28 19:14:57.772123",
+ "modified": "2021-06-28 18:31:06.822168",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Item",
diff --git a/erpnext/accounts/accounts b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/__init__.py
similarity index 100%
copy from erpnext/accounts/accounts
copy to erpnext/manufacturing/doctype/production_plan_sub_assembly_item/__init__.py
diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
new file mode 100644
index 0000000..657ee35
--- /dev/null
+++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
@@ -0,0 +1,202 @@
+{
+ "actions": [],
+ "creation": "2020-12-27 16:08:36.127199",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "production_item",
+ "item_name",
+ "fg_warehouse",
+ "parent_item_code",
+ "schedule_date",
+ "column_break_3",
+ "qty",
+ "bom_no",
+ "bom_level",
+ "type_of_manufacturing",
+ "supplier",
+ "work_order_details_section",
+ "work_order",
+ "purchase_order",
+ "production_plan_item",
+ "column_break_7",
+ "produced_qty",
+ "received_qty",
+ "indent",
+ "section_break_19",
+ "uom",
+ "stock_uom",
+ "column_break_22",
+ "description"
+ ],
+ "fields": [
+ {
+ "fetch_from": "sub_assembly_item_code.item_name",
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.type_of_manufacturing == \"In House\"",
+ "fieldname": "work_order_details_section",
+ "fieldtype": "Section Break",
+ "label": "Reference"
+ },
+ {
+ "fieldname": "work_order",
+ "fieldtype": "Link",
+ "label": "Work Order",
+ "options": "Work Order",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ },
+ {
+ "columns": 1,
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Required Qty",
+ "read_only": 1
+ },
+ {
+ "fieldname": "purchase_order",
+ "fieldtype": "Link",
+ "label": "Purchase Order",
+ "options": "Purchase Order",
+ "read_only": 1
+ },
+ {
+ "fieldname": "received_qty",
+ "fieldtype": "Float",
+ "label": "Received Qty"
+ },
+ {
+ "fieldname": "bom_no",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Bom No",
+ "options": "BOM"
+ },
+ {
+ "fieldname": "production_plan_item",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Production Plan Item",
+ "read_only": 1
+ },
+ {
+ "fieldname": "parent_item_code",
+ "fieldtype": "Link",
+ "label": "Finished Good",
+ "options": "Item",
+ "read_only": 1
+ },
+ {
+ "columns": 1,
+ "fetch_from": "bom_no.bom_level",
+ "fieldname": "bom_level",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Level (BOM)",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_19",
+ "fieldtype": "Section Break",
+ "label": "Item Details"
+ },
+ {
+ "fieldname": "uom",
+ "fieldtype": "Link",
+ "label": "UOM",
+ "options": "UOM",
+ "read_only": 1
+ },
+ {
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock UOM",
+ "options": "UOM",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_22",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "description",
+ "read_only": 1
+ },
+ {
+ "fieldname": "production_item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Sub Assembly Item Code",
+ "options": "Item",
+ "read_only": 1
+ },
+ {
+ "fieldname": "indent",
+ "fieldtype": "Int",
+ "label": "Indent"
+ },
+ {
+ "fieldname": "fg_warehouse",
+ "fieldtype": "Link",
+ "label": "Target Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "produced_qty",
+ "fieldtype": "Data",
+ "label": "Produced Quantity",
+ "read_only": 1
+ },
+ {
+ "default": "In House",
+ "fieldname": "type_of_manufacturing",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Manufacturing Type",
+ "options": "In House\nSubcontract"
+ },
+ {
+ "fieldname": "supplier",
+ "fieldtype": "Link",
+ "label": "Supplier",
+ "mandatory_depends_on": "eval:doc.type_of_manufacturing == 'Subcontract'",
+ "options": "Supplier"
+ },
+ {
+ "fieldname": "schedule_date",
+ "fieldtype": "Datetime",
+ "in_list_view": 1,
+ "label": "Schedule Date"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-06-28 20:10:56.296410",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Production Plan Sub Assembly Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py
new file mode 100644
index 0000000..6850a2e
--- /dev/null
+++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class ProductionPlanSubAssemblyItem(Document):
+ pass
diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json
index f63d2b9..10cee32 100644
--- a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json
+++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json
@@ -19,6 +19,7 @@
"options": "Operation"
},
{
+ "default": "0",
"description": "Time in mins",
"fieldname": "time_in_mins",
"fieldtype": "Float",
@@ -38,7 +39,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-12-07 18:09:18.005578",
+ "modified": "2021-07-15 16:39:41.635362",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Sub Operation",
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 68de0b2..bf1ccb7 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -513,6 +513,60 @@
work_order1.save()
self.assertEqual(work_order1.operations[0].time_in_mins, 40.0)
+ def test_batch_size_for_fg_item(self):
+ fg_item = "Test Batch Size Item For BOM 3"
+ rm1 = "Test Batch Size Item RM 1 For BOM 3"
+
+ frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0)
+ for item in ["Test Batch Size Item For BOM 3", "Test Batch Size Item RM 1 For BOM 3"]:
+ item_args = {
+ "include_item_in_manufacturing": 1,
+ "is_stock_item": 1
+ }
+
+ if item == fg_item:
+ item_args['has_batch_no'] = 1
+ item_args['create_new_batch'] = 1
+ item_args['batch_number_series'] = 'TBSI3.#####'
+
+ make_item(item, item_args)
+
+ bom_name = frappe.db.get_value("BOM",
+ {"item": fg_item, "is_active": 1, "with_operations": 1}, "name")
+
+ if not bom_name:
+ bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True)
+ bom.save()
+ bom.submit()
+ bom_name = bom.name
+
+ work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1)
+ ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
+ for row in ste1.get('items'):
+ if row.is_finished_item:
+ self.assertEqual(row.item_code, fg_item)
+
+ work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1)
+ frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 1)
+ ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
+ for row in ste1.get('items'):
+ if row.is_finished_item:
+ self.assertEqual(row.item_code, fg_item)
+
+ work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(),
+ qty=30, do_not_save = True)
+ work_order.batch_size = 10
+ work_order.insert()
+ work_order.submit()
+ self.assertEqual(work_order.has_batch_no, 1)
+ ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 30))
+ for row in ste1.get('items'):
+ if row.is_finished_item:
+ self.assertEqual(row.item_code, fg_item)
+ self.assertEqual(row.qty, 10)
+
+ frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0)
+
def test_partial_material_consumption(self):
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1)
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4)
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index 44d76d2..3b56854 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -64,11 +64,16 @@
"description",
"stock_uom",
"column_break2",
+ "references_section",
"material_request",
"material_request_item",
"sales_order_item",
+ "column_break_61",
"production_plan",
"production_plan_item",
+ "production_plan_sub_assembly_item",
+ "parent_work_order",
+ "bom_level",
"product_bundle_item",
"amended_from"
],
@@ -546,17 +551,26 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
- }
+ },
+ {
+ "fieldname": "production_plan_sub_assembly_item",
+ "fieldtype": "Data",
+ "label": "Production Plan Sub-assembly Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ }
],
"icon": "fa fa-cogs",
"idx": 1,
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2021-06-20 15:19:14.902699",
+ "modified": "2021-06-28 16:19:14.902699",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
+ "nsm_parent_field": "parent_work_order",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 180815d..282b5d0 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -239,7 +239,7 @@
self.create_serial_no_batch_no()
def on_submit(self):
- if not self.wip_warehouse:
+ if not self.wip_warehouse and not self.skip_transfer:
frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
if not self.fg_warehouse:
frappe.throw(_("For Warehouse is required before Submit"))
@@ -483,25 +483,24 @@
self.set('operations', [])
- if not self.bom_no:
+ if not self.bom_no or not frappe.get_cached_value('BOM', self.bom_no, 'with_operations'):
return
operations = []
- if not self.use_multi_level_bom:
- bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
- operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
- else:
+
+ if self.use_multi_level_bom:
bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation()
- bom_traversal = list(reversed(bom_tree.level_order_traversal()))
- bom_traversal.append(bom_tree) # add operation on top level item last
+ bom_traversal = reversed(bom_tree.level_order_traversal())
- for d in bom_traversal:
- if d.is_bom:
- operations.extend(_get_operations(d.name, qty=d.exploded_qty))
+ for node in bom_traversal:
+ if node.is_bom:
+ operations.extend(_get_operations(node.name, qty=node.exploded_qty))
- for correct_index, operation in enumerate(operations, start=1):
- operation.idx = correct_index
+ bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
+ operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
+ for correct_index, operation in enumerate(operations, start=1):
+ operation.idx = correct_index
self.set('operations', operations)
self.calculate_time()
@@ -590,6 +589,7 @@
def validate_operation_time(self):
for d in self.operations:
if not d.time_in_mins > 0:
+ print(self.bom_no, self.production_item)
frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation))
def update_required_items(self):
@@ -655,7 +655,7 @@
for item in sorted(item_dict.values(), key=lambda d: d['idx'] or 9999):
self.append('required_items', {
'rate': item.rate,
- 'amount': item.amount,
+ 'amount': item.rate * item.qty,
'operation': item.operation or operation,
'item_code': item.item_code,
'item_name': item.item_name,
diff --git a/erpnext/manufacturing/doctype/work_order/work_order_preview.html b/erpnext/manufacturing/doctype/work_order/work_order_preview.html
new file mode 100644
index 0000000..a4bf93e
--- /dev/null
+++ b/erpnext/manufacturing/doctype/work_order/work_order_preview.html
@@ -0,0 +1,33 @@
+<div style="padding: 15px;">
+ <div class="row mb-5">
+ <div class="col-md-5" style="max-height: 500px">
+ {% if data.image %}
+ <div class="border image-field " style="overflow: hidden;border-color:#e6e6e6">
+ <img class="responsive" src={{ data.image }}>
+ </div>
+ {% endif %}
+ </div>
+ <div class="col-md-7 h-500">
+ <div style="padding-top: 10px;">
+ <b> Status </b> {{ data.status }}
+ </div>
+ <div style="padding-top: 10px;">
+ <b> Qty to Produce </b> {{ data.qty }}
+ </div>
+ <div style="padding-top: 10px;">
+ <b> Produced Qty </b> {{ data.produced_qty }}
+ </div>
+ <hr style="margin: 15px -15px;">
+ <p>
+ {% if data.value %}
+ <a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="#Form/Work Order/{{ data.value }}">
+ {{ __("Open Work Order {0}", [data.value.bold()]) }}</a>
+ {% endif %}
+ {% if data.item_code %}
+ <a class="btn btn-default btn-xs" href="#Form/Item/{{ data.item_code }}">
+ {{ __("Open Item {0}", [data.item_code.bold()]) }}</a>
+ {% endif %}
+ </p>
+ </div>
+ </div>
+</div>
\ No newline at end of file
diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
index 48907ad..858b554 100644
--- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
+++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
@@ -20,17 +20,20 @@
fields= ['qty','bom_no','qty','scrap','item_code','item_name','description','uom'])
for item in exploded_items:
+ print(item.bom_no, indent)
item["indent"] = indent
data.append({
'item_code': item.item_code,
'item_name': item.item_name,
'indent': indent,
+ 'bom_level': (frappe.get_cached_value("BOM", item.bom_no, "bom_level")
+ if item.bom_no else ""),
'bom': item.bom_no,
'qty': item.qty * qty,
'uom': item.uom,
'description': item.description,
'scrap': item.scrap
- })
+ })
if item.bom_no:
get_exploded_items(item.bom_no, data, indent=indent+1, qty=item.qty)
@@ -69,6 +72,12 @@
"width": 100
},
{
+ "label": "BOM Level",
+ "fieldtype": "Data",
+ "fieldname": "bom_level",
+ "width": 100
+ },
+ {
"label": "Standard Description",
"fieldtype": "data",
"fieldname": "description",
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
index 1c6758e..ed8b939 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
@@ -70,12 +70,12 @@
ON bom_item.item_code = ledger.item_code
{conditions}
WHERE
- bom_item.parent = '{bom}' and bom_item.parenttype='BOM'
+ bom_item.parent = {bom} and bom_item.parenttype='BOM'
GROUP BY bom_item.item_code""".format(
qty_field=qty_field,
table=table,
conditions=conditions,
- bom=bom,
+ bom=frappe.db.escape(bom),
qty_to_produce=qty_to_produce or 1)
)
diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js
index bd68db1..cb771e4 100644
--- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js
+++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js
@@ -68,6 +68,18 @@
get_data: function(txt) {
return frappe.db.get_link_options('Item', txt);
}
+ },
+ {
+ label: __("Workstation"),
+ fieldname: "workstation",
+ fieldtype: "Link",
+ options: "Workstation"
+ },
+ {
+ label: __("Operation"),
+ fieldname: "operation",
+ fieldtype: "Link",
+ options: "Operation"
}
]
};
diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.json b/erpnext/manufacturing/report/job_card_summary/job_card_summary.json
index 9f08fc3..ecf2b74 100644
--- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.json
+++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.json
@@ -1,14 +1,16 @@
{
- "add_total_row": 0,
+ "add_total_row": 1,
+ "columns": [],
"creation": "2020-04-20 12:00:21.436619",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
+ "filters": [],
"idx": 0,
"is_standard": "Yes",
- "letter_head": "Gadgets International",
- "modified": "2020-04-20 12:00:21.436619",
+ "letter_head": "",
+ "modified": "2020-12-30 11:49:21.713561",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Summary",
diff --git a/erpnext/accounts/accounts b/erpnext/manufacturing/report/production_plan_summary/__init__.py
similarity index 100%
copy from erpnext/accounts/accounts
copy to erpnext/manufacturing/report/production_plan_summary/__init__.py
diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js
new file mode 100644
index 0000000..59396fe
--- /dev/null
+++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js
@@ -0,0 +1,32 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Production Plan Summary"] = {
+ "filters": [
+ {
+ fieldname: "production_plan",
+ label: __("Production Plan"),
+ fieldtype: "Link",
+ options: "Production Plan",
+ reqd: 1,
+ get_query: function() {
+ return {
+ filters: {
+ "docstatus": 1
+ }
+ };
+ }
+ }
+ ],
+ "formatter": function(value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+
+ if (column.fieldname == "document_name") {
+ var color = data.pending_qty > 0 ? 'red': 'green';
+ value = `<a style='color:${color}' href="#Form/${data['document_type']}/${data['document_name']}" data-doctype="${data['document_type']}">${data['document_name']}</a>`;
+ }
+
+ return value;
+ },
+};
diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.json b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.json
new file mode 100644
index 0000000..33aca21
--- /dev/null
+++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.json
@@ -0,0 +1,26 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2020-12-27 11:43:39.781793",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2020-12-27 11:43:42.677584",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Production Plan Summary",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Production Plan",
+ "report_name": "Production Plan Summary",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Manufacturing User"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py
new file mode 100644
index 0000000..81b1791
--- /dev/null
+++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py
@@ -0,0 +1,136 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.utils import flt
+
+def execute(filters=None):
+ columns, data = [], []
+ data = get_data(filters)
+ columns = get_column(filters)
+
+ return columns, data
+
+def get_data(filters):
+ data = []
+
+ order_details = {}
+ get_work_order_details(filters, order_details)
+ get_purchase_order_details(filters, order_details)
+ get_production_plan_item_details(filters, data, order_details)
+
+ return data
+
+def get_production_plan_item_details(filters, data, order_details):
+ itemwise_indent = {}
+
+ production_plan_doc = frappe.get_cached_doc("Production Plan", filters.get("production_plan"))
+ for row in production_plan_doc.po_items:
+ work_order = frappe.get_cached_value("Work Order", {"production_plan_item": row.name,
+ "bom_no": row.bom_no, "production_item": row.item_code}, "name")
+
+ if row.item_code not in itemwise_indent:
+ itemwise_indent.setdefault(row.item_code, {})
+
+ data.append({
+ "indent": 0,
+ "item_code": row.item_code,
+ "item_name": frappe.get_cached_value("Item", row.item_code, "item_name"),
+ "qty": row.planned_qty,
+ "document_type": "Work Order",
+ "document_name": work_order,
+ "bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"),
+ "produced_qty": order_details.get((work_order, row.item_code)).get("produced_qty"),
+ "pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code)).get("produced_qty"))
+ })
+
+ get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details)
+
+def get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details):
+ for item in production_plan_doc.sub_assembly_items:
+ if row.name == item.production_plan_item:
+ subcontracted_item = (item.type_of_manufacturing == 'Subcontract')
+
+ if subcontracted_item:
+ docname = frappe.get_cached_value("Purchase Order Item",
+ {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "parent")
+ else:
+ docname = frappe.get_cached_value("Work Order",
+ {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "name")
+
+ data.append({
+ "indent": 1,
+ "item_code": item.production_item,
+ "item_name": item.item_name,
+ "qty": item.qty,
+ "document_type": "Work Order" if not subcontracted_item else "Purchase Order",
+ "document_name": docname,
+ "bom_level": item.bom_level,
+ "produced_qty": order_details.get((docname, item.production_item)).get("produced_qty"),
+ "pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item)).get("produced_qty"))
+ })
+
+def get_work_order_details(filters, order_details):
+ for row in frappe.get_all("Work Order", filters = {"production_plan": filters.get("production_plan")},
+ fields=["name", "produced_qty", "production_plan", "production_item"]):
+ order_details.setdefault((row.name, row.production_item), row)
+
+def get_purchase_order_details(filters, order_details):
+ for row in frappe.get_all("Purchase Order Item", filters = {"production_plan": filters.get("production_plan")},
+ fields=["parent", "received_qty as produced_qty", "item_code"]):
+ order_details.setdefault((row.parent, row.item_code), row)
+
+def get_column(filters):
+ return [
+ {
+ "label": "Finished Good",
+ "fieldtype": "Link",
+ "fieldname": "item_code",
+ "width": 300,
+ "options": "Item"
+ },
+ {
+ "label": "Item Name",
+ "fieldtype": "data",
+ "fieldname": "item_name",
+ "width": 100
+ },
+ {
+ "label": "Document Type",
+ "fieldtype": "Link",
+ "fieldname": "document_type",
+ "width": 150,
+ "options": "DocType"
+ },
+ {
+ "label": "Document Name",
+ "fieldtype": "Dynamic Link",
+ "fieldname": "document_name",
+ "width": 150
+ },
+ {
+ "label": "BOM Level",
+ "fieldtype": "Int",
+ "fieldname": "bom_level",
+ "width": 100
+ },
+ {
+ "label": "Order Qty",
+ "fieldtype": "Float",
+ "fieldname": "qty",
+ "width": 120
+ },
+ {
+ "label": "Received Qty",
+ "fieldtype": "Float",
+ "fieldname": "produced_qty",
+ "width": 160
+ },
+ {
+ "label": "Pending Qty",
+ "fieldtype": "Float",
+ "fieldname": "pending_qty",
+ "width": 110
+ }
+ ]
diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py
index fb047b2..612dad0 100644
--- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py
+++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py
@@ -19,7 +19,7 @@
return columns, data, None, chart_data
def get_data(filters):
- query_filters = {"docstatus": 1}
+ query_filters = {"docstatus": ("<", 2)}
fields = ["name", "status", "sales_order", "production_item", "qty", "produced_qty",
"planned_start_date", "planned_end_date", "actual_start_date", "actual_end_date", "lead_time"]
@@ -62,7 +62,8 @@
"Not Started": 0,
"In Process": 0,
"Stopped": 0,
- "Completed": 0
+ "Completed": 0,
+ "Draft": 0
}
for d in data:
diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json
index f190cfa..7c1baf1 100644
--- a/erpnext/non_profit/doctype/member/member.json
+++ b/erpnext/non_profit/doctype/member/member.json
@@ -26,7 +26,7 @@
"razorpay_details_section",
"subscription_id",
"customer_id",
- "subscription_activated",
+ "subscription_status",
"column_break_21",
"subscription_start",
"subscription_end"
@@ -152,12 +152,6 @@
"fieldtype": "Column Break"
},
{
- "default": "0",
- "fieldname": "subscription_activated",
- "fieldtype": "Check",
- "label": "Subscription Activated"
- },
- {
"fieldname": "subscription_start",
"fieldtype": "Date",
"label": "Subscription Start "
@@ -166,11 +160,17 @@
"fieldname": "subscription_end",
"fieldtype": "Date",
"label": "Subscription End"
+ },
+ {
+ "fieldname": "subscription_status",
+ "fieldtype": "Select",
+ "label": "Subscription Status",
+ "options": "\nActive\nHalted"
}
],
"image_field": "image",
"links": [],
- "modified": "2020-11-09 12:12:10.174647",
+ "modified": "2021-07-11 14:27:26.368039",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Member",
diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py
index 30be585..67828d6 100644
--- a/erpnext/non_profit/doctype/member/member.py
+++ b/erpnext/non_profit/doctype/member/member.py
@@ -84,7 +84,9 @@
"email_id": user_details.email,
"pan_number": user_details.pan or None,
"membership_type": user_details.plan_id,
- "subscription_id": user_details.subscription_id or None
+ "customer_id": user_details.customer_id or None,
+ "subscription_id": user_details.subscription_id or None,
+ "subscription_status": user_details.subscription_status or ""
})
member.insert(ignore_permissions=True)
diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py
index e8ae618..b584116 100644
--- a/erpnext/non_profit/doctype/membership/membership.py
+++ b/erpnext/non_profit/doctype/membership/membership.py
@@ -196,11 +196,14 @@
return invoice
-def get_member_based_on_subscription(subscription_id, email):
- members = frappe.get_all("Member", filters={
- "subscription_id": subscription_id,
- "email_id": email
- }, order_by="creation desc")
+def get_member_based_on_subscription(subscription_id, email=None, customer_id=None):
+ filters = {"subscription_id": subscription_id}
+ if email:
+ filters.update({"email_id": email})
+ if customer_id:
+ filters.update({"customer_id": customer_id})
+
+ members = frappe.get_all("Member", filters=filters, order_by="creation desc")
try:
return frappe.get_doc("Member", members[0]["name"])
@@ -209,8 +212,6 @@
def verify_signature(data, endpoint="Membership"):
- if frappe.flags.in_test or os.environ.get("CI"):
- return True
signature = frappe.request.headers.get("X-Razorpay-Signature")
settings = frappe.get_doc("Non Profit Settings")
@@ -225,16 +226,7 @@
@frappe.whitelist(allow_guest=True)
def trigger_razorpay_subscription(*args, **kwargs):
data = frappe.request.get_data(as_text=True)
- try:
- verify_signature(data)
- except Exception as e:
- log = frappe.log_error(e, "Membership Webhook Verification Error")
- notify_failure(log)
- return { "status": "Failed", "reason": e}
-
- if isinstance(data, six.string_types):
- data = json.loads(data)
- data = frappe._dict(data)
+ data = process_request_data(data)
subscription = data.payload.get("subscription", {}).get("entity", {})
subscription = frappe._dict(subscription)
@@ -281,7 +273,7 @@
# Update membership values
member.subscription_start = datetime.fromtimestamp(subscription.start_at)
member.subscription_end = datetime.fromtimestamp(subscription.end_at)
- member.subscription_activated = 1
+ member.subscription_status = "Active"
member.flags.ignore_mandatory = True
member.save()
@@ -294,9 +286,67 @@
message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id)
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
notify_failure(log)
- return { "status": "Failed", "reason": e}
+ return {"status": "Failed", "reason": e}
- return { "status": "Success" }
+ return {"status": "Success"}
+
+
+@frappe.whitelist(allow_guest=True)
+def update_halted_razorpay_subscription(*args, **kwargs):
+ """
+ When all retries have been exhausted, Razorpay moves the subscription to the halted state.
+ The customer has to manually retry the charge or change the card linked to the subscription,
+ for the subscription to move back to the active state.
+ """
+ if frappe.request:
+ data = frappe.request.get_data(as_text=True)
+ data = process_request_data(data)
+ elif frappe.flags.in_test:
+ data = kwargs.get("data")
+ data = frappe._dict(data)
+ else:
+ return
+
+ if not data.event == "subscription.halted":
+ return
+
+ subscription = data.payload.get("subscription", {}).get("entity", {})
+ subscription = frappe._dict(subscription)
+
+ try:
+ member = get_member_based_on_subscription(subscription.id, customer_id=subscription.customer_id)
+ if not member:
+ frappe.throw(_("Member with Razorpay Subscription ID {0} not found").format(subscription.id))
+
+ member.subscription_status = "Halted"
+ member.flags.ignore_mandatory = True
+ member.save()
+
+ if subscription.get("notes"):
+ member = get_additional_notes(member, subscription)
+
+ except Exception as e:
+ message = "{0}\n\n{1}".format(e, frappe.get_traceback())
+ log = frappe.log_error(message, _("Error updating halted status for member {0}").format(member.name))
+ notify_failure(log)
+ return {"status": "Failed", "reason": e}
+
+ return {"status": "Success"}
+
+
+def process_request_data(data):
+ try:
+ verify_signature(data)
+ except Exception as e:
+ log = frappe.log_error(e, "Membership Webhook Verification Error")
+ notify_failure(log)
+ return {"status": "Failed", "reason": e}
+
+ if isinstance(data, six.string_types):
+ data = json.loads(data)
+ data = frappe._dict(data)
+
+ return data
def get_company_for_memberships():
@@ -362,4 +412,4 @@
`tabMembership` SET `status` = 'Expired'
WHERE
`status` not in ('Cancelled') AND `to_date` < %s
- """, (nowdate()))
\ No newline at end of file
+ """, (nowdate()))
diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py
index 31da792..0f5a9be 100644
--- a/erpnext/non_profit/doctype/membership/test_membership.py
+++ b/erpnext/non_profit/doctype/membership/test_membership.py
@@ -6,6 +6,7 @@
import frappe
import erpnext
from erpnext.non_profit.doctype.member.member import create_member
+from erpnext.non_profit.doctype.membership.membership import update_halted_razorpay_subscription
from frappe.utils import nowdate, add_months
class TestMembership(unittest.TestCase):
@@ -13,11 +14,16 @@
plan = setup_membership()
# make test member
- self.member_doc = create_member(frappe._dict({
- 'fullname': "_Test_Member",
- 'email': "_test_member_erpnext@example.com",
- 'plan_id': plan.name
- }))
+ self.member_doc = create_member(
+ frappe._dict({
+ "fullname": "_Test_Member",
+ "email": "_test_member_erpnext@example.com",
+ "plan_id": plan.name,
+ "subscription_id": "sub_DEX6xcJ1HSW4CR",
+ "customer_id": "cust_C0WlbKhp3aLA7W",
+ "subscription_status": "Active"
+ })
+ )
self.member_doc.make_customer_and_link()
self.member = self.member_doc.name
@@ -51,6 +57,20 @@
"to_date": add_months(nowdate(), 3),
})
+ def test_halted_memberships(self):
+ make_membership(self.member, {
+ "from_date": add_months(nowdate(), 2),
+ "to_date": add_months(nowdate(), 3)
+ })
+
+ self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Active")
+ payload = get_subscription_payload()
+ update_halted_razorpay_subscription(data=payload)
+ self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Halted")
+
+ def tearDown(self):
+ frappe.db.rollback()
+
def set_config(key, value):
frappe.db.set_value("Non Profit Settings", None, key, value)
@@ -115,4 +135,28 @@
else:
plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
- return plan
\ No newline at end of file
+ return plan
+
+def get_subscription_payload():
+ return {
+ "entity": "event",
+ "account_id": "acc_BFQ7uQEaa7j2z7",
+ "event": "subscription.halted",
+ "contains": [
+ "subscription"
+ ],
+ "payload": {
+ "subscription": {
+ "entity": {
+ "id": "sub_DEX6xcJ1HSW4CR",
+ "entity": "subscription",
+ "plan_id": "_rzpy_test_milythm",
+ "customer_id": "cust_C0WlbKhp3aLA7W",
+ "status": "halted",
+ "notes": {
+ "Important": "Notes for Internal Reference"
+ },
+ }
+ }
+ }
+ }
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 986b0c5..a029627a 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -290,5 +290,10 @@
erpnext.patches.v13_0.set_training_event_attendance
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
+erpnext.patches.v13_0.update_response_by_variance
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.update_job_card_details
+erpnext.patches.v13_0.update_level_in_bom #1234sswef
+erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
+erpnext.patches.v13_0.update_subscription_status_in_memberships
+erpnext.patches.v13_0.update_amt_in_work_order_required_items
diff --git a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py
new file mode 100644
index 0000000..d7ad1fc
--- /dev/null
+++ b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py
@@ -0,0 +1,111 @@
+# Copyright (c) 2020, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from frappe.utils import cstr, flt, cint
+from erpnext.stock.stock_ledger import make_sl_entries
+from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
+
+def execute():
+ if not frappe.db.has_column('Work Order', 'has_batch_no'):
+ return
+
+ frappe.reload_doc('manufacturing', 'doctype', 'manufacturing_settings')
+ if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')):
+ return
+
+ frappe.reload_doc('manufacturing', 'doctype', 'work_order')
+ filters = {
+ 'docstatus': 1,
+ 'produced_qty': ('>', 0),
+ 'creation': ('>=', '2021-06-29 00:00:00'),
+ 'has_batch_no': 1
+ }
+
+ fields = ['name', 'production_item']
+
+ work_orders = [d.name for d in frappe.get_all('Work Order', filters = filters, fields=fields)]
+
+ if not work_orders:
+ return
+
+ repost_stock_entries = []
+ stock_entries = frappe.db.sql_list('''
+ SELECT
+ se.name
+ FROM
+ `tabStock Entry` se
+ WHERE
+ se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in {work_orders}
+ and not exists(
+ select name from `tabStock Entry Detail` sed where sed.parent = se.name and sed.is_finished_item = 1
+ )
+ Order BY
+ se.posting_date, se.posting_time
+ '''.format(work_orders=tuple(work_orders)))
+
+ if stock_entries:
+ print('Length of stock entries', len(stock_entries))
+
+ for stock_entry in stock_entries:
+ doc = frappe.get_doc('Stock Entry', stock_entry)
+ doc.set_work_order_details()
+ doc.load_items_from_bom()
+ doc.calculate_rate_and_amount()
+ set_expense_account(doc)
+ doc.make_batches('t_warehouse')
+
+ if doc.docstatus == 0:
+ doc.save()
+ else:
+ repost_stock_entry(doc)
+ repost_stock_entries.append(doc)
+
+ for repost_doc in repost_stock_entries:
+ repost_future_sle_and_gle(repost_doc)
+
+def set_expense_account(doc):
+ for row in doc.items:
+ if row.is_finished_item and not row.expense_account:
+ row.expense_account = frappe.get_cached_value('Company', doc.company, 'stock_adjustment_account')
+
+def repost_stock_entry(doc):
+ doc.db_update()
+ for child_row in doc.items:
+ if child_row.is_finished_item:
+ child_row.db_update()
+
+ sl_entries = []
+ finished_item_row = doc.get_finished_item_row()
+ get_sle_for_target_warehouse(doc, sl_entries, finished_item_row)
+
+ if sl_entries:
+ try:
+ make_sl_entries(sl_entries, True)
+ except Exception:
+ print(f'SLE entries not posted for the stock entry {doc.name}')
+ traceback = frappe.get_traceback()
+ frappe.log_error(traceback)
+
+def get_sle_for_target_warehouse(doc, sl_entries, finished_item_row):
+ for d in doc.get('items'):
+ if cstr(d.t_warehouse) and finished_item_row and d.name == finished_item_row.name:
+ sle = doc.get_sl_entries(d, {
+ "warehouse": cstr(d.t_warehouse),
+ "actual_qty": flt(d.transfer_qty),
+ "incoming_rate": flt(d.valuation_rate)
+ })
+
+ sle.recalculate_rate = 1
+ sl_entries.append(sle)
+
+def repost_future_sle_and_gle(doc):
+ args = frappe._dict({
+ "posting_date": doc.posting_date,
+ "posting_time": doc.posting_time,
+ "voucher_type": doc.doctype,
+ "voucher_no": doc.name,
+ "company": doc.company
+ })
+
+ create_repost_item_valuation_entry(args)
diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py
index fa1dfed..41c51c3 100644
--- a/erpnext/patches/v13_0/rename_issue_doctype_fields.py
+++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py
@@ -37,7 +37,7 @@
if frappe.db.exists('DocType', 'Opportunity'):
opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc')
- frappe.reload_doc('crm', 'doctype', 'opportunity')
+ frappe.reload_doctype('Opportunity', force=True)
rename_field('Opportunity', 'mins_to_first_response', 'first_response_time')
# change fieldtype to duration
diff --git a/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py
new file mode 100644
index 0000000..eae5ff6
--- /dev/null
+++ b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py
@@ -0,0 +1,10 @@
+import frappe
+
+def execute():
+ """ Correct amount in child table of required items table."""
+
+ frappe.reload_doc("manufacturing", "doctype", "work_order")
+ frappe.reload_doc("manufacturing", "doctype", "work_order_item")
+
+ frappe.db.sql("""UPDATE `tabWork Order Item` SET amount = rate * required_qty""")
+
diff --git a/erpnext/patches/v13_0/update_level_in_bom.py b/erpnext/patches/v13_0/update_level_in_bom.py
new file mode 100644
index 0000000..0d03c42
--- /dev/null
+++ b/erpnext/patches/v13_0/update_level_in_bom.py
@@ -0,0 +1,30 @@
+# Copyright (c) 2020, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ for document in ["bom", "bom_item", "bom_explosion_item"]:
+ frappe.reload_doc('manufacturing', 'doctype', document)
+
+ frappe.db.sql(" update `tabBOM` set bom_level = 0 where docstatus = 1")
+
+ 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, '')!='')""")
+
+ count = 0
+ while(count < len(bom_list)):
+ for parent_bom in get_parent_boms(bom_list[count]):
+ bom_doc = frappe.get_cached_doc("BOM", parent_bom)
+ bom_doc.set_bom_level(update=True)
+ bom_list.append(parent_bom)
+ count += 1
+
+def get_parent_boms(bom_no):
+ 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)
diff --git a/erpnext/patches/v13_0/update_response_by_variance.py b/erpnext/patches/v13_0/update_response_by_variance.py
new file mode 100644
index 0000000..ef4d976
--- /dev/null
+++ b/erpnext/patches/v13_0/update_response_by_variance.py
@@ -0,0 +1,31 @@
+# Copyright (c) 2020, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ if frappe.db.exists('DocType', 'Issue') and frappe.db.count('Issue'):
+ invalid_issues = frappe.get_all('Issue', {
+ 'first_responded_on': ['is', 'set'],
+ 'response_by_variance': ['<', 0]
+ }, ["name", "response_by_variance", "timestampdiff(Second, `first_responded_on`, `response_by`) as variance"])
+
+ # issues which has response_by_variance set as -ve
+ # but diff between first_responded_on & response_by is +ve i.e SLA isn't failed
+ invalid_issues = [d for d in invalid_issues if d.get('variance') > 0]
+
+ for issue in invalid_issues:
+ frappe.db.set_value('Issue', issue.get('name'), 'response_by_variance', issue.get('variance'), update_modified=False)
+
+ invalid_issues = frappe.get_all('Issue', {
+ 'resolution_date': ['is', 'set'],
+ 'resolution_by_variance': ['<', 0]
+ }, ["name", "resolution_by_variance", "timestampdiff(Second, `resolution_date`, `resolution_by`) as variance"])
+
+ # issues which has resolution_by_variance set as -ve
+ # but diff between resolution_date & resolution_by is +ve i.e SLA isn't failed
+ invalid_issues = [d for d in invalid_issues if d.get('variance') > 0]
+
+ for issue in invalid_issues:
+ frappe.db.set_value('Issue', issue.get('name'), 'resolution_by_variance', issue.get('variance'), update_modified=False)
diff --git a/erpnext/patches/v13_0/update_subscription_status_in_memberships.py b/erpnext/patches/v13_0/update_subscription_status_in_memberships.py
new file mode 100644
index 0000000..28e650e
--- /dev/null
+++ b/erpnext/patches/v13_0/update_subscription_status_in_memberships.py
@@ -0,0 +1,9 @@
+import frappe
+
+def execute():
+ if frappe.db.exists('DocType', 'Member'):
+ frappe.reload_doc('Non Profit', 'doctype', 'Member')
+
+ if frappe.db.has_column('Member', 'subscription_activated'):
+ frappe.db.sql('UPDATE `tabMember` SET subscription_status = "Active" WHERE subscription_activated = 1')
+ frappe.db.sql_ddl('ALTER table `tabMember` DROP COLUMN subscription_activated')
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 36e728f..13cc423 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -117,7 +117,6 @@
Creates salary slip for selected employees if already not created
"""
self.check_permission('write')
- self.created = 1
employees = [emp.employee for emp in self.employees]
if employees:
args = frappe._dict({
@@ -686,7 +685,7 @@
if filters.start_date and filters.end_date:
employee_list = get_employee_list(filters)
- emp = filters.get('employees')
+ emp = filters.get('employees') or []
include_employees = [employee.employee for employee in employee_list if employee.employee not in emp]
filters.pop('start_date')
filters.pop('end_date')
diff --git a/erpnext/payroll/doctype/salary_component/salary_component.js b/erpnext/payroll/doctype/salary_component/salary_component.js
index dbf7514..e9e6f81 100644
--- a/erpnext/payroll/doctype/salary_component/salary_component.js
+++ b/erpnext/payroll/doctype/salary_component/salary_component.js
@@ -4,11 +4,18 @@
frappe.ui.form.on('Salary Component', {
setup: function(frm) {
frm.set_query("account", "accounts", function(doc, cdt, cdn) {
- var d = locals[cdt][cdn];
+ let d = frappe.get_doc(cdt, cdn);
+
+ let root_type = "Liability";
+ if (frm.doc.type == "Deduction") {
+ root_type = "Expense";
+ }
+
return {
filters: {
"is_group": 0,
- "company": d.company
+ "company": d.company,
+ "root_type": root_type
}
};
});
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index c55bec8..f82b0d5 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -1088,6 +1088,7 @@
"applicant": self.employee,
"docstatus": 1,
"repay_from_salary": 1,
+ "company": self.company
})
def make_loan_repayment_entry(self):
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index ce88cc3..d730fcf 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -481,15 +481,19 @@
if not salary_structure:
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
+ employee = frappe.db.get_value("Employee",
+ {
+ "user_id": user
+ },
+ ["name", "company", "employee_name"],
+ as_dict=True)
- employee = frappe.db.get_value("Employee", {"user_id": user})
- salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee)
+ salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee.name, company=employee.company)
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})
if not salary_slip_name:
- salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee)
- salary_slip.employee_name = frappe.get_value("Employee",
- {"name":frappe.db.get_value("Employee", {"user_id": user})}, "employee_name")
+ salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee.name)
+ salary_slip.employee_name = employee.employee_name
salary_slip.payroll_frequency = payroll_frequency
salary_slip.posting_date = nowdate()
salary_slip.insert()
diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
index e7d123c..374dd7e 100644
--- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
@@ -119,26 +119,25 @@
if test_tax:
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure))
- if not frappe.db.exists('Salary Structure', salary_structure):
- details = {
- "doctype": "Salary Structure",
- "name": salary_structure,
- "company": company or erpnext.get_default_company(),
- "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
- "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
- "payroll_frequency": payroll_frequency,
- "payment_account": get_random("Account", filters={'account_currency': currency}),
- "currency": currency
- }
- if other_details and isinstance(other_details, dict):
- details.update(other_details)
- salary_structure_doc = frappe.get_doc(details)
- salary_structure_doc.insert()
- if not dont_submit:
- salary_structure_doc.submit()
+ if frappe.db.exists("Salary Structure", salary_structure):
+ frappe.db.delete("Salary Structure", salary_structure)
- else:
- salary_structure_doc = frappe.get_doc("Salary Structure", salary_structure)
+ details = {
+ "doctype": "Salary Structure",
+ "name": salary_structure,
+ "company": company or erpnext.get_default_company(),
+ "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
+ "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
+ "payroll_frequency": payroll_frequency,
+ "payment_account": get_random("Account", filters={"account_currency": currency}),
+ "currency": currency
+ }
+ if other_details and isinstance(other_details, dict):
+ details.update(other_details)
+ salary_structure_doc = frappe.get_doc(details)
+ salary_structure_doc.insert()
+ if not dont_submit:
+ salary_structure_doc.submit()
filters = {'employee':employee, 'docstatus': 1}
if not from_date and payroll_period:
diff --git a/erpnext/payroll/report/bank_remittance/bank_remittance.py b/erpnext/payroll/report/bank_remittance/bank_remittance.py
index 500543c..05a5366 100644
--- a/erpnext/payroll/report/bank_remittance/bank_remittance.py
+++ b/erpnext/payroll/report/bank_remittance/bank_remittance.py
@@ -95,6 +95,7 @@
"amount": salary.net_pay,
}
data.append(row)
+
return columns, data
def get_bank_accounts():
@@ -116,7 +117,7 @@
entries = get_all("Payroll Entry", payroll_filter, ["name", "payment_account"])
payment_accounts = [d.payment_account for d in entries]
- set_company_account(payment_accounts, entries)
+ entries = set_company_account(payment_accounts, entries)
return entries
def get_salary_slips(payroll_entries):
diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py
index d77eb2c..d60b1a2 100644
--- a/erpnext/portal/product_configurator/utils.py
+++ b/erpnext/portal/product_configurator/utils.py
@@ -2,6 +2,7 @@
from frappe.utils import cint
from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
from erpnext.shopping_cart.product_info import get_product_info_for_website
+from erpnext.setup.doctype.item_group.item_group import get_child_groups
def get_field_filter_data():
product_settings = get_product_settings()
@@ -89,6 +90,7 @@
def get_products_html_for_website(field_filters=None, attribute_filters=None):
field_filters = frappe.parse_json(field_filters)
attribute_filters = frappe.parse_json(attribute_filters)
+ set_item_group_filters(field_filters)
items = get_products_for_website(field_filters, attribute_filters)
html = ''.join(get_html_for_items(items))
@@ -98,6 +100,10 @@
return html
+def set_item_group_filters(field_filters):
+ if field_filters is not None and 'item_group' in field_filters:
+ field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])]
+
def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
items = []
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index c8fbe0b..1e4b2b0 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -14,6 +14,7 @@
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from frappe.model.document import Document
from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
+from erpnext.controllers.employee_boarding_controller import update_employee_boarding_status
class Project(Document):
def get_feed(self):
@@ -37,6 +38,7 @@
self.send_welcome_email()
self.update_costing()
self.update_percent_complete()
+ update_employee_boarding_status(self)
def copy_from_template(self):
'''
@@ -132,6 +134,7 @@
def update_project(self):
'''Called externally by Task'''
self.update_percent_complete()
+ update_employee_boarding_status(self)
self.update_costing()
self.db_update()
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index 39a6024..5976e01 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -77,9 +77,6 @@
if flt(self.progress or 0) > 100:
frappe.throw(_("Progress % for a task cannot be more than 100."))
- if flt(self.progress) == 100:
- self.status = 'Completed'
-
if self.status == 'Completed':
self.progress = 100
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
index 142fe79..239fbb9 100644
--- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -16,7 +16,7 @@
doctype: "Bank Transaction",
filters: { name: this.bank_transaction_name },
fieldname: [
- "date",
+ "date as reference_date",
"deposit",
"withdrawal",
"currency",
diff --git a/erpnext/public/js/contact.js b/erpnext/public/js/contact.js
new file mode 100644
index 0000000..41a0e8a
--- /dev/null
+++ b/erpnext/public/js/contact.js
@@ -0,0 +1,16 @@
+
+
+frappe.ui.form.on("Contact", {
+ refresh(frm) {
+ frm.set_query('link_doctype', "links", function() {
+ return {
+ query: "frappe.contacts.address_and_contact.filter_dynamic_link_doctypes",
+ filters: {
+ fieldtype: ["in", ["HTML", "Text Editor"]],
+ fieldname: ["in", ["contact_html", "company_description"]],
+ }
+ };
+ });
+ frm.refresh_field("links");
+ }
+});
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 0471704..a495a9b 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -65,26 +65,25 @@
this.frm.refresh_fields();
}
- calculate_discount_amount(){
+ calculate_discount_amount() {
if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) {
+ this.calculate_item_values();
+ this.calculate_net_total();
this.set_discount_amount();
this.apply_discount_amount();
}
}
_calculate_taxes_and_totals() {
- frappe.run_serially([
- () => this.validate_conversion_rate(),
- () => this.calculate_item_values(),
- () => this.update_item_tax_map(),
- () => this.initialize_taxes(),
- () => this.determine_exclusive_rate(),
- () => this.calculate_net_total(),
- () => this.calculate_taxes(),
- () => this.manipulate_grand_total_for_inclusive_tax(),
- () => this.calculate_totals(),
- () => this._cleanup()
- ]);
+ this.validate_conversion_rate();
+ this.calculate_item_values();
+ this.initialize_taxes();
+ this.determine_exclusive_rate();
+ this.calculate_net_total();
+ this.calculate_taxes();
+ this.manipulate_grand_total_for_inclusive_tax();
+ this.calculate_totals();
+ this._cleanup();
}
validate_conversion_rate() {
@@ -268,46 +267,6 @@
frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]);
}
- update_item_tax_map() {
- let me = this;
- let item_codes = [];
- let item_rates = {};
- let item_tax_templates = {};
-
- $.each(this.frm.doc.items || [], function(i, item) {
- if (item.item_code) {
- // Use combination of name and item code in case same item is added multiple times
- item_codes.push([item.item_code, item.name]);
- item_rates[item.name] = item.net_rate;
- item_tax_templates[item.name] = item.item_tax_template;
- }
- });
-
- if (item_codes.length) {
- return this.frm.call({
- method: "erpnext.stock.get_item_details.get_item_tax_info",
- args: {
- company: me.frm.doc.company,
- tax_category: cstr(me.frm.doc.tax_category),
- item_codes: item_codes,
- item_rates: item_rates,
- item_tax_templates: item_tax_templates
- },
- callback: function(r) {
- if (!r.exc) {
- $.each(me.frm.doc.items || [], function(i, item) {
- if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) {
- item.item_tax_template = r.message[item.name].item_tax_template;
- item.item_tax_rate = r.message[item.name].item_tax_rate;
- me.add_taxes_from_item_tax_template(item.item_tax_rate);
- }
- });
- }
- }
- });
- }
- }
-
add_taxes_from_item_tax_template(item_tax_map) {
let me = this;
@@ -632,8 +591,6 @@
tax.item_wise_tax_detail = JSON.stringify(tax.item_wise_tax_detail);
});
}
-
- this.frm.refresh_fields();
}
set_discount_amount() {
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 8360337..33366db 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -846,9 +846,9 @@
frappe.run_serially([
() => me.frm.script_manager.trigger("currency"),
+ () => me.update_item_tax_map(),
() => me.apply_default_taxes(),
- () => me.apply_pricing_rule(),
- () => me.calculate_taxes_and_totals()
+ () => me.apply_pricing_rule()
]);
}
}
@@ -1807,6 +1807,46 @@
]);
}
+ update_item_tax_map() {
+ let me = this;
+ let item_codes = [];
+ let item_rates = {};
+ let item_tax_templates = {};
+
+ $.each(this.frm.doc.items || [], function(i, item) {
+ if (item.item_code) {
+ // Use combination of name and item code in case same item is added multiple times
+ item_codes.push([item.item_code, item.name]);
+ item_rates[item.name] = item.net_rate;
+ item_tax_templates[item.name] = item.item_tax_template;
+ }
+ });
+
+ if (item_codes.length) {
+ return this.frm.call({
+ method: "erpnext.stock.get_item_details.get_item_tax_info",
+ args: {
+ company: me.frm.doc.company,
+ tax_category: cstr(me.frm.doc.tax_category),
+ item_codes: item_codes,
+ item_rates: item_rates,
+ item_tax_templates: item_tax_templates
+ },
+ callback: function(r) {
+ if (!r.exc) {
+ $.each(me.frm.doc.items || [], function(i, item) {
+ if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) {
+ item.item_tax_template = r.message[item.name].item_tax_template;
+ item.item_tax_rate = r.message[item.name].item_tax_rate;
+ me.add_taxes_from_item_tax_template(item.item_tax_rate);
+ }
+ });
+ }
+ }
+ });
+ }
+ }
+
item_tax_template(doc, cdt, cdn) {
var me = this;
if(me.frm.updating_party_details) return;
diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js
index aa9bba1..d0c935f 100644
--- a/erpnext/public/js/help_links.js
+++ b/erpnext/public/js/help_links.js
@@ -54,7 +54,7 @@
frappe.help.help_links["Form/System Settings"] = [
{
- label: "Naming Series",
+ label: "System Settings",
url: docsUrl + "user/manual/en/setting-up/settings/system-settings",
},
];
@@ -206,7 +206,7 @@
label: "PayPal Settings",
url:
docsUrl +
- "user/manual/en/setting-up/integrations/paypal-integration",
+ "user/manual/en/erpnext_integration/paypal-integration",
},
];
@@ -215,14 +215,14 @@
label: "Razorpay Settings",
url:
docsUrl +
- "user/manual/en/setting-up/integrations/razorpay-integration",
+ "user/manual/en/erpnext_integration/razorpay-integration",
},
];
frappe.help.help_links["Form/Dropbox Settings"] = [
{
label: "Dropbox Settings",
- url: docsUrl + "user/manual/en/setting-up/integrations/dropbox-backup",
+ url: docsUrl + "user/manual/en/erpnext_integration/dropbox-backup",
},
];
@@ -230,7 +230,7 @@
{
label: "LDAP Settings",
url:
- docsUrl + "user/manual/en/setting-up/integrations/ldap-integration",
+ docsUrl + "user/manual/en/erpnext_integration/ldap-integration",
},
];
@@ -239,7 +239,7 @@
label: "Stripe Settings",
url:
docsUrl +
- "user/manual/en/setting-up/integrations/stripe-integration",
+ "user/manual/en/erpnext_integration/stripe-integration",
},
];
@@ -991,7 +991,7 @@
label: "Nested BOM Structure",
url:
docsUrl +
- "user/manual/en/manufacturing/articles/nested-bom-structure",
+ "user/manual/en/manufacturing/articles/managing-multi-level-bom",
},
];
diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js
index ef03b01..6f5d67c 100644
--- a/erpnext/public/js/setup_wizard.js
+++ b/erpnext/public/js/setup_wizard.js
@@ -147,7 +147,7 @@
}
// Validate bank name
- if(me.values.bank_account){
+ if(me.values.bank_account) {
frappe.call({
async: false,
method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account",
diff --git a/erpnext/public/js/utils/customer_quick_entry.js b/erpnext/public/js/utils/customer_quick_entry.js
index efb8dd9..d2c5c72 100644
--- a/erpnext/public/js/utils/customer_quick_entry.js
+++ b/erpnext/public/js/utils/customer_quick_entry.js
@@ -1,8 +1,8 @@
frappe.provide('frappe.ui.form');
frappe.ui.form.CustomerQuickEntryForm = class CustomerQuickEntryForm extends frappe.ui.form.QuickEntryForm {
- constructor(doctype, after_insert) {
- super(doctype, after_insert);
+ constructor(doctype, after_insert, init_callback, doc, force) {
+ super(doctype, after_insert, init_callback, doc, force);
this.skip_redirect_on_error = true;
}
diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js
index cc2d9f0..54e4886 100644
--- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js
+++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js
@@ -3,7 +3,7 @@
frappe.ui.form.on('E Invoice Settings', {
refresh(frm) {
- const docs_link = 'https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing';
+ const docs_link = 'https://docs.erpnext.com/docs/v13/user/manual/en/regional/india/setup-e-invoicing';
frm.dashboard.set_headline(
__("Read {0} for more information on E Invoicing features.", [`<a href='${docs_link}'>documentation</a>`])
);
diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.js b/erpnext/regional/doctype/gst_settings/gst_settings.js
index 808f9bc..cd682c5 100644
--- a/erpnext/regional/doctype/gst_settings/gst_settings.js
+++ b/erpnext/regional/doctype/gst_settings/gst_settings.js
@@ -35,6 +35,7 @@
return {
filters: {
company: row.company,
+ account_type: "Tax",
is_group: 0
}
};
diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.py b/erpnext/regional/doctype/gst_settings/gst_settings.py
index bc956e9..af3d92e 100644
--- a/erpnext/regional/doctype/gst_settings/gst_settings.py
+++ b/erpnext/regional/doctype/gst_settings/gst_settings.py
@@ -19,6 +19,21 @@
from tabAddress where country = "India" and ifnull(gstin, '')!='' ''')
self.set_onload('data', data)
+ def validate(self):
+ # Validate duplicate accounts
+ self.validate_duplicate_accounts()
+
+ def validate_duplicate_accounts(self):
+ account_list = []
+ for account in self.get('gst_accounts'):
+ for fieldname in ['cgst_account', 'sgst_account', 'igst_account', 'cess_account']:
+ if account.get(fieldname) in account_list:
+ frappe.throw(_("Account {0} appears multiple times").format(
+ frappe.bold(account.get(fieldname))))
+
+ if account.get(fieldname):
+ account_list.append(account.get(fieldname))
+
@frappe.whitelist()
def send_reminder():
frappe.has_permission('GST Settings', throw=True)
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
index 6415204..ea39fe1 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -214,9 +214,8 @@
for d in item_details:
if d.item_code not in self.invoice_items.get(d.parent, {}):
- self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code,
- sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in item_details
- if i.item_code == d.item_code and i.parent == d.parent))
+ self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
+ self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0)
if d.is_nil_exempt and d.item_code not in self.is_nil_exempt:
self.is_nil_exempt.append(d.item_code)
@@ -322,6 +321,9 @@
inter_state_supply_details[(gst_category, place_of_supply)]['txval'] += taxable_value
inter_state_supply_details[(gst_category, place_of_supply)]['iamt'] += (taxable_value * rate /100)
+ if self.invoice_cess.get(inv):
+ self.report_dict['sup_details']['osup_det']['csamt'] += flt(self.invoice_cess.get(inv), 2)
+
self.set_inter_state_supply(inter_state_supply_details)
def set_supplies_liable_to_reverse_charge(self):
diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
index 3857ce1..065f80d 100644
--- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
@@ -46,14 +46,14 @@
make_sales_invoice()
create_purchase_invoices()
- if frappe.db.exists("GSTR 3B Report", "GSTR3B-March-2019-_Test Address-Billing"):
- report = frappe.get_doc("GSTR 3B Report", "GSTR3B-March-2019-_Test Address-Billing")
+ if frappe.db.exists("GSTR 3B Report", "GSTR3B-March-2019-_Test Address GST-Billing"):
+ report = frappe.get_doc("GSTR 3B Report", "GSTR3B-March-2019-_Test Address GST-Billing")
report.save()
else:
report = frappe.get_doc({
"doctype": "GSTR 3B Report",
"company": "_Test Company GST",
- "company_address": "_Test Address-Billing",
+ "company_address": "_Test Address GST-Billing",
"year": getdate().year,
"month": month_number_mapping.get(getdate().month)
}).insert()
@@ -89,7 +89,7 @@
si.append("taxes", {
"charge_type": "On Net Total",
- "account_head": "IGST - _GST",
+ "account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
@@ -117,7 +117,7 @@
si.append("taxes", {
"charge_type": "On Net Total",
- "account_head": "IGST - _GST",
+ "account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
@@ -138,7 +138,7 @@
si1.append("taxes", {
"charge_type": "On Net Total",
- "account_head": "IGST - _GST",
+ "account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
@@ -159,7 +159,7 @@
si2.append("taxes", {
"charge_type": "On Net Total",
- "account_head": "IGST - _GST",
+ "account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
@@ -195,7 +195,7 @@
pi.append("taxes", {
"charge_type": "On Net Total",
- "account_head": "CGST - _GST",
+ "account_head": "Input Tax CGST - _GST",
"cost_center": "Main - _GST",
"description": "CGST @ 9.0",
"rate": 9
@@ -203,7 +203,7 @@
pi.append("taxes", {
"charge_type": "On Net Total",
- "account_head": "SGST - _GST",
+ "account_head": "Input Tax SGST - _GST",
"cost_center": "Main - _GST",
"description": "SGST @ 9.0",
"rate": 9
@@ -410,10 +410,10 @@
company.country = "India"
company.insert()
- if not frappe.db.exists('Address', '_Test Address-Billing'):
+ if not frappe.db.exists('Address', '_Test Address GST-Billing'):
address = frappe.get_doc({
+ "address_title": "_Test Address GST",
"address_line1": "_Test Address Line 1",
- "address_title": "_Test Address",
"address_type": "Billing",
"city": "_Test City",
"state": "Test State",
@@ -444,9 +444,9 @@
if not gst_account:
gst_settings.append("gst_accounts", {
"company": "_Test Company GST",
- "cgst_account": "CGST - _GST",
- "sgst_account": "SGST - _GST",
- "igst_account": "IGST - _GST",
+ "cgst_account": "Output Tax CGST - _GST",
+ "sgst_account": "Output Tax SGST - _GST",
+ "igst_account": "Output Tax IGST - _GST"
})
gst_settings.save()
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index 23d4fe9..8ad30fa 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -1,6 +1,8 @@
erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, {
async refresh(frm) {
+ if (frm.doc.docstatus == 2) return;
+
const res = await frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility',
args: { doc: frm.doc }
@@ -111,7 +113,7 @@
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
const action = () => {
- let message = __('Cancellation of e-way bill is currently not supported. ');
+ let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
message += '<br><br>';
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 5d33c1b..81c7a6b 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -42,7 +42,10 @@
invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') })
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
- no_taxes_applied = not doc.get('taxes')
+
+ # if export invoice, then taxes can be empty
+ # invoice can only be ineligible if no taxes applied and is not an export invoice
+ no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas'
has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst'))
if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item:
@@ -188,9 +191,10 @@
item.qty = abs(item.qty)
- item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty)
- item.gross_amount = abs(item.taxable_value) + item.discount_amount
+ item.unit_rate = abs(item.taxable_value / item.qty)
+ item.gross_amount = abs(item.taxable_value)
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
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 3e0b9b7..9265460 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -12,7 +12,10 @@
from frappe.utils import today
def setup(company=None, patch=True):
- setup_company_independent_fixtures(patch=patch)
+ # Company independent fixtures should be called only once at the first company setup
+ if frappe.db.count('Company', {'country': 'India'}) <=1:
+ setup_company_independent_fixtures(patch=patch)
+
if not patch:
make_fixtures(company)
@@ -25,6 +28,7 @@
frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test)
create_gratuity_rule()
add_print_formats()
+ update_accounts_settings_for_taxes()
def add_hsn_sac_codes():
if frappe.flags.in_test and frappe.flags.created_hsn_codes:
@@ -121,10 +125,12 @@
def make_property_setters(patch=False):
# GST rules do not allow for an invoice no. bigger than 16 characters
journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC']
+ sales_invoice_series = ['SINV-.YY.-', 'SRET-.YY.-', ''] + frappe.get_meta("Sales Invoice").get_options("naming_series").split("\n")
+ purchase_invoice_series = ['PINV-.YY.-', 'PRET-.YY.-', ''] + frappe.get_meta("Purchase Invoice").get_options("naming_series").split("\n")
if not patch:
- make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '')
- make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '')
+ make_property_setter('Sales Invoice', 'naming_series', 'options', '\n'.join(sales_invoice_series), '')
+ make_property_setter('Purchase Invoice', 'naming_series', 'options', '\n'.join(purchase_invoice_series), '')
make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '')
def make_custom_fields(update=True):
@@ -680,7 +686,7 @@
def make_fixtures(company=None):
docs = []
- company = company.name if company else frappe.db.get_value("Global Defaults", None, "default_company")
+ company = company or frappe.db.get_value("Global Defaults", None, "default_company")
set_salary_components(docs)
set_tds_account(docs, company)
@@ -698,6 +704,53 @@
# create records for Tax Withholding Category
set_tax_withholding_category(company)
+def update_regional_tax_settings(country, company):
+ # Will only add default GST accounts if present
+ input_account_names = ['Input Tax CGST', 'Input Tax SGST', 'Input Tax IGST']
+ output_account_names = ['Output Tax CGST', 'Output Tax SGST', 'Output Tax IGST']
+ rcm_accounts = ['Input Tax CGST RCM', 'Input Tax SGST RCM', 'Input Tax IGST RCM']
+ gst_settings = frappe.get_single('GST Settings')
+ existing_account_list = []
+
+ for account in gst_settings.get('gst_accounts'):
+ for key in ['cgst_account', 'sgst_account', 'igst_account']:
+ existing_account_list.append(account.get(key))
+
+ gst_accounts = frappe._dict(frappe.get_all("Account",
+ {'company': company, 'account_name': ('in', input_account_names +
+ output_account_names + rcm_accounts)}, ['account_name', 'name'], as_list=1))
+
+ add_accounts_in_gst_settings(company, input_account_names, gst_accounts,
+ existing_account_list, gst_settings)
+ add_accounts_in_gst_settings(company, output_account_names, gst_accounts,
+ existing_account_list, gst_settings)
+ add_accounts_in_gst_settings(company, rcm_accounts, gst_accounts,
+ existing_account_list, gst_settings, is_reverse_charge=1)
+
+ gst_settings.save()
+
+def add_accounts_in_gst_settings(company, account_names, gst_accounts,
+ existing_account_list, gst_settings, is_reverse_charge=0):
+ accounts_not_added = 1
+
+ for account in account_names:
+ # Default Account Added does not exists
+ if not gst_accounts.get(account):
+ accounts_not_added = 0
+
+ # Check if already added in GST Settings
+ if gst_accounts.get(account) in existing_account_list:
+ accounts_not_added = 0
+
+ if accounts_not_added:
+ gst_settings.append('gst_accounts', {
+ 'company': company,
+ 'cgst_account': gst_accounts.get(account_names[0]),
+ 'sgst_account': gst_accounts.get(account_names[1]),
+ 'igst_account': gst_accounts.get(account_names[2]),
+ 'is_reverse_charge_account': is_reverse_charge
+ })
+
def set_salary_components(docs):
docs.extend([
{'doctype': 'Salary Component', 'salary_component': 'Professional Tax',
@@ -731,13 +784,14 @@
docs = get_tds_details(accounts, fiscal_year)
for d in docs:
- try:
+ if not frappe.db.exists("Tax Withholding Category", d.get("name")):
doc = frappe.get_doc(d)
+ doc.flags.ignore_validate = True
doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True
doc.insert()
- except frappe.DuplicateEntryError:
- doc = frappe.get_doc("Tax Withholding Category", d.get("name"))
+ else:
+ doc = frappe.get_doc("Tax Withholding Category", d.get("name"), for_update=True)
if accounts:
doc.append("accounts", accounts[0])
@@ -749,11 +803,12 @@
doc.append("rates", d.get('rates')[0])
doc.flags.ignore_permissions = True
+ doc.flags.ignore_validate = True
doc.flags.ignore_mandatory = True
+ doc.flags.ignore_links = True
doc.save()
def set_tds_account(docs, company):
- abbr = frappe.get_value("Company", company, "abbr")
parent_account = frappe.db.get_value("Account", filters = {"account_name": "Duties and Taxes", "company": company})
if parent_account:
docs.extend([
@@ -912,7 +967,6 @@
]
def create_gratuity_rule():
-
# Standard Indain Gratuity Rule
if not frappe.db.exists("Gratuity Rule", "Indian Standard Gratuity Rule"):
rule = frappe.new_doc("Gratuity Rule")
@@ -930,3 +984,7 @@
rule.flags.ignore_mandatory = True
rule.save()
+
+def update_accounts_settings_for_taxes():
+ if frappe.db.count('Company') == 1:
+ frappe.db.set_value('Accounts Settings', None, "add_taxes_from_item_tax_template", 0)
\ No newline at end of file
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index a4466e7..fbe47d0 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -431,9 +431,11 @@
company_address = frappe.get_doc('Address', doc.company_address)
billing_address = frappe.get_doc('Address', doc.customer_address)
+ #added dispatch address
+ dispatch_address = frappe.get_doc('Address', doc.dispatch_address_name) if doc.dispatch_address_name else company_address
shipping_address = frappe.get_doc('Address', doc.shipping_address_name)
- data = get_address_details(data, doc, company_address, billing_address)
+ data = get_address_details(data, doc, company_address, billing_address, dispatch_address)
data.itemList = []
data.totalValue = doc.total
@@ -519,10 +521,10 @@
`tabDynamic Link`.link_name = %(company)s""", {"company": company})
return company_gstins
-def get_address_details(data, doc, company_address, billing_address):
+def get_address_details(data, doc, company_address, billing_address, dispatch_address):
data.fromPincode = validate_pincode(company_address.pincode, 'Company Address')
- data.fromStateCode = data.actualFromStateCode = validate_state_code(
- company_address.gst_state_number, 'Company Address')
+ data.fromStateCode = validate_state_code(company_address.gst_state_number, 'Company Address')
+ data.actualFromStateCode = validate_state_code(dispatch_address.gst_state_number, 'Dispatch Address')
if not doc.billing_address_gstin or len(doc.billing_address_gstin) < 15:
data.toGstin = 'URP'
@@ -834,8 +836,16 @@
depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
if row.depreciation_method in ("Straight Line", "Manual"):
- depreciation_amount = (flt(row.value_after_depreciation) -
- flt(row.expected_value_after_useful_life)) / depreciation_left
+ # if the Depreciation Schedule is being prepared for the first time
+ if not asset.flags.increase_in_asset_life:
+ depreciation_amount = (flt(row.value_after_depreciation) -
+ flt(row.expected_value_after_useful_life)) / depreciation_left
+
+ # if the Depreciation Schedule is being modified after Asset Repair
+ else:
+ depreciation_amount = (flt(row.value_after_depreciation) -
+ flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
+
else:
rate_of_depreciation = row.rate_of_depreciation
# if its the first depreciation
diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json
index 4deb073..d0000ad 100644
--- a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json
+++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json
@@ -11,7 +11,7 @@
"is_standard": "Yes",
"json": "{}",
"letter_head": "Logo",
- "modified": "2021-03-12 12:36:48.689413",
+ "modified": "2021-03-13 12:36:48.689413",
"modified_by": "Administrator",
"module": "Regional",
"name": "E-Invoice Summary",
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 1096159..b81fa81 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -217,9 +217,8 @@
for d in items:
if d.item_code not in self.invoice_items.get(d.parent, {}):
- self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code,
- sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in items
- if i.item_code == d.item_code and i.parent == d.parent))
+ self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
+ self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0)
item_tax_rate = {}
@@ -584,7 +583,7 @@
def get_json(filters, report_name, data):
filters = json.loads(filters)
report_data = json.loads(data)
- gstin = get_company_gstin_number(filters["company"], filters["company_address"])
+ gstin = get_company_gstin_number(filters.get("company"), filters.get("company_address"))
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year)
diff --git a/erpnext/selling/doctype/campaign/README.md b/erpnext/selling/doctype/campaign/README.md
deleted file mode 100644
index a837318..0000000
--- a/erpnext/selling/doctype/campaign/README.md
+++ /dev/null
@@ -1 +0,0 @@
-Sales campaign / promotion, like special discount, exhibition, newsletter etc.
\ No newline at end of file
diff --git a/erpnext/selling/doctype/campaign/__init__.py b/erpnext/selling/doctype/campaign/__init__.py
deleted file mode 100644
index baffc48..0000000
--- a/erpnext/selling/doctype/campaign/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from __future__ import unicode_literals
diff --git a/erpnext/selling/doctype/campaign/campaign.js b/erpnext/selling/doctype/campaign/campaign.js
deleted file mode 100644
index 72a90d0..0000000
--- a/erpnext/selling/doctype/campaign/campaign.js
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-// License: GNU General Public License v3. See license.txt
-
-frappe.ui.form.on("Campaign", "refresh", function(frm) {
- erpnext.toggle_naming_series();
- if(frm.doc.__islocal) {
- frm.toggle_display("naming_series", frappe.boot.sysdefaults.campaign_naming_by=="Naming Series");
- }
- else{
- cur_frm.add_custom_button(__("View Leads"), function() {
- frappe.route_options = {"source": "Campaign","campaign_name": frm.doc.name}
- frappe.set_route("List", "Lead");
- }, "fa fa-list", true);
- }
-})
diff --git a/erpnext/selling/doctype/campaign/campaign_dashboard.py b/erpnext/selling/doctype/campaign/campaign_dashboard.py
deleted file mode 100644
index 3cef560..0000000
--- a/erpnext/selling/doctype/campaign/campaign_dashboard.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return {
- 'fieldname': 'campaign_name',
- 'transactions': [
- {
- 'label': _('Email Campaigns'),
- 'items': ['Email Campaign']
- },
- {
- 'label': _('Social Media Campaigns'),
- 'items': ['Social Media Post']
- }
- ]
- }
diff --git a/erpnext/selling/doctype/campaign/test_campaign.py b/erpnext/selling/doctype/campaign/test_campaign.py
deleted file mode 100644
index 4d062ff..0000000
--- a/erpnext/selling/doctype/campaign/test_campaign.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
-
-import frappe
-test_records = frappe.get_test_records('Campaign')
\ No newline at end of file
diff --git a/erpnext/selling/doctype/campaign/test_records.json b/erpnext/selling/doctype/campaign/test_records.json
deleted file mode 100644
index 625d3b3..0000000
--- a/erpnext/selling/doctype/campaign/test_records.json
+++ /dev/null
@@ -1,10 +0,0 @@
-[
- {
- "campaign_name": "_Test Campaign",
- "doctype": "Campaign"
- },
- {
- "campaign_name": "_Test Campaign 1",
- "doctype": "Campaign"
- }
-]
\ No newline at end of file
diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js
index 825b170..2849466 100644
--- a/erpnext/selling/doctype/customer/customer.js
+++ b/erpnext/selling/doctype/customer/customer.js
@@ -130,6 +130,10 @@
erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name);
}, __('Create'));
+ frm.add_custom_button(__('Get Customer Group Details'), function () {
+ frm.trigger("get_customer_group_details");
+ }, __('Actions'));
+
// indicator
erpnext.utils.set_party_dashboard_indicators(frm);
@@ -145,4 +149,15 @@
if(frm.doc.lead_name) frappe.model.clear_doc("Lead", frm.doc.lead_name);
},
-});
\ No newline at end of file
+ get_customer_group_details: function(frm) {
+ frappe.call({
+ method: "get_customer_group_details",
+ doc: frm.doc,
+ callback: function() {
+ frm.refresh();
+ }
+ });
+
+ }
+});
+
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 818888c..3b62081 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -78,6 +78,29 @@
if sum(member.allocated_percentage or 0 for member in self.sales_team) != 100:
frappe.throw(_("Total contribution percentage should be equal to 100"))
+ @frappe.whitelist()
+ def get_customer_group_details(self):
+ doc = frappe.get_doc('Customer Group', self.customer_group)
+ self.accounts = self.credit_limits = []
+ self.payment_terms = self.default_price_list = ""
+
+ tables = [["accounts", "account"], ["credit_limits", "credit_limit"]]
+ fields = ["payment_terms", "default_price_list"]
+
+ for row in tables:
+ table, field = row[0], row[1]
+ if not doc.get(table): continue
+
+ for entry in doc.get(table):
+ child = self.append(table)
+ child.update({"company": entry.company, field: entry.get(field)})
+
+ for field in fields:
+ if not doc.get(field): continue
+ self.update({field: doc.get(field)})
+
+ self.save()
+
def check_customer_group_change(self):
frappe.flags.customer_group_changed = False
diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py
index 7761aa7..b1a5b52 100644
--- a/erpnext/selling/doctype/customer/test_customer.py
+++ b/erpnext/selling/doctype/customer/test_customer.py
@@ -27,6 +27,42 @@
def tearDown(self):
set_credit_limit('_Test Customer', '_Test Company', 0)
+ def test_get_customer_group_details(self):
+ doc = frappe.new_doc("Customer Group")
+ doc.customer_group_name = "_Testing Customer Group"
+ doc.payment_terms = "_Test Payment Term Template 3"
+ doc.accounts = []
+ doc.default_price_list = "Standard Buying"
+ doc.credit_limits = []
+ test_account_details = {
+ "company": "_Test Company",
+ "account": "Creditors - _TC",
+ }
+ test_credit_limits = {
+ "company": "_Test Company",
+ "credit_limit": 350000
+ }
+ doc.append("accounts", test_account_details)
+ doc.append("credit_limits", test_credit_limits)
+ doc.insert()
+
+ c_doc = frappe.new_doc("Customer")
+ c_doc.customer_name = "Testing Customer"
+ c_doc.customer_group = "_Testing Customer Group"
+ c_doc.payment_terms = c_doc.default_price_list = ""
+ c_doc.accounts = c_doc.credit_limits= []
+ c_doc.insert()
+ c_doc.get_customer_group_details()
+ self.assertEqual(c_doc.payment_terms, "_Test Payment Term Template 3")
+
+ self.assertEqual(c_doc.accounts[0].company, "_Test Company")
+ self.assertEqual(c_doc.accounts[0].account, "Creditors - _TC")
+
+ self.assertEqual(c_doc.credit_limits[0].company, "_Test Company")
+ self.assertEqual(c_doc.credit_limits[0].credit_limit, 350000)
+ c_doc.delete()
+ doc.delete()
+
def test_party_details(self):
from erpnext.accounts.party import get_party_details
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 762b6f1..d31db82 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -38,6 +38,8 @@
"col_break46",
"shipping_address_name",
"shipping_address",
+ "dispatch_address_name",
+ "dispatch_address",
"customer_group",
"territory",
"currency_and_price_list",
@@ -1486,13 +1488,29 @@
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
"label": "Disable Rounded Total"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "dispatch_address_name",
+ "fieldtype": "Link",
+ "label": "Dispatch Address Name",
+ "options": "Address",
+ "print_hide": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "depends_on": "dispatch_address_name",
+ "fieldname": "dispatch_address",
+ "fieldtype": "Small Text",
+ "label": "Dispatch Address",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-15 23:55:13.439068",
+ "modified": "2021-07-08 21:37:44.177493",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index 7cae0e4..6e36d28 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -367,15 +367,16 @@
`<div class="add-discount-field"></div>`
);
const me = this;
+ const frm = me.events.get_frm();
+ let discount = frm.doc.additional_discount_percentage;
this.discount_field = frappe.ui.form.make_control({
df: {
label: __('Discount'),
fieldtype: 'Data',
- placeholder: __('Enter discount percentage.'),
+ placeholder: ( discount ? discount + '%' : __('Enter discount percentage.') ),
input_class: 'input-xs',
onchange: function() {
- const frm = me.events.get_frm();
if (flt(this.value) != 0) {
frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', flt(this.value));
me.hide_discount_control(this.value);
@@ -472,12 +473,7 @@
const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total;
this.render_grand_total(grand_total);
- const taxes = frm.doc.taxes.map(t => {
- return {
- description: t.description, rate: t.rate
- };
- });
- this.render_taxes(frm.doc.total_taxes_and_charges, taxes);
+ this.render_taxes(frm.doc.taxes);
}
render_net_total(value) {
@@ -502,14 +498,14 @@
);
}
- render_taxes(value, taxes) {
+ render_taxes(taxes) {
if (taxes.length) {
const currency = this.events.get_frm().doc.currency;
const taxes_html = taxes.map(t => {
const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`;
return `<div class="tax-row">
<div class="tax-label">${description}</div>
- <div class="tax-value">${format_currency(value, currency)}</div>
+ <div class="tax-value">${format_currency(t.tax_amount_after_discount_amount, currency)}</div>
</div>`;
}).join('');
this.$totals_section.find('.taxes-container').css('display', 'flex').html(taxes_html);
@@ -970,8 +966,23 @@
});
}
+ attach_refresh_field_event(frm) {
+ $(frm.wrapper).off('refresh-fields');
+ $(frm.wrapper).on('refresh-fields', () => {
+ if (frm.doc.items.length) {
+ frm.doc.items.forEach(item => {
+ this.update_item_html(item);
+ });
+ }
+ this.update_totals_section(frm);
+ });
+ }
+
load_invoice() {
const frm = this.events.get_frm();
+
+ this.attach_refresh_field_event(frm);
+
this.fetch_customer_details(frm.doc.customer).then(() => {
this.events.customer_details_updated(this.customer_info);
this.update_customer_section();
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index c484873..f1a166b 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -56,7 +56,7 @@
);
let df_events = {
onchange: function() {
- frm.set_value(this.df.fieldname, this.value);
+ frm.set_value(this.df.fieldname, this.get_value());
}
};
if (df.fieldtype == "Button") {
diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.css b/erpnext/selling/page/sales_funnel/sales_funnel.css
index 89e904f..455d37c 100644
--- a/erpnext/selling/page/sales_funnel/sales_funnel.css
+++ b/erpnext/selling/page/sales_funnel/sales_funnel.css
@@ -1,3 +1,4 @@
.funnel-wrapper {
margin: 15px;
+ width: 100%;
}
\ No newline at end of file
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index eb02867..f515baf 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -26,7 +26,7 @@
}
};
});
- }
+ }
setup_queries() {
var me = this;
@@ -85,7 +85,7 @@
refresh() {
super.refresh();
-
+
frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'}
this.frm.toggle_display("customer_name",
@@ -114,6 +114,10 @@
erpnext.utils.set_taxes_from_address(this.frm, "shipping_address_name", "customer_address", "shipping_address_name");
}
+ dispatch_address_name() {
+ erpnext.utils.get_address_display(this.frm, "dispatch_address_name", "dispatch_address");
+ }
+
sales_partner() {
this.apply_pricing_rule();
}
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 061986d..e6ec496 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -74,7 +74,7 @@
"stock_received_but_not_billed",
"service_received_but_not_billed",
"expenses_included_in_valuation",
- "fixed_asset_depreciation_settings",
+ "fixed_asset_defaults",
"accumulated_depreciation_account",
"depreciation_expense_account",
"series_for_depreciation_entry",
@@ -83,6 +83,7 @@
"disposal_account",
"depreciation_cost_center",
"capital_work_in_progress_account",
+ "repair_and_maintenance_account",
"asset_received_but_not_billed",
"budget_detail",
"exception_budget_approver_role",
@@ -520,12 +521,6 @@
"options": "Account"
},
{
- "collapsible": 1,
- "fieldname": "fixed_asset_depreciation_settings",
- "fieldtype": "Section Break",
- "label": "Fixed Asset Depreciation Settings"
- },
- {
"fieldname": "accumulated_depreciation_account",
"fieldtype": "Link",
"label": "Accumulated Depreciation Account",
@@ -734,6 +729,18 @@
"fieldtype": "Link",
"label": "Default Payment Discount Account",
"options": "Account"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "fixed_asset_defaults",
+ "fieldtype": "Section Break",
+ "label": "Fixed Asset Defaults"
+ },
+ {
+ "fieldname": "repair_and_maintenance_account",
+ "fieldtype": "Link",
+ "label": "Repair and Maintenance Account",
+ "options": "Account"
}
],
"icon": "fa fa-building",
@@ -741,7 +748,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
- "modified": "2021-05-07 03:11:28.189740",
+ "modified": "2021-05-12 16:51:08.187233",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 0427abe..8755125 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -110,7 +110,7 @@
self.create_default_warehouses()
if frappe.flags.country_change:
- install_country_fixtures(self.name)
+ install_country_fixtures(self.name, self.country)
self.create_default_tax_template()
if not frappe.db.get_value("Department", {"company": self.name}):
@@ -291,7 +291,7 @@
cash = frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name')
if cash and self.default_cash_account \
and not frappe.db.get_value('Mode of Payment Account', {'company': self.name, 'parent': cash}):
- mode_of_payment = frappe.get_doc('Mode of Payment', cash)
+ mode_of_payment = frappe.get_doc('Mode of Payment', cash, for_update=True)
mode_of_payment.append('accounts', {
'company': self.name,
'default_account': self.default_cash_account
@@ -395,7 +395,7 @@
@frappe.whitelist()
def enqueue_replace_abbr(company, old, new):
- kwargs = dict(company=company, old=old, new=new)
+ kwargs = dict(queue="long", company=company, old=old, new=new)
frappe.enqueue('erpnext.setup.doctype.company.company.replace_abbr', **kwargs)
@@ -440,16 +440,15 @@
return " - ".join(parts)
-def install_country_fixtures(company):
- company_doc = frappe.get_doc("Company", company)
- path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(company_doc.country))
+def install_country_fixtures(company, country):
+ path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country))
if os.path.exists(path.encode("utf-8")):
try:
- module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(company_doc.country))
- frappe.get_attr(module_name)(company_doc, False)
+ module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(country))
+ frappe.get_attr(module_name)(company, False)
except Exception as e:
frappe.log_error()
- frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(company_doc.country)))
+ frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(country)))
def update_company_current_month_sales(company):
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index 1a83cb6..c46b6cc 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -87,8 +87,8 @@
if not field_filters:
field_filters = {}
- # Ensure the query remains within current item group
- field_filters['item_group'] = self.name
+ # Ensure the query remains within current item group & sub group
+ field_filters['item_group'] = [ig[0] for ig in get_child_groups(self.name)]
engine = ProductQuery()
context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name)
diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
index ece9fb5..691d331 100644
--- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
+++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
@@ -12,10 +12,14 @@
class TransactionDeletionRecord(Document):
def validate(self):
frappe.only_for('System Manager')
+ self.validate_doctypes_to_be_ignored()
+
+ def validate_doctypes_to_be_ignored(self):
doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
for doctype in self.doctypes_to_be_ignored:
if doctype.doctype_name not in doctypes_to_be_ignored_list:
- frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it. "), title=_("Not Allowed"))
+ frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it. "),
+ title=_("Not Allowed"))
def before_submit(self):
if not self.doctypes_to_be_ignored:
@@ -23,54 +27,9 @@
self.delete_bins()
self.delete_lead_addresses()
-
- company_obj = frappe.get_doc('Company', self.company)
- # reset company values
- company_obj.total_monthly_sales = 0
- company_obj.sales_monthly_history = None
- company_obj.save()
- # Clear notification counts
+ self.reset_company_values()
clear_notifications()
-
- singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name')
- tables = frappe.get_all('DocType', filters = {'istable': 1}, pluck = 'name')
- doctypes_to_be_ignored_list = singles
- for doctype in self.doctypes_to_be_ignored:
- doctypes_to_be_ignored_list.append(doctype.doctype_name)
-
- docfields = frappe.get_all('DocField',
- filters = {
- 'fieldtype': 'Link',
- 'options': 'Company',
- 'parent': ['not in', doctypes_to_be_ignored_list]},
- fields=['parent', 'fieldname'])
-
- for docfield in docfields:
- if docfield['parent'] != self.doctype:
- no_of_docs = frappe.db.count(docfield['parent'], {
- docfield['fieldname'] : self.company
- })
-
- if no_of_docs > 0:
- self.delete_version_log(docfield['parent'], docfield['fieldname'])
- self.delete_communications(docfield['parent'], docfield['fieldname'])
-
- # populate DocTypes table
- if docfield['parent'] not in tables:
- self.append('doctypes', {
- 'doctype_name' : docfield['parent'],
- 'no_of_docs' : no_of_docs
- })
-
- # delete the docs linked with the specified company
- frappe.db.delete(docfield['parent'], {
- docfield['fieldname'] : self.company
- })
-
- naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname')
- if naming_series:
- if '#' in naming_series:
- self.update_naming_series(naming_series, docfield['parent'])
+ self.delete_company_transactions()
def populate_doctypes_to_be_ignored_table(self):
doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
@@ -79,6 +38,111 @@
'doctype_name' : doctype
})
+ def delete_bins(self):
+ frappe.db.sql("""delete from tabBin where warehouse in
+ (select name from tabWarehouse where company=%s)""", self.company)
+
+ def delete_lead_addresses(self):
+ """Delete addresses to which leads are linked"""
+ leads = frappe.get_all('Lead', filters={'company': self.company})
+ leads = ["'%s'" % row.get("name") for row in leads]
+ addresses = []
+ if leads:
+ addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name
+ in ({leads})""".format(leads=",".join(leads)))
+
+ if addresses:
+ addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
+
+ frappe.db.sql("""delete from tabAddress where name in ({addresses}) and
+ name not in (select distinct dl1.parent from `tabDynamic Link` dl1
+ inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
+ and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses)))
+
+ frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead'
+ and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads)))
+
+ frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads)))
+
+ def reset_company_values(self):
+ company_obj = frappe.get_doc('Company', self.company)
+ company_obj.total_monthly_sales = 0
+ company_obj.sales_monthly_history = None
+ company_obj.save()
+
+ def delete_company_transactions(self):
+ doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list()
+ docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list)
+
+ tables = self.get_all_child_doctypes()
+ for docfield in docfields:
+ if docfield['parent'] != self.doctype:
+ no_of_docs = self.get_number_of_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname'])
+
+ if no_of_docs > 0:
+ self.delete_version_log(docfield['parent'], docfield['fieldname'])
+ self.delete_communications(docfield['parent'], docfield['fieldname'])
+ self.populate_doctypes_table(tables, docfield['parent'], no_of_docs)
+
+ self.delete_child_tables(docfield['parent'], docfield['fieldname'])
+ self.delete_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname'])
+
+ naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname')
+ if naming_series:
+ if '#' in naming_series:
+ self.update_naming_series(naming_series, docfield['parent'])
+
+ def get_doctypes_to_be_ignored_list(self):
+ singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name')
+ doctypes_to_be_ignored_list = singles
+ for doctype in self.doctypes_to_be_ignored:
+ doctypes_to_be_ignored_list.append(doctype.doctype_name)
+
+ return doctypes_to_be_ignored_list
+
+ def get_doctypes_with_company_field(self, doctypes_to_be_ignored_list):
+ docfields = frappe.get_all('DocField',
+ filters = {
+ 'fieldtype': 'Link',
+ 'options': 'Company',
+ 'parent': ['not in', doctypes_to_be_ignored_list]},
+ fields=['parent', 'fieldname'])
+
+ return docfields
+
+ def get_all_child_doctypes(self):
+ return frappe.get_all('DocType', filters = {'istable': 1}, pluck = 'name')
+
+ def get_number_of_docs_linked_with_specified_company(self, doctype, company_fieldname):
+ return frappe.db.count(doctype, {company_fieldname : self.company})
+
+ def populate_doctypes_table(self, tables, doctype, no_of_docs):
+ if doctype not in tables:
+ self.append('doctypes', {
+ 'doctype_name' : doctype,
+ 'no_of_docs' : no_of_docs
+ })
+
+ def delete_child_tables(self, doctype, company_fieldname):
+ parent_docs_to_be_deleted = frappe.get_all(doctype, {
+ company_fieldname : self.company
+ }, pluck = 'name')
+
+ child_tables = frappe.get_all('DocField', filters = {
+ 'fieldtype': 'Table',
+ 'parent': doctype
+ }, pluck = 'options')
+
+ for table in child_tables:
+ frappe.db.delete(table, {
+ 'parent': ['in', parent_docs_to_be_deleted]
+ })
+
+ def delete_docs_linked_with_specified_company(self, doctype, company_fieldname):
+ frappe.db.delete(doctype, {
+ company_fieldname : self.company
+ })
+
def update_naming_series(self, naming_series, doctype_name):
if '.' in naming_series:
prefix, hashes = naming_series.rsplit('.', 1)
@@ -107,32 +171,6 @@
frappe.delete_doc('Communication', communication_names, ignore_permissions=True)
- def delete_bins(self):
- frappe.db.sql("""delete from tabBin where warehouse in
- (select name from tabWarehouse where company=%s)""", self.company)
-
- def delete_lead_addresses(self):
- """Delete addresses to which leads are linked"""
- leads = frappe.get_all('Lead', filters={'company': self.company})
- leads = ["'%s'" % row.get("name") for row in leads]
- addresses = []
- if leads:
- addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name
- in ({leads})""".format(leads=",".join(leads)))
-
- if addresses:
- addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
-
- frappe.db.sql("""delete from tabAddress where name in ({addresses}) and
- name not in (select distinct dl1.parent from `tabDynamic Link` dl1
- inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
- and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses)))
-
- frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead'
- and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads)))
-
- frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads)))
-
@frappe.whitelist()
def get_doctypes_to_be_ignored():
doctypes_to_be_ignored_list = ['Account', 'Cost Center', 'Warehouse', 'Budget',
diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json
index daaa626..34af093 100644
--- a/erpnext/setup/setup_wizard/data/country_wise_tax.json
+++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json
@@ -1164,33 +1164,292 @@
},
"India": {
+ "tax_categories": [
+ {
+ "title": "In-State",
+ "is_inter_state": 0,
+ "gst_state": ""
+ },
+ {
+ "title": "Out-State",
+ "is_inter_state": 1,
+ "gst_state": ""
+ },
+ {
+ "title": "Reverse Charge In-State",
+ "is_inter_state": 0,
+ "gst_state": ""
+ },
+ {
+ "title": "Reverse Charge Out-State",
+ "is_inter_state": 1,
+ "gst_state": ""
+ },
+ {
+ "title": "Registered Composition",
+ "is_inter_state": 0,
+ "gst_state": ""
+ }
+ ],
"chart_of_accounts": {
"*": {
"item_tax_templates": [
{
- "title": "In State GST",
+ "title": "GST 9%",
"taxes": [
{
"tax_type": {
- "account_name": "SGST",
+ "account_name": "Output Tax SGST",
"tax_rate": 9.00
}
},
{
"tax_type": {
- "account_name": "CGST",
+ "account_name": "Output Tax CGST",
"tax_rate": 9.00
}
+ },
+ {
+ "tax_type": {
+ "account_name": "Output Tax IGST",
+ "tax_rate": 18.00
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax SGST",
+ "tax_rate": 9.00,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax CGST",
+ "tax_rate": 9.00,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax IGST",
+ "tax_rate": 18.00,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax SGST RCM",
+ "tax_rate": 9.00,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax CGST RCM",
+ "tax_rate": 9.00,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax IGST RCM",
+ "tax_rate": 18.00,
+ "root_type": "Asset"
+ }
}
]
},
{
- "title": "Out of State GST",
+ "title": "GST 5%",
"taxes": [
{
"tax_type": {
- "account_name": "IGST",
- "tax_rate": 18.00
+ "account_name": "Output Tax SGST",
+ "tax_rate": 2.5
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Output Tax CGST",
+ "tax_rate": 2.5
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Output Tax IGST",
+ "tax_rate": 5.0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax SGST",
+ "tax_rate": 2.5,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax CGST",
+ "tax_rate": 2.5,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax IGST",
+ "tax_rate": 5.0,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax SGST RCM",
+ "tax_rate": 2.50,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax CGST RCM",
+ "tax_rate": 2.50,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax IGST RCM",
+ "tax_rate": 5.00,
+ "root_type": "Asset"
+ }
+ }
+ ]
+ },
+ {
+ "title": "GST 12%",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Output Tax SGST",
+ "tax_rate": 6.0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Output Tax CGST",
+ "tax_rate": 6.0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Output Tax IGST",
+ "tax_rate": 12.0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax SGST",
+ "tax_rate": 6.0,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax CGST",
+ "tax_rate": 6.0,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax IGST",
+ "tax_rate": 12.0,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax SGST RCM",
+ "tax_rate": 6.00,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax CGST RCM",
+ "tax_rate": 6.00,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax IGST RCM",
+ "tax_rate": 12.00,
+ "root_type": "Asset"
+ }
+ }
+ ]
+ },
+ {
+ "title": "GST 28%",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Output Tax SGST",
+ "tax_rate": 14.0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Output Tax CGST",
+ "tax_rate": 14.0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Output Tax IGST",
+ "tax_rate": 28.0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax SGST",
+ "tax_rate": 14.0,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax CGST",
+ "tax_rate": 14.0,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax IGST",
+ "tax_rate": 28.0,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax SGST RCM",
+ "tax_rate": 14.00,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax CGST RCM",
+ "tax_rate": 14.00,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax IGST RCM",
+ "tax_rate": 28.00,
+ "root_type": "Asset"
}
}
]
@@ -1229,36 +1488,117 @@
]
}
],
- "*": [
+ "sales_tax_templates": [
{
- "title": "In State GST",
+ "title": "Output GST In-state",
"taxes": [
{
"account_head": {
- "account_name": "SGST",
- "tax_rate": 9.00
+ "account_name": "Output Tax SGST",
+ "tax_rate": 9.00,
+ "account_type": "Tax"
}
},
{
"account_head": {
- "account_name": "CGST",
- "tax_rate": 9.00
+ "account_name": "Output Tax CGST",
+ "tax_rate": 9.00,
+ "account_type": "Tax"
}
}
- ]
+ ],
+ "tax_category": "In-State"
},
{
- "title": "Out of State GST",
+ "title": "Output GST Out-state",
"taxes": [
{
"account_head": {
- "account_name": "IGST",
- "tax_rate": 18.00
+ "account_name": "Output Tax IGST",
+ "tax_rate": 18.00,
+ "account_type": "Tax"
}
}
- ]
+ ],
+ "tax_category": "Out-State"
+ }
+ ],
+ "purchase_tax_templates": [
+ {
+ "title": "Input GST In-state",
+ "taxes": [
+ {
+ "account_head": {
+ "account_name": "Input Tax SGST",
+ "tax_rate": 9.00,
+ "root_type": "Asset",
+ "account_type": "Tax"
+ }
+ },
+ {
+ "account_head": {
+ "account_name": "Input Tax CGST",
+ "tax_rate": 9.00,
+ "root_type": "Asset",
+ "account_type": "Tax"
+ }
+ }
+ ],
+ "tax_category": "In-State"
},
{
+ "title": "Input GST Out-state",
+ "taxes": [
+ {
+ "account_head": {
+ "account_name": "Input Tax IGST",
+ "tax_rate": 18.00,
+ "root_type": "Asset",
+ "account_type": "Tax"
+ }
+ }
+ ],
+ "tax_category": "Out-State"
+ },
+ {
+ "title": "Input GST RCM In-state",
+ "taxes": [
+ {
+ "account_head": {
+ "account_name": "Input Tax SGST RCM",
+ "tax_rate": 9.00,
+ "root_type": "Asset",
+ "account_type": "Tax"
+ }
+ },
+ {
+ "account_head": {
+ "account_name": "Input Tax CGST RCM",
+ "tax_rate": 9.00,
+ "root_type": "Asset",
+ "account_type": "Tax"
+ }
+ }
+ ],
+ "tax_category": "Reverse Charge In-State"
+ },
+ {
+ "title": "Input GST RCM Out-state",
+ "taxes": [
+ {
+ "account_head": {
+ "account_name": "Input Tax IGST RCM",
+ "tax_rate": 18.00,
+ "root_type": "Asset",
+ "account_type": "Tax"
+ }
+ }
+ ],
+ "tax_category": "Reverse Charge Out-State"
+ }
+ ],
+ "*": [
+ {
"title": "VAT 5%",
"taxes": [
{
@@ -1349,7 +1689,7 @@
"Italy VAT 4%":{
"account_name": "IVA 4%",
"tax_rate": 4.00
- }
+ }
},
"Ivory Coast": {
diff --git a/erpnext/setup/setup_wizard/operations/company_setup.py b/erpnext/setup/setup_wizard/operations/company_setup.py
index 3f0bb14..4edf948 100644
--- a/erpnext/setup/setup_wizard/operations/company_setup.py
+++ b/erpnext/setup/setup_wizard/operations/company_setup.py
@@ -42,29 +42,6 @@
'quotation_series': "QTN-",
}).insert()
-def create_bank_account(args):
- if args.get("bank_account"):
- company_name = args.get('company_name')
- bank_account_group = frappe.db.get_value("Account",
- {"account_type": "Bank", "is_group": 1, "root_type": "Asset",
- "company": company_name})
- if bank_account_group:
- bank_account = frappe.get_doc({
- "doctype": "Account",
- 'account_name': args.get("bank_account"),
- 'parent_account': bank_account_group,
- 'is_group':0,
- 'company': company_name,
- "account_type": "Bank",
- })
- try:
- return bank_account.insert()
- except RootNotEditable:
- frappe.throw(_("Bank account cannot be named as {0}").format(args.get("bank_account")))
- except frappe.DuplicateEntryError:
- # bank account same as a CoA entry
- pass
-
def create_email_digest():
from frappe.utils.user import get_system_managers
system_managers = get_system_managers(only_name=True)
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 7ae81d7..cd49a18 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -448,6 +448,8 @@
set_active_domains(args)
update_stock_settings()
update_shopping_cart_settings(args)
+
+ args.update({"set_default": 1})
create_bank_account(args)
def set_global_defaults(args):
@@ -479,17 +481,17 @@
stock_settings.save()
def create_bank_account(args):
- if not args.bank_account:
+ if not args.get('bank_account'):
return
- company_name = args.company_name
+ company_name = args.get('company_name')
bank_account_group = frappe.db.get_value("Account",
{"account_type": "Bank", "is_group": 1, "root_type": "Asset",
"company": company_name})
if bank_account_group:
bank_account = frappe.get_doc({
"doctype": "Account",
- 'account_name': args.bank_account,
+ 'account_name': args.get('bank_account'),
'parent_account': bank_account_group,
'is_group':0,
'company': company_name,
@@ -498,10 +500,13 @@
try:
doc = bank_account.insert()
- frappe.db.set_value("Company", args.company_name, "default_bank_account", bank_account.name, update_modified=False)
+ if args.get('set_default'):
+ frappe.db.set_value("Company", args.get('company_name'), "default_bank_account", bank_account.name, update_modified=False)
+
+ return doc
except RootNotEditable:
- frappe.throw(_("Bank account cannot be named as {0}").format(args.bank_account))
+ frappe.throw(_("Bank account cannot be named as {0}").format(args.get('bank_account')))
except frappe.DuplicateEntryError:
# bank account same as a CoA entry
pass
diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py
index f4fe18e..cbb3dc8 100644
--- a/erpnext/setup/setup_wizard/operations/taxes_setup.py
+++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py
@@ -27,6 +27,7 @@
country_wise_tax = simple_to_detailed(country_wise_tax)
from_detailed_data(company_name, country_wise_tax)
+ update_regional_tax_settings(country, company_name)
def simple_to_detailed(templates):
@@ -86,7 +87,7 @@
if tax_categories:
for tax_category in tax_categories:
- make_tax_catgory(tax_category)
+ make_tax_category(tax_category)
if sales_tax_templates:
for template in sales_tax_templates:
@@ -101,6 +102,17 @@
make_item_tax_template(company_name, template)
+def update_regional_tax_settings(country, company):
+ path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country))
+ if os.path.exists(path.encode("utf-8")):
+ try:
+ module_name = "erpnext.regional.{0}.setup.update_regional_tax_settings".format(frappe.scrub(country))
+ frappe.get_attr(module_name)(country, company)
+ except Exception as e:
+ # Log error and ignore if failed to setup regional tax settings
+ frappe.log_error()
+ pass
+
def make_taxes_and_charges_template(company_name, doctype, template):
template['company'] = company_name
template['doctype'] = doctype
@@ -130,8 +142,14 @@
if fieldname not in tax_row:
tax_row[fieldname] = default_value
- return frappe.get_doc(template).insert(ignore_permissions=True)
+ doc = frappe.get_doc(template)
+ # Data in country wise json is already pre validated, hence validations can be ignored
+ # Ingone validations to make doctypes faster
+ doc.flags.ignore_links = True
+ doc.flags.ignore_validate = True
+ doc.insert(ignore_permissions=True)
+ return doc
def make_item_tax_template(company_name, template):
"""Create an Item Tax Template.
@@ -156,8 +174,14 @@
if 'tax_rate' not in tax_row:
tax_row['tax_rate'] = account_data.get('tax_rate')
- return frappe.get_doc(template).insert(ignore_permissions=True)
+ doc = frappe.get_doc(template)
+ # Data in country wise json is already pre validated, hence validations can be ignored
+ # Ingone validations to make doctypes faster
+ doc.flags.ignore_links = True
+ doc.flags.ignore_validate = True
+ doc.insert(ignore_permissions=True)
+ return doc
def get_or_create_account(company_name, account):
"""
@@ -175,8 +199,7 @@
or_filters={
'account_name': account.get('account_name'),
'account_number': account.get('account_number')
- }
- )
+ })
if existing_accounts:
return frappe.get_doc('Account', existing_accounts[0].name)
@@ -191,8 +214,11 @@
account['root_type'] = root_type
account['is_group'] = 0
- return frappe.get_doc(account).insert(ignore_permissions=True, ignore_mandatory=True)
-
+ doc = frappe.get_doc(account)
+ doc.flags.ignore_links = True
+ doc.flags.ignore_validate = True
+ doc.insert(ignore_permissions=True, ignore_mandatory=True)
+ return doc
def get_or_create_tax_group(company_name, root_type):
# Look for a group account of type 'Tax'
@@ -237,14 +263,18 @@
'account_type': 'Tax',
'account_name': account_name,
'parent_account': root_account.name
- }).insert(ignore_permissions=True)
+ })
+
+ tax_group_account.flags.ignore_links = True
+ tax_group_account.flags.ignore_validate = True
+ tax_group_account.insert(ignore_permissions=True)
tax_group_name = tax_group_account.name
return tax_group_name
-def make_tax_catgory(tax_category):
+def make_tax_category(tax_category):
doctype = 'Tax Category'
if isinstance(tax_category, str):
tax_category = {'title': tax_category}
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index 27237bf..e49259e 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -28,21 +28,21 @@
from frappe.desk.page.setup_wizard.setup_wizard import setup_complete
if not frappe.get_list("Company"):
setup_complete({
- "currency" :"USD",
- "full_name" :"Test User",
- "company_name" :"Wind Power LLC",
- "timezone" :"America/New_York",
- "company_abbr" :"WP",
- "industry" :"Manufacturing",
- "country" :"United States",
- "fy_start_date" :"2011-01-01",
- "fy_end_date" :"2011-12-31",
- "language" :"english",
- "company_tagline" :"Testing",
- "email" :"test@erpnext.com",
- "password" :"test",
+ "currency" :"USD",
+ "full_name" :"Test User",
+ "company_name" :"Wind Power LLC",
+ "timezone" :"America/New_York",
+ "company_abbr" :"WP",
+ "industry" :"Manufacturing",
+ "country" :"United States",
+ "fy_start_date" :"2021-01-01",
+ "fy_end_date" :"2021-12-31",
+ "language" :"english",
+ "company_tagline" :"Testing",
+ "email" :"test@erpnext.com",
+ "password" :"test",
"chart_of_accounts" : "Standard",
- "domains" : ["Manufacturing"],
+ "domains" : ["Manufacturing"],
})
frappe.db.sql("delete from `tabLeave Allocation`")
diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
index 014f409..6ca3d63 100644
--- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
+++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
@@ -11,10 +11,11 @@
"hide_custom": 0,
"icon": "settings",
"idx": 0,
+ "is_default": 0,
"is_standard": 1,
"label": "ERPNext Settings",
"links": [],
- "modified": "2020-12-01 13:38:37.759596",
+ "modified": "2021-06-12 01:58:11.399566",
"modified_by": "Administrator",
"module": "Setup",
"name": "ERPNext Settings",
@@ -109,6 +110,13 @@
"label": "Domain Settings",
"link_to": "Domain Settings",
"type": "DocType"
+ },
+ {
+ "doc_view": "",
+ "icon": "retail",
+ "label": "Products Settings",
+ "link_to": "Products Settings",
+ "type": "DocType"
}
]
-}
\ No newline at end of file
+}
diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py
index 3eab4ff..6c92d96 100644
--- a/erpnext/shopping_cart/product_query.py
+++ b/erpnext/shopping_cart/product_query.py
@@ -87,7 +87,8 @@
filters=self.filters,
or_filters=self.or_filters,
start=start,
- limit=self.page_length
+ limit=self.page_length,
+ order_by="weightage desc"
)
# Combine results having context of website item groups into item results
diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json
index e6d2e13..fc4cf1d 100644
--- a/erpnext/stock/doctype/batch/batch.json
+++ b/erpnext/stock/doctype/batch/batch.json
@@ -193,7 +193,7 @@
"image_field": "image",
"links": [],
"max_attachments": 5,
- "modified": "2021-01-07 11:10:09.149170",
+ "modified": "2021-07-08 16:22:01.343105",
"modified_by": "Administrator",
"module": "Stock",
"name": "Batch",
@@ -217,5 +217,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
- "title_field": "batch_id"
+ "title_field": "batch_id",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index f20e76f..dbfeb4a 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -32,6 +32,8 @@
"contact_info",
"shipping_address_name",
"shipping_address",
+ "dispatch_address_name",
+ "dispatch_address",
"contact_person",
"contact_display",
"contact_mobile",
@@ -1282,13 +1284,28 @@
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
"label": "Disable Rounded Total"
+ },
+ {
+ "fieldname": "dispatch_address_name",
+ "fieldtype": "Link",
+ "label": "Dispatch Address Name",
+ "options": "Address",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "dispatch_address_name",
+ "fieldname": "dispatch_address",
+ "fieldtype": "Small Text",
+ "label": "Dispatch Address",
+ "print_hide": 1,
+ "read_only": 1
}
],
"icon": "fa fa-truck",
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2021-06-11 19:27:30.901112",
+ "modified": "2021-07-08 21:37:20.802652",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 45e3c21..87bd9e6 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -93,17 +93,18 @@
erpnext.item.edit_prices_button(frm);
erpnext.item.toggle_attributes(frm);
-
+
if (!frm.doc.is_fixed_asset) {
erpnext.item.make_dashboard(frm);
}
frm.add_custom_button(__('Duplicate'), function() {
var new_item = frappe.model.copy_doc(frm.doc);
- if(new_item.item_name===new_item.item_code) {
+ // Duplicate item could have different name, causing "copy paste" error.
+ if (new_item.item_name===new_item.item_code) {
new_item.item_name = null;
}
- if(new_item.description===new_item.description) {
+ if (new_item.item_code===new_item.description || new_item.item_code===new_item.description) {
new_item.description = null;
}
frappe.set_route('Form', 'Item', new_item.name);
@@ -186,8 +187,6 @@
item_code: function(frm) {
if(!frm.doc.item_name)
frm.set_value("item_name", frm.doc.item_code);
- if(!frm.doc.description)
- frm.set_value("description", frm.doc.item_code);
},
is_stock_item: function(frm) {
@@ -381,7 +380,8 @@
// Show Stock Levels only if is_stock_item
if (frm.doc.is_stock_item) {
frappe.require('item-dashboard.bundle.js', function() {
- const section = frm.dashboard.add_section('', __("Stock Levels"));
+ frm.dashboard.parent.find('.stock-levels').remove();
+ const section = frm.dashboard.add_section('', __("Stock Levels"), 'stock-levels');
erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({
parent: section,
item_code: frm.doc.name,
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index c7467a5..922049f 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -587,8 +587,8 @@
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,
- company="_Test Company"):
+ 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
@@ -596,6 +596,8 @@
item.description = item_code
item.item_group = "All Item Groups"
item.is_stock_item = is_stock_item
+ item.is_fixed_asset = is_fixed_asset
+ item.asset_category = asset_category
item.opening_stock = opening_stock
item.valuation_rate = valuation_rate
item.is_purchase_item = is_purchase_item
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 5df4d87..bf969f9 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -41,7 +41,7 @@
def validate(self):
self.check_mandatory()
- self.validate_purchase_receipts()
+ self.validate_receipt_documents()
init_landed_taxes_and_totals(self)
self.set_total_taxes_and_charges()
if not self.get("items"):
@@ -56,14 +56,23 @@
frappe.throw(_("Please enter Receipt Document"))
- def validate_purchase_receipts(self):
+ def validate_receipt_documents(self):
receipt_documents = []
for d in self.get("purchase_receipts"):
- if frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus") != 1:
- frappe.throw(_("Receipt document must be submitted"))
- else:
- receipt_documents.append(d.receipt_document)
+ 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"
+ 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 += "<br>" + _("Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled.")
+ frappe.throw(msg, title=_("Incorrect Invoice"))
+
+ receipt_documents.append(d.receipt_document)
for item in self.get("items"):
if not item.receipt_document:
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 3ad9909..026b85e 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -162,8 +162,15 @@
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')
- if d.ordered_qty and d.ordered_qty > d.stock_qty:
+ if mr_qty_allowance:
+ allowed_qty = d.qty + (d.qty * (mr_qty_allowance/100))
+ if d.ordered_qty and d.ordered_qty > allowed_qty:
+ frappe.throw(_("The total Issue / Transfer quantity {0} in Material Request {1} \
+ cannot be greater than allowed requested quantity {2} for Item {3}").format(d.ordered_qty, d.parent, allowed_qty, d.item_code))
+
+ elif d.ordered_qty and d.ordered_qty > d.stock_qty:
frappe.throw(_("The total Issue / Transfer quantity {0} in Material Request {1} \
cannot be greater than requested quantity {2} for Item {3}").format(d.ordered_qty, d.parent, d.qty, d.item_code))
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index 72a3a5e..b4776ba 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -329,6 +329,58 @@
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.company = "_Test Company"
+ mr.scheduled_date = today()
+ mr.append('items',{
+ "item_code": "_Test FG Item",
+ "item_name": "_Test FG Item",
+ "qty": 10,
+ "schedule_date": today(),
+ "uom": "_Test UOM 1",
+ "warehouse": "_Test Warehouse - _TC"
+ })
+
+ mr.material_request_type = "Material Transfer"
+ mr.insert()
+ mr.submit()
+
+ frappe.db.set_value('Stock Settings', None, 'mr_qty_allowance', 20)
+
+ # map a stock entry
+
+ se_doc = make_stock_entry(mr.name)
+ se_doc.update({
+ "posting_date": today(),
+ "posting_time": "00:00",
+ })
+ se_doc.get("items")[0].update({
+ "qty": 13,
+ "transfer_qty": 12.0,
+ "s_warehouse": "_Test Warehouse - _TC",
+ "t_warehouse": "_Test Warehouse 1 - _TC",
+ "basic_rate": 1.0
+ })
+
+ # make available the qty in _Test Warehouse 1 before transfer
+ sr = frappe.new_doc("Stock Reconciliation")
+ sr.company = "_Test Company"
+ sr.purpose = "Opening Stock"
+ sr.append('items', {
+ "item_code": "_Test FG Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 20,
+ "valuation_rate": 0.01
+ })
+ sr.insert()
+ sr.submit()
+ se = frappe.copy_doc(se_doc)
+ se.insert()
+ self.assertRaises(frappe.ValidationError)
+ se.items[0].qty = 12
+ se.submit()
+
def test_completed_qty_for_over_transfer(self):
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")
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 5ba9c70..41800e3 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -291,7 +291,7 @@
continue
self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks,
- stock_rbnb, account_currency=warehouse_account_currency, item=d)
+ stock_rbnb, account_currency=warehouse_account_currency, item=d)
# GL Entry for from warehouse or Stock Received but not billed
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
@@ -318,11 +318,11 @@
(exchange_rate_map[d.purchase_invoice] - self.conversion_rate)
self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, discrepancy_caused_by_exchange_rate_difference,
- remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
+ remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
account_currency=credit_currency, item=d)
- self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0,
- remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
+ self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0,
+ remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
account_currency=credit_currency, item=d)
# Amount added through landed-cos-voucher
@@ -407,6 +407,7 @@
against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0])
total_valuation_amount = sum(valuation_tax.values())
amount_including_divisional_loss = negative_expense_to_be_booked
+ stock_rbnb = self.get_company_default("stock_received_but_not_billed")
i = 1
for tax in self.get("taxes"):
if valuation_tax.get(tax.name):
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index d56822a..dbba21f 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -1054,30 +1054,30 @@
def test_purchase_receipt_with_exchange_rate_difference(self):
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice
-
- pi = create_purchase_invoice(currency = "USD", conversion_rate = 70)
-
- create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory",
- properties={"account": '_Test Account Stock In Hand - TCP1'})
+ from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_purchase_receipt as create_purchase_receipt
- pr = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1',
- company="_Test Company with perpetual inventory", currency = "USD", conversion_rate = 80,
- do_not_save = "True")
-
+ pi = create_purchase_invoice(company="_Test Company with perpetual inventory",
+ cost_center = "Main - TCP1",
+ warehouse = "Stores - TCP1",
+ expense_account ="_Test Account Cost for Goods Sold - TCP1",
+ currency = "USD", conversion_rate = 70)
+
+ pr = create_purchase_receipt(pi.name)
+ pr.conversion_rate = 80
pr.items[0].purchase_invoice = pi.name
pr.items[0].purchase_invoice_item = pi.items[0].name
- pr.insert()
+ pr.save()
pr.submit()
- # fetching the latest GL Entry with 'Exchange Gain/Loss - TCP1' account
- gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - TCP1'})
- voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no')
- self.assertEqual(pr.name, voucher_no)
+ # Get exchnage gain and loss account
+ exchange_gain_loss_account = frappe.db.get_value('Company', pr.company, 'exchange_gain_loss_account')
- exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit')
+ # fetching the latest GL Entry with exchange gain and loss account account
+ amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pr.name}, 'credit')
discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount)
- self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff)
+
+ self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
def get_sl_entries(voucher_type, voucher_no):
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py
index ea26cac..0f50bcd 100644
--- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py
@@ -97,7 +97,7 @@
at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse)
if not rules:
- warehouse = source_warehouse or item.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])
diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
index 7f3d701..f5d076a 100644
--- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
@@ -14,7 +14,7 @@
)
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import create_item
-from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
# test_records = frappe.get_test_records('Quality Inspection')
@@ -159,6 +159,47 @@
frappe.delete_doc("Quality Inspection", qi)
dn.delete()
+ def test_rejected_qi_validation(self):
+ """Test if rejected QI blocks Stock Entry as per Stock Settings."""
+ se = make_stock_entry(
+ item_code="_Test Item with QA",
+ target="_Test Warehouse - _TC",
+ qty=1,
+ basic_rate=100,
+ inspection_required=True,
+ do_not_submit=True
+ )
+
+ readings = [
+ {
+ "specification": "Iron Content",
+ "min_value": 0.1,
+ "max_value": 0.9,
+ "reading_1": "0.4"
+ }
+ ]
+
+ qa = create_quality_inspection(
+ reference_type="Stock Entry",
+ reference_name=se.name,
+ readings=readings,
+ status="Rejected"
+ )
+
+ frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop")
+ se.reload()
+ self.assertRaises(QualityInspectionRejectedError, se.submit) # when blocked in Stock settings, block rejected QI
+
+ frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Warn")
+ se.reload()
+ se.submit() # when allowed in Stock settings, allow rejected QI
+
+ # teardown
+ qa.reload()
+ qa.cancel()
+ se.reload()
+ se.cancel()
+ frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop")
def create_quality_inspection(**args):
args = frappe._dict(args)
@@ -175,12 +216,11 @@
if not args.readings:
create_quality_inspection_parameter("Size")
readings = {"specification": "Size", "min_value": 0, "max_value": 10}
+ if args.status == "Rejected":
+ readings["reading_1"] = "12" # status is auto set in child on save
else:
readings = args.readings
- if args.status == "Rejected":
- readings["reading_1"] = "12" # status is auto set in child on save
-
if isinstance(readings, list):
for entry in readings:
create_quality_inspection_parameter(entry["specification"])
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 55f2ebb..5f31d9c 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -133,6 +133,6 @@
def get_repost_item_valuation_entries():
return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation`
- WHERE status != 'Completed' and creation <= %s and docstatus = 1
+ WHERE status in ('Queued', 'In Progress') and creation <= %s and docstatus = 1
ORDER BY timestamp(posting_date, posting_time) asc, creation asc
""", now(), as_dict=1)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 8f27ef4..fcb6f0f 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -72,7 +72,7 @@
self.validate_with_material_request()
self.validate_batch()
self.validate_inspection()
- self.validate_fg_completed_qty()
+ # self.validate_fg_completed_qty()
self.validate_difference_account()
self.set_job_card_data()
self.set_purpose_for_stock_entry()
@@ -529,7 +529,7 @@
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 frappe.db.get_single_value("Manufacturing Settings", "material_consumption"):
+ if 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()])
@@ -719,6 +719,10 @@
frappe.throw(_("Multiple items cannot be marked as finished item"))
if self.purpose == "Manufacture":
+ if not finished_items:
+ frappe.throw(_('Finished Good has not set in the stock entry {0}')
+ .format(self.name))
+
allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
"overproduction_percentage_for_work_order"))
@@ -1090,13 +1094,13 @@
"is_finished_item": 1
}
- if self.work_order and self.pro_doc.has_batch_no:
+ 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_finisged_goods(args, item)
+ self.add_finished_goods(args, item)
def set_batchwise_finished_goods(self, args, item):
- qty = flt(self.fg_completed_qty)
filters = {
"reference_name": self.pro_doc.name,
"reference_doctype": self.pro_doc.doctype,
@@ -1105,7 +1109,17 @@
fields = ["qty_to_produce as qty", "produced_qty", "name"]
- for row in 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)
+ else:
+ self.add_batchwise_finished_good(data, args, item)
+
+ def add_batchwise_finished_good(self, data, args, item):
+ qty = flt(self.fg_completed_qty)
+
+ for row in data:
batch_qty = flt(row.qty) - flt(row.produced_qty)
if not batch_qty:
continue
@@ -1121,9 +1135,9 @@
args["qty"] = fg_qty
args["batch_no"] = row.name
- self.add_finisged_goods(args, item)
+ self.add_finished_goods(args, item)
- def add_finisged_goods(self, args, item):
+ def add_finished_goods(self, args, item):
self.add_to_stock_entry_detail({
item.name: args
}, bom_no = self.bom_no)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
index b12a854..563fcb0 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
@@ -45,6 +45,8 @@
s.posting_date = args.posting_date
if args.posting_time:
s.posting_time = args.posting_time
+ if args.inspection_required:
+ s.inspection_required = args.inspection_required
# map names
if args.from_warehouse:
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index a178283..22f412a 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -307,6 +307,7 @@
"fieldname": "quality_inspection",
"fieldtype": "Link",
"label": "Quality Inspection",
+ "no_copy": 1,
"options": "Quality Inspection"
},
{
@@ -548,7 +549,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-04-22 20:08:23.799715",
+ "modified": "2021-06-21 16:03:18.834880",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",
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 0febcb6..93482e8 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -89,17 +89,16 @@
if item_det.is_stock_item != 1:
frappe.throw(_("Item {0} must be a stock Item").format(self.item_code))
- # check if batch number is required
- if self.voucher_type != 'Stock Reconciliation':
- 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
- 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))
+ # 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
+ 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 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))
+ 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),
@@ -178,3 +177,4 @@
frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"])
frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"])
+ frappe.db.add_index("Stock Ledger Entry", ["voucher_detail_no"])
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 ba31ad7..af2ada8 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
@@ -54,7 +54,7 @@
)
# _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020
- make_stock_entry(
+ se = make_stock_entry(
item_code="_Test Item for Reposting",
source="Stores - _TC",
target="Finished Goods - _TC",
@@ -64,29 +64,29 @@
posting_date='2020-04-30',
posting_time='14:00'
)
- target_wh_sle = get_previous_sle({
+ target_wh_sle = frappe.db.get_value('Stock Ledger Entry', {
"item_code": "_Test Item for Reposting",
"warehouse": "Finished Goods - _TC",
- "posting_date": '2020-04-30',
- "posting_time": '14:00'
- })
+ "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')
- finished_item_sle = get_previous_sle({
+ finished_item_sle = frappe.db.get_value('Stock Ledger Entry', {
"item_code": "_Test Finished Item for Reposting",
"warehouse": "Finished Goods - _TC",
- "posting_date": '2020-05-05',
- "posting_time": '14:00'
- })
+ "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)
# Reconciliation for _Test Item for Reposting at Stores on 12-04-2020: Qty = 50, Rate = 150
- create_stock_reconciliation(
+ sr = create_stock_reconciliation(
item_code="_Test Item for Reposting",
warehouse="Stores - _TC",
qty=50,
@@ -109,12 +109,12 @@
self.assertEqual(target_wh_sle.get("valuation_rate"), 175)
# Check valuation rate of repacked item after back-dated entry at Stores
- finished_item_sle = get_previous_sle({
+ finished_item_sle = frappe.db.get_value('Stock Ledger Entry', {
"item_code": "_Test Finished Item for Reposting",
"warehouse": "Finished Goods - _TC",
- "posting_date": '2020-05-05',
- "posting_time": '14:00'
- })
+ "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)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index 76a3f1a..4540954 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -17,6 +17,14 @@
}
}
});
+ frm.set_query("batch_no", "items", function(doc, cdt, cdn) {
+ var item = locals[cdt][cdn];
+ return {
+ filters: {
+ 'item': item.item_code
+ }
+ };
+ });
if (frm.doc.company) {
erpnext.queries.setup_queries(frm, "Warehouse", function() {
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 2956384..9875491 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -357,6 +357,7 @@
if row.current_qty:
data.actual_qty = -1 * row.current_qty
data.qty_after_transaction = flt(row.current_qty)
+ data.previous_qty_after_transaction = flt(row.qty)
data.valuation_rate = flt(row.current_valuation_rate)
data.stock_value = data.qty_after_transaction * data.valuation_rate
data.stock_value_difference = -1 * flt(row.amount_difference)
@@ -404,17 +405,18 @@
key = (d.item_code, d.warehouse)
if key not in merge_similar_entries:
+ 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.valuation_rate = (data.valuation_rate + d.valuation_rate) / data.actual_qty
+ 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
- if data.incoming_rate:
- data.incoming_rate = (data.incoming_rate + d.incoming_rate) / data.actual_qty
+ data.incoming_rate = (data.total_amount) / data.actual_qty
for key, value in merge_similar_entries.items():
new_sl_entries.append(value)
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 36380b8..c192582 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -6,7 +6,7 @@
from __future__ import unicode_literals
import frappe, unittest
-from frappe.utils import flt, nowdate, nowtime
+from frappe.utils import flt, nowdate, nowtime, random_string, add_days
from erpnext.accounts.utils import get_stock_and_account_balance
from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items
@@ -14,6 +14,8 @@
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+
class TestStockReconciliation(unittest.TestCase):
@classmethod
@@ -150,6 +152,42 @@
stock_doc = frappe.get_doc("Stock Reconciliation", d)
stock_doc.cancel()
+
+ def test_stock_reco_for_merge_serialized_item(self):
+ to_delete_records = []
+
+ # Add new serial nos
+ 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')
+
+ 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.save()
+ sr.submit()
+
+ 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)
+
+ to_delete_records.append(sr.name)
+ to_delete_records.reverse()
+
+ for d in to_delete_records:
+ stock_doc = frappe.get_doc("Stock Reconciliation", d)
+ stock_doc.cancel()
+
def test_stock_reco_for_batch_item(self):
to_delete_records = []
to_delete_serial_nos = []
@@ -204,6 +242,137 @@
self.assertEqual(sr.get("items")[0].valuation_rate, 0)
self.assertEqual(sr.get("items")[0].amount, 0)
+ 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
+ """
+ item_code = "Backdated-Reco-Item"
+ 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_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")
+ 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")
+ self.assertEqual(pr1_balance, 18)
+ self.assertEqual(pr2_balance, 19)
+ 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")
+ self.assertEqual(pr1_balance, 10)
+ self.assertEqual(pr2_balance, 11)
+ self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
+
+ # teardown
+ sr4.cancel()
+ pr3.cancel()
+ pr2.cancel()
+ pr1.cancel()
+
+ 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)
+ """
+ from erpnext.stock.stock_ledger import NegativeStockError
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+
+ item_code = "Backdated-Reco-Item"
+ warehouse = "_Test Warehouse - _TC"
+ create_item(item_code)
+
+ negative_stock_setting = frappe.db.get_single_value("Stock Settings", "allow_negative_stock")
+ frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 0)
+
+ 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")
+ 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)
+ self.assertRaises(NegativeStockError, sr3.submit)
+
+ # teardown
+ frappe.db.set_value("Stock Settings", None, "allow_negative_stock", negative_stock_setting)
+ sr3.cancel()
+ dn2.cancel()
+ pr1.cancel()
+
+ 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)
+ self.assertRaises(frappe.ValidationError, sr.submit)
+
+def create_batch_item_with_batch(item_name, batch_id):
+ batch_item_doc = create_item(item_name, is_stock_item=1)
+ if not batch_item_doc.has_batch_no:
+ batch_item_doc.has_batch_no = 1
+ 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')
+ b.item = item_name
+ b.batch_id = batch_id
+ b.save()
+
def insert_existing_sle(warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -231,6 +400,12 @@
serial_item_doc.serial_no_series = "SRSI.####"
serial_item_doc.save(ignore_permissions=True)
+ serial_item_doc = create_item("Stock-Reco-Serial-Item-2", is_stock_item=1)
+ if not serial_item_doc.has_serial_no:
+ serial_item_doc.has_serial_no = 1
+ serial_item_doc.serial_no_series = "SRSII.####"
+ serial_item_doc.save(ignore_permissions=True)
+
batch_item_doc = create_item("Stock-Reco-batch-Item-1", is_stock_item=1)
if not batch_item_doc.has_batch_no:
batch_item_doc.has_batch_no = 1
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index cf5d98d..f75cb56 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -18,12 +18,16 @@
"section_break_9",
"over_delivery_receipt_allowance",
"role_allowed_to_over_deliver_receive",
+ "mr_qty_allowance",
"column_break_12",
"auto_insert_price_list_rate_if_missing",
"allow_negative_stock",
"show_barcode_field",
"clean_description_html",
+ "quality_inspection_settings_section",
"action_if_quality_inspection_is_not_submitted",
+ "column_break_21",
+ "action_if_quality_inspection_is_rejected",
"section_break_7",
"automatically_set_serial_nos_based_on_fifo",
"set_qty_in_transactions_based_on_serial_no_input",
@@ -264,6 +268,28 @@
{
"fieldname": "column_break_31",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "quality_inspection_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Quality Inspection Settings"
+ },
+ {
+ "fieldname": "column_break_21",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "Stop",
+ "fieldname": "action_if_quality_inspection_is_rejected",
+ "fieldtype": "Select",
+ "label": "Action If Quality Inspection Is Rejected",
+ "options": "Stop\nWarn"
+ },
+ {
+ "description": "The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units.",
+ "fieldname": "mr_qty_allowance",
+ "fieldtype": "Float",
+ "label": "Over Transfer Allowance"
}
],
"icon": "icon-cog",
@@ -271,7 +297,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-04-30 17:27:42.709231",
+ "modified": "2021-06-28 17:02:26.683002",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",
@@ -291,4 +317,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index ca174a3..cf52803 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -74,9 +74,8 @@
update_party_blanket_order(args, out)
- if not doc or cint(doc.get('is_return')) == 0:
- # get price list rate only if the invoice is not a credit or debit note
- get_price_list_rate(args, item, out)
+
+ get_price_list_rate(args, item, out)
if args.customer and cint(args.is_pos):
out.update(get_pos_profile_item_details(args.company, args, update_data=True))
@@ -441,7 +440,7 @@
if item_tax_templates is None:
item_tax_templates = {}
-
+
if item_rates is None:
item_rates = {}
@@ -807,10 +806,14 @@
def validate_conversion_rate(args, meta):
from erpnext.controllers.accounts_controller import validate_conversion_rate
- if (not args.conversion_rate
- and args.currency==frappe.get_cached_value('Company', args.company, "default_currency")):
+ company_currency = frappe.get_cached_value('Company', args.company, "default_currency")
+ if (not args.conversion_rate and args.currency==company_currency):
args.conversion_rate = 1.0
+ if (not args.ignore_conversion_rate and args.conversion_rate == 1 and args.currency!=company_currency):
+ args.conversion_rate = get_exchange_rate(args.currency,
+ company_currency, args.transaction_date, "for_buying") or 1.0
+
# validate currency conversion rate
validate_conversion_rate(args.currency, args.conversion_rate,
meta.get_label("conversion_rate"), args.company)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 9fe89c3..c15d1ed 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -6,13 +6,14 @@
import erpnext
import copy
from frappe import _
-from frappe.utils import cint, flt, cstr, now, get_link_to_form
+from frappe.utils import cint, flt, cstr, now, get_link_to_form, getdate
from frappe.model.meta import get_field_precision
from erpnext.stock.utils import get_valuation_method, get_incoming_outgoing_rate_for_cancel
from erpnext.stock.utils import get_bin
import json
from six import iteritems
+
# future reposting
class NegativeStockError(frappe.ValidationError): pass
class SerialNoExistsInFutureTransaction(frappe.ValidationError):
@@ -55,6 +56,11 @@
sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
args = sle_doc.as_dict()
+
+ if sle.get("voucher_type") == "Stock Reconciliation":
+ # preserve previous_qty_after_transaction for qty reposting
+ args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
+
update_bin(args, allow_negative_stock, via_landed_cost_voucher)
def get_args_for_future_sle(row):
@@ -125,7 +131,13 @@
if not args and voucher_type and voucher_no:
args = get_args_for_voucher(voucher_type, voucher_no)
- distinct_item_warehouses = [(d.item_code, d.warehouse) for d in args]
+ distinct_item_warehouses = {}
+ 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
+ }))
i = 0
while i < len(args):
@@ -134,13 +146,21 @@
"warehouse": args[i].warehouse,
"posting_date": args[i].posting_date,
"posting_time": args[i].posting_time,
- "creation": args[i].get("creation")
+ "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)
- for item_wh, new_sle in iteritems(obj.new_items):
- if item_wh not in distinct_item_warehouses:
- args.append(new_sle)
+ distinct_item_warehouses[(args[i].item_code, args[i].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):
+ data.args_idx = len(args)
+ args.append(data.sle)
+ elif data.sle_changed and not data.reposting_status:
+ args[data.args_idx] = data.sle
+
+ data.sle_changed = False
i += 1
def get_args_for_voucher(voucher_type, voucher_no):
@@ -181,11 +201,12 @@
self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
self.get_precision()
self.valuation_method = get_valuation_method(self.item_code)
- self.new_items = {}
+
+ self.new_items_found = False
+ self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
self.data = frappe._dict()
self.initialize_previous_data(self.args)
-
self.build()
def get_precision(self):
@@ -215,7 +236,7 @@
"""
self.data.setdefault(args.warehouse, frappe._dict())
warehouse_dict = self.data[args.warehouse]
- previous_sle = self.get_previous_sle_of_current_voucher(args)
+ previous_sle = get_previous_sle_of_current_voucher(args)
warehouse_dict.previous_sle = previous_sle
for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
@@ -227,29 +248,6 @@
"stock_value_difference": 0.0
})
- def get_previous_sle_of_current_voucher(self, args):
- """get stock ledger entries filtered by specific posting datetime conditions"""
-
- args['time_format'] = '%H:%i:%s'
- if not args.get("posting_date"):
- args["posting_date"] = "1900-01-01"
- if not args.get("posting_time"):
- args["posting_time"] = "00:00"
-
- sle = frappe.db.sql("""
- select *, timestamp(posting_date, posting_time) as "timestamp"
- from `tabStock Ledger Entry`
- where item_code = %(item_code)s
- and warehouse = %(warehouse)s
- and is_cancelled = 0
- 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""", args, as_dict=1)
-
- return sle[0] if sle else frappe._dict()
-
-
def build(self):
from erpnext.controllers.stock_controller import future_sle_exists
@@ -314,11 +312,29 @@
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:
- if (dependant_sle.item_code, dependant_sle.warehouse) not in self.new_items:
- self.new_items[(dependant_sle.item_code, dependant_sle.warehouse)] = dependant_sle
+ self.update_distinct_item_warehouses(dependant_sle)
return entries_to_fix
elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
return entries_to_fix
+ else:
+ return self.append_future_sle_for_dependant(dependant_sle, entries_to_fix)
+
+ def update_distinct_item_warehouses(self, dependant_sle):
+ key = (dependant_sle.item_code, dependant_sle.warehouse)
+ 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")
+ if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
+ val.sle_changed = True
+ self.distinct_item_warehouses[key] = val
+ self.new_items_found = True
+
+ 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 \
@@ -411,6 +427,7 @@
rate = 0
# Material Transfer, Repack, Manufacturing
if sle.voucher_type == "Stock Entry":
+ 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"):
@@ -460,7 +477,11 @@
frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
# Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
- stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no, for_update=True)
+ if not sle.dependant_sle_voucher_detail_no:
+ self.recalculate_amounts_in_stock_entry(sle.voucher_no)
+
+ def recalculate_amounts_in_stock_entry(self, voucher_no):
+ stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
stock_entry.db_update()
for d in stock_entry.items:
@@ -734,6 +755,35 @@
bin_doc.flags.via_stock_ledger_entry = True
bin_doc.save(ignore_permissions=True)
+
+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'
+ if not args.get("posting_date"):
+ args["posting_date"] = "1900-01-01"
+ if not args.get("posting_time"):
+ args["posting_time"] = "00:00"
+
+ voucher_condition = ""
+ if exclude_current_voucher:
+ voucher_no = args.get("voucher_no")
+ voucher_condition = f"and voucher_no != '{voucher_no}'"
+
+ sle = frappe.db.sql("""
+ select *, timestamp(posting_date, posting_time) as "timestamp"
+ from `tabStock Ledger Entry`
+ where item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and is_cancelled = 0
+ {voucher_condition}
+ 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)
+
+ 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,
@@ -862,9 +912,24 @@
return valuation_rate
def update_qty_in_future_sle(args, allow_negative_stock=None):
+ """Recalculate Qty after Transaction in future SLEs based on current SLE."""
+ datetime_limit_condition = ""
+ qty_shift = args.actual_qty
+
+ # find difference/shift in qty caused by stock reconciliation
+ if args.voucher_type == "Stock Reconciliation":
+ qty_shift = get_stock_reco_qty_shift(args)
+
+ # find the next nearest stock reco so that we only recalculate SLEs till that point
+ next_stock_reco_detail = get_next_stock_reco(args)
+ if next_stock_reco_detail:
+ detail = next_stock_reco_detail[0]
+ # add condition to update SLEs before this date & time
+ datetime_limit_condition = get_datetime_limit_condition(detail)
+
frappe.db.sql("""
update `tabStock Ledger Entry`
- set qty_after_transaction = qty_after_transaction + {qty}
+ set qty_after_transaction = qty_after_transaction + {qty_shift}
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
@@ -876,15 +941,70 @@
and creation > %(creation)s
)
)
- """.format(qty=args.actual_qty), args)
+ {datetime_limit_condition}
+ """.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"):
+ if args.get("previous_qty_after_transaction"):
+ # get qty (balance) that was set at submission
+ last_balance = args.get("previous_qty_after_transaction")
+ stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
+ else:
+ 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")
+
+ if last_balance is not None:
+ stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
+ else:
+ stock_reco_qty_shift = args.qty_after_transaction
+
+ return stock_reco_qty_shift
+
+def get_next_stock_reco(args):
+ """Returns next nearest stock reconciliaton's details."""
+
+ return frappe.db.sql("""
+ select
+ name, posting_date, posting_time, creation, voucher_no
+ from
+ `tabStock Ledger Entry`
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and voucher_type = 'Stock Reconciliation'
+ and voucher_no != %(voucher_no)s
+ and is_cancelled = 0
+ and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
+ or (
+ timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
+ and creation > %(creation)s
+ )
+ )
+ limit 1
+ """, args, as_dict=1)
+
+def get_datetime_limit_condition(detail):
+ return f"""
+ and
+ (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
+ or (
+ timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
+ and creation < '{detail.creation}'
+ )
+ )"""
+
def validate_negative_qty_in_future_sle(args, allow_negative_stock=None):
allow_negative_stock = allow_negative_stock \
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
- if args.actual_qty < 0 and not allow_negative_stock:
+ if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock:
sle = get_future_sle_with_negative_qty(args)
if sle:
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 8a6a3a3..b57b2aa 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -314,13 +314,16 @@
for row_idx, row in enumerate(result):
data = row.items() if is_dict_obj else enumerate(row)
for key, value in data:
- if key not in convertible_columns or not conversion_factors[row_idx-1]:
+ if key not in convertible_columns:
continue
+ # If no conversion factor for the UOM, defaults to 1
+ if not conversion_factors[row_idx]:
+ conversion_factors[row_idx] = 1
if convertible_columns.get(key) == 'rate':
- new_value = flt(value) * conversion_factors[row_idx-1]
+ new_value = flt(value) * conversion_factors[row_idx]
else:
- new_value = flt(value) / conversion_factors[row_idx-1]
+ new_value = flt(value) / conversion_factors[row_idx]
if not is_dict_obj:
row.insert(key+1, new_value)
@@ -386,4 +389,4 @@
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)
\ No newline at end of file
+ frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1)
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index e092b07..b9a65b6 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -5,10 +5,10 @@
import frappe
import json
from frappe import _
-from frappe import utils
from frappe.model.document import Document
-from frappe.utils import now_datetime
-from datetime import datetime, timedelta
+from frappe.utils import now_datetime, time_diff_in_seconds, get_datetime, date_diff
+from frappe.core.utils import get_parent_doc
+from datetime import timedelta
from frappe.model.mapper import get_mapped_doc
from frappe.utils.user import is_website_user
from frappe.email.inbox import link_communication_to_document
@@ -26,9 +26,6 @@
self.set_lead_contact(self.raised_by)
- if not self.service_level_agreement:
- self.reset_sla_fields()
-
def on_update(self):
# Add a communication in the issue timeline
if self.flags.create_communication and self.via_customer_portal:
@@ -54,106 +51,6 @@
self.company = frappe.db.get_value("Lead", self.lead, "company") or \
frappe.db.get_default("Company")
- def reset_sla_fields(self):
- self.agreement_status = ""
- self.response_by = ""
- self.resolution_by = ""
- self.response_by_variance = 0
- self.resolution_by_variance = 0
-
- def update_status(self):
- status = frappe.db.get_value("Issue", self.name, "status")
- if self.status != "Open" and status == "Open" and not self.first_responded_on:
- self.first_responded_on = frappe.flags.current_time or now_datetime()
-
- if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]:
- self.resolution_date = frappe.flags.current_time or now_datetime()
- if frappe.db.get_value("Issue", self.name, "agreement_status") == "Ongoing":
- set_service_level_agreement_variance(issue=self.name)
- self.update_agreement_status()
- set_resolution_time(issue=self)
- set_user_resolution_time(issue=self)
-
- if self.status == "Open" and status != "Open":
- # if no date, it should be set as None and not a blank string "", as per mysql strict config
- self.resolution_date = None
- self.reset_issue_metrics()
- # enable SLA and variance on Reopen
- self.agreement_status = "Ongoing"
- set_service_level_agreement_variance(issue=self.name)
-
- self.handle_hold_time(status)
-
- 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})
- 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()
- 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
-
- # 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:
- hold_time = self.total_hold_time if self.total_hold_time else 0
- now_time = frappe.flags.current_time or now_datetime()
- last_hold_time = 0
- 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
-
- # re-calculate SLA variables after issue changes from any hold status to any non-hold status
- # add hold time to SLA variables
- start_date_time = get_datetime(self.service_level_agreement_creation)
- priority = get_priority(self)
- 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 = 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
-
- 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
-
- 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:
-
- self.agreement_status = "Failed"
- else:
- self.agreement_status = "Fulfilled"
-
- def update_agreement_status_on_custom_status(self):
- """
- 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
- 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
- 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"
-
def create_communication(self):
communication = frappe.new_doc("Communication")
communication.update({
@@ -315,6 +212,128 @@
return issue.name
+def get_time_in_timedelta(time):
+ """
+ Converts datetime.time(10, 36, 55, 961454) to datetime.timedelta(seconds=38215)
+ """
+ return timedelta(hours=time.hour, minutes=time.minute, seconds=time.second)
+
+def set_first_response_time(communication, method):
+ if communication.get('reference_doctype') == "Issue":
+ issue = get_parent_doc(communication)
+ if is_first_response(issue):
+ first_response_time = calculate_first_response_time(issue, get_datetime(issue.first_responded_on))
+ issue.db_set("first_response_time", first_response_time)
+
+def is_first_response(issue):
+ responses = frappe.get_all('Communication', filters = {'reference_name': issue.name, 'sent_or_received': 'Sent'})
+ if len(responses) == 1:
+ return True
+ return False
+
+def calculate_first_response_time(issue, first_responded_on):
+ issue_creation_date = issue.creation
+ issue_creation_time = get_time_in_seconds(issue_creation_date)
+ first_responded_on_in_seconds = get_time_in_seconds(first_responded_on)
+ support_hours = frappe.get_cached_doc("Service Level Agreement", issue.service_level_agreement).support_and_resolution
+
+ if issue_creation_date.day == first_responded_on.day:
+ if is_work_day(issue_creation_date, support_hours):
+ start_time, end_time = get_working_hours(issue_creation_date, support_hours)
+
+ # issue creation and response on the same day during working hours
+ if is_during_working_hours(issue_creation_date, support_hours) and is_during_working_hours(first_responded_on, support_hours):
+ return get_elapsed_time(issue_creation_date, first_responded_on)
+
+ # issue creation is during working hours, but first response was after working hours
+ elif is_during_working_hours(issue_creation_date, support_hours):
+ return get_elapsed_time(issue_creation_time, end_time)
+
+ # issue creation was before working hours but first response is during working hours
+ elif is_during_working_hours(first_responded_on, support_hours):
+ return get_elapsed_time(start_time, first_responded_on_in_seconds)
+
+ # both issue creation and first response were after working hours
+ else:
+ return 1.0 # this should ideally be zero, but it gets reset when the next response is sent if the value is zero
+
+ else:
+ return 1.0
+
+ else:
+ # response on the next day
+ if date_diff(first_responded_on, issue_creation_date) == 1:
+ first_response_time = 0
+ else:
+ first_response_time = calculate_initial_frt(issue_creation_date, date_diff(first_responded_on, issue_creation_date)- 1, support_hours)
+
+ # time taken on day of issue creation
+ if is_work_day(issue_creation_date, support_hours):
+ start_time, end_time = get_working_hours(issue_creation_date, support_hours)
+
+ if is_during_working_hours(issue_creation_date, support_hours):
+ first_response_time += get_elapsed_time(issue_creation_time, end_time)
+ elif is_before_working_hours(issue_creation_date, support_hours):
+ first_response_time += get_elapsed_time(start_time, end_time)
+
+ # time taken on day of first response
+ if is_work_day(first_responded_on, support_hours):
+ start_time, end_time = get_working_hours(first_responded_on, support_hours)
+
+ if is_during_working_hours(first_responded_on, support_hours):
+ first_response_time += get_elapsed_time(start_time, first_responded_on_in_seconds)
+ elif not is_before_working_hours(first_responded_on, support_hours):
+ first_response_time += get_elapsed_time(start_time, end_time)
+
+ if first_response_time:
+ return first_response_time
+ else:
+ return 1.0
+
+def get_time_in_seconds(date):
+ return timedelta(hours=date.hour, minutes=date.minute, seconds=date.second)
+
+def get_working_hours(date, support_hours):
+ if is_work_day(date, support_hours):
+ weekday = frappe.utils.get_weekday(date)
+ for day in support_hours:
+ if day.workday == weekday:
+ return day.start_time, day.end_time
+
+def is_work_day(date, support_hours):
+ weekday = frappe.utils.get_weekday(date)
+ for day in support_hours:
+ if day.workday == weekday:
+ return True
+ return False
+
+def is_during_working_hours(date, support_hours):
+ start_time, end_time = get_working_hours(date, support_hours)
+ time = get_time_in_seconds(date)
+ if time >= start_time and time <= end_time:
+ return True
+ return False
+
+def get_elapsed_time(start_time, end_time):
+ return round(time_diff_in_seconds(end_time, start_time), 2)
+
+def calculate_initial_frt(issue_creation_date, days_in_between, support_hours):
+ initial_frt = 0
+ for i in range(days_in_between):
+ date = issue_creation_date + timedelta(days = (i+1))
+ if is_work_day(date, support_hours):
+ start_time, end_time = get_working_hours(date, support_hours)
+ initial_frt += get_elapsed_time(start_time, end_time)
+
+ return initial_frt
+
+def is_before_working_hours(date, support_hours):
+ start_time, end_time = get_working_hours(date, support_hours)
+ time = get_time_in_seconds(date)
+ if time < start_time:
+ return True
+ return False
+
def get_holidays(holiday_list_name):
holiday_list = frappe.get_cached_doc("Holiday List", holiday_list_name)
holidays = [holiday.holiday_date for holiday in holiday_list.holidays]
diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py
index 7b9b144..84f8c39 100644
--- a/erpnext/support/doctype/issue/test_issue.py
+++ b/erpnext/support/doctype/issue/test_issue.py
@@ -5,16 +5,18 @@
import frappe
import unittest
from erpnext.support.doctype.service_level_agreement.test_service_level_agreement import create_service_level_agreements_for_issues
-from frappe.utils import now_datetime, get_datetime, flt
+from frappe.core.doctype.user_permission.test_user_permission import create_user
+from frappe.utils import get_datetime, flt
import datetime
from datetime import timedelta
-class TestIssue(unittest.TestCase):
+class TestSetUp(unittest.TestCase):
def setUp(self):
frappe.db.sql("delete from `tabService Level Agreement`")
frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1)
create_service_level_agreements_for_issues()
+class TestIssue(TestSetUp):
def test_response_time_and_resolution_time_based_on_different_sla(self):
creation = datetime.datetime(2019, 3, 4, 12, 0)
@@ -133,6 +135,223 @@
issue.reload()
self.assertEqual(flt(issue.total_hold_time, 2), 2700)
+class TestFirstResponseTime(TestSetUp):
+ # working hours used in all cases: Mon-Fri, 10am to 6pm
+ # all dates are in the mm-dd-yyyy format
+
+ # issue creation and first response are on the same day
+ def test_first_response_time_case1(self):
+ """
+ Test frt when issue creation and first response are during working hours on the same day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 11:00"), get_datetime("06-28-2021 12:00"))
+ self.assertEqual(issue.first_response_time, 3600.0)
+
+ def test_first_response_time_case2(self):
+ """
+ Test frt when issue creation was during working hours, but first response is sent after working hours on the same day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("06-28-2021 20:00"))
+ self.assertEqual(issue.first_response_time, 21600.0)
+
+ def test_first_response_time_case3(self):
+ """
+ Test frt when issue creation was before working hours but first response is sent during working hours on the same day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("06-28-2021 12:00"))
+ self.assertEqual(issue.first_response_time, 7200.0)
+
+ def test_first_response_time_case4(self):
+ """
+ Test frt when both issue creation and first response were after working hours on the same day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 19:00"), get_datetime("06-28-2021 20:00"))
+ self.assertEqual(issue.first_response_time, 1.0)
+
+ def test_first_response_time_case5(self):
+ """
+ Test frt when both issue creation and first response are on the same day, but it's not a work day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-27-2021 10:00"), get_datetime("06-27-2021 11:00"))
+ self.assertEqual(issue.first_response_time, 1.0)
+
+ # issue creation and first response are on consecutive days
+ def test_first_response_time_case6(self):
+ """
+ Test frt when the issue was created before working hours and the first response is also sent before working hours, but on the next day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("06-29-2021 6:00"))
+ self.assertEqual(issue.first_response_time, 28800.0)
+
+ def test_first_response_time_case7(self):
+ """
+ Test frt when the issue was created before working hours and the first response is sent during working hours, but on the next day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("06-29-2021 11:00"))
+ self.assertEqual(issue.first_response_time, 32400.0)
+
+ def test_first_response_time_case8(self):
+ """
+ Test frt when the issue was created before working hours and the first response is sent after working hours, but on the next day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("06-29-2021 20:00"))
+ self.assertEqual(issue.first_response_time, 57600.0)
+
+ def test_first_response_time_case9(self):
+ """
+ Test frt when the issue was created before working hours and the first response is sent on the next day, which is not a work day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-25-2021 6:00"), get_datetime("06-26-2021 11:00"))
+ self.assertEqual(issue.first_response_time, 28800.0)
+
+ def test_first_response_time_case10(self):
+ """
+ Test frt when the issue was created during working hours and the first response is sent before working hours, but on the next day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("06-29-2021 6:00"))
+ self.assertEqual(issue.first_response_time, 21600.0)
+
+ def test_first_response_time_case11(self):
+ """
+ Test frt when the issue was created during working hours and the first response is also sent during working hours, but on the next day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("06-29-2021 11:00"))
+ self.assertEqual(issue.first_response_time, 25200.0)
+
+ def test_first_response_time_case12(self):
+ """
+ Test frt when the issue was created during working hours and the first response is sent after working hours, but on the next day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("06-29-2021 20:00"))
+ self.assertEqual(issue.first_response_time, 50400.0)
+
+ def test_first_response_time_case13(self):
+ """
+ Test frt when the issue was created during working hours and the first response is sent on the next day, which is not a work day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-25-2021 12:00"), get_datetime("06-26-2021 11:00"))
+ self.assertEqual(issue.first_response_time, 21600.0)
+
+ def test_first_response_time_case14(self):
+ """
+ Test frt when the issue was created after working hours and the first response is sent before working hours, but on the next day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("06-29-2021 6:00"))
+ self.assertEqual(issue.first_response_time, 1.0)
+
+ def test_first_response_time_case15(self):
+ """
+ Test frt when the issue was created after working hours and the first response is sent during working hours, but on the next day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("06-29-2021 11:00"))
+ self.assertEqual(issue.first_response_time, 3600.0)
+
+ def test_first_response_time_case16(self):
+ """
+ Test frt when the issue was created after working hours and the first response is also sent after working hours, but on the next day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("06-29-2021 20:00"))
+ self.assertEqual(issue.first_response_time, 28800.0)
+
+ def test_first_response_time_case17(self):
+ """
+ Test frt when the issue was created after working hours and the first response is sent on the next day, which is not a work day.
+ """
+ issue = create_issue_and_communication(get_datetime("06-25-2021 20:00"), get_datetime("06-26-2021 11:00"))
+ self.assertEqual(issue.first_response_time, 1.0)
+
+ # issue creation and first response are a few days apart
+ def test_first_response_time_case18(self):
+ """
+ Test frt when the issue was created before working hours and the first response is also sent before working hours, but after a few days.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("07-01-2021 6:00"))
+ self.assertEqual(issue.first_response_time, 86400.0)
+
+ def test_first_response_time_case19(self):
+ """
+ Test frt when the issue was created before working hours and the first response is sent during working hours, but after a few days.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("07-01-2021 11:00"))
+ self.assertEqual(issue.first_response_time, 90000.0)
+
+ def test_first_response_time_case20(self):
+ """
+ Test frt when the issue was created before working hours and the first response is sent after working hours, but after a few days.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("07-01-2021 20:00"))
+ self.assertEqual(issue.first_response_time, 115200.0)
+
+ def test_first_response_time_case21(self):
+ """
+ Test frt when the issue was created before working hours and the first response is sent after a few days, on a holiday.
+ """
+ issue = create_issue_and_communication(get_datetime("06-25-2021 6:00"), get_datetime("06-27-2021 11:00"))
+ self.assertEqual(issue.first_response_time, 28800.0)
+
+ def test_first_response_time_case22(self):
+ """
+ Test frt when the issue was created during working hours and the first response is sent before working hours, but after a few days.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("07-01-2021 6:00"))
+ self.assertEqual(issue.first_response_time, 79200.0)
+
+ def test_first_response_time_case23(self):
+ """
+ Test frt when the issue was created during working hours and the first response is also sent during working hours, but after a few days.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("07-01-2021 11:00"))
+ self.assertEqual(issue.first_response_time, 82800.0)
+
+ def test_first_response_time_case24(self):
+ """
+ Test frt when the issue was created during working hours and the first response is sent after working hours, but after a few days.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("07-01-2021 20:00"))
+ self.assertEqual(issue.first_response_time, 108000.0)
+
+ def test_first_response_time_case25(self):
+ """
+ Test frt when the issue was created during working hours and the first response is sent after a few days, on a holiday.
+ """
+ issue = create_issue_and_communication(get_datetime("06-25-2021 12:00"), get_datetime("06-27-2021 11:00"))
+ self.assertEqual(issue.first_response_time, 21600.0)
+
+ def test_first_response_time_case26(self):
+ """
+ Test frt when the issue was created after working hours and the first response is sent before working hours, but after a few days.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("07-01-2021 6:00"))
+ self.assertEqual(issue.first_response_time, 57600.0)
+
+ def test_first_response_time_case27(self):
+ """
+ Test frt when the issue was created after working hours and the first response is sent during working hours, but after a few days.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("07-01-2021 11:00"))
+ self.assertEqual(issue.first_response_time, 61200.0)
+
+ def test_first_response_time_case28(self):
+ """
+ Test frt when the issue was created after working hours and the first response is also sent after working hours, but after a few days.
+ """
+ issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("07-01-2021 20:00"))
+ self.assertEqual(issue.first_response_time, 86400.0)
+
+ def test_first_response_time_case29(self):
+ """
+ Test frt when the issue was created after working hours and the first response is sent after a few days, on a holiday.
+ """
+ issue = create_issue_and_communication(get_datetime("06-25-2021 20:00"), get_datetime("06-27-2021 11:00"))
+ self.assertEqual(issue.first_response_time, 1.0)
+
+def create_issue_and_communication(issue_creation, first_responded_on):
+ issue = make_issue(issue_creation, index=1)
+ sender = create_user("test@admin.com")
+ create_communication(issue.name, sender.email, "Sent", first_responded_on)
+ issue.reload()
+
+ return issue
def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None):
issue = frappe.get_doc({
@@ -185,7 +404,7 @@
def create_communication(reference_name, sender, sent_or_received, creation):
- issue = frappe.get_doc({
+ communication = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"communication_medium": "Email",
@@ -199,4 +418,4 @@
"creation": creation,
"reference_name": reference_name
})
- issue.save()
+ communication.save()
\ No newline at end of file
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
index 308bce4..ae2080c 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
@@ -5,15 +5,15 @@
setup: function(frm) {
if (cint(frm.doc.apply_sla_for_resolution) === 1) {
frm.get_field('priorities').grid.editable_fields = [
- {fieldname: 'priority', columns: 1},
{fieldname: 'default_priority', columns: 1},
+ {fieldname: 'priority', columns: 2},
{fieldname: 'response_time', columns: 2},
{fieldname: 'resolution_time', columns: 2}
];
} else {
frm.get_field('priorities').grid.editable_fields = [
- {fieldname: 'priority', columns: 1},
{fieldname: 'default_priority', columns: 1},
+ {fieldname: 'priority', columns: 2},
{fieldname: 'response_time', columns: 3},
];
}
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
index 61ca3a3..ef14b29 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
@@ -1,6 +1,6 @@
{
"actions": [],
- "autoname": "format:SLA-{document_type}-{service_level}-{####}",
+ "autoname": "format:SLA-{document_type}-{service_level}",
"creation": "2018-12-26 21:08:15.448812",
"doctype": "DocType",
"editable_grid": 1,
@@ -150,7 +150,8 @@
"fieldtype": "Link",
"label": "Document Type",
"options": "DocType",
- "reqd": 1
+ "reqd": 1,
+ "set_only_once": 1
},
{
"default": "1",
@@ -178,7 +179,7 @@
}
],
"links": [],
- "modified": "2021-05-29 13:35:41.956849",
+ "modified": "2021-07-08 12:28:46.283334",
"modified_by": "Administrator",
"module": "Support",
"name": "Service Level Agreement",
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
index 60e5fbe..8739cb2 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
@@ -797,7 +797,7 @@
if meta.has_field("response_by"):
doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
- if meta.has_field("response_by_variance"):
+ if meta.has_field("response_by_variance") and not doc.get('first_responded_on'):
now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2)
@@ -805,7 +805,7 @@
if meta.has_field("resolution_by"):
doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
- if meta.has_field("resolution_by_variance"):
+ if meta.has_field("resolution_by_variance") and not doc.get("resolution_date"):
now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2)
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 2a8446d..7bc97d6 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
@@ -81,10 +81,9 @@
# check SLA custom fields created for leads
sla_fields = get_service_level_agreement_fields()
- meta = frappe.get_meta(doctype, cached=False)
for field in sla_fields:
- self.assertTrue(meta.has_field(field.get("fieldname")))
+ self.assertTrue(frappe.db.exists("Custom Field", {"dt": doctype, "fieldname": field.get("fieldname")}))
def test_docfield_creation_for_sla_on_custom_dt(self):
doctype = create_custom_doctype()
@@ -102,10 +101,9 @@
# check SLA docfields created
sla_fields = get_service_level_agreement_fields()
- meta = frappe.get_meta(doctype.name, cached=False)
for field in sla_fields:
- self.assertTrue(meta.has_field(field.get("fieldname")))
+ self.assertTrue(frappe.db.exists("DocField", {"fieldname": field.get("fieldname"), "parent": doctype.name}))
def test_sla_application(self):
# Default Service Level Agreement
@@ -219,6 +217,42 @@
lead.reload()
self.assertEqual(lead.agreement_status, 'Fulfilled')
+ def test_changing_of_variance_after_response(self):
+ # create lead
+ doctype = "Lead"
+ lead_sla = create_service_level_agreement(
+ default_service_level_agreement=1,
+ holiday_list="__Test Holiday List",
+ entity_type=None, entity=None,
+ response_time=14400,
+ doctype=doctype,
+ sla_fulfilled_on=[{"status": "Replied"}],
+ apply_sla_for_resolution=0
+ )
+ creation = datetime.datetime(2019, 3, 4, 12, 0)
+ lead = make_lead(creation=creation, index=2)
+ self.assertEqual(lead.service_level_agreement, lead_sla.name)
+
+ # set lead as replied to set first responded on
+ frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 30)
+ lead.reload()
+ lead.status = 'Replied'
+ lead.save()
+ lead.reload()
+ self.assertEqual(lead.agreement_status, 'Fulfilled')
+
+ # check response_by_variance
+ self.assertEqual(lead.first_responded_on, frappe.flags.current_time)
+ self.assertEqual(lead.response_by_variance, 1800.0)
+
+ # make a change on the document &
+ # check response_by_variance is unchanged
+ frappe.flags.current_time = datetime.datetime(2019, 3, 4, 18, 30)
+ lead.status = 'Open'
+ lead.save()
+ lead.reload()
+ self.assertEqual(lead.response_by_variance, 1800.0)
+
def tearDown(self):
for d in frappe.get_all("Service Level Agreement"):
frappe.delete_doc("Service Level Agreement", d.name, force=1)
@@ -251,7 +285,7 @@
"doctype": "Service Level Agreement",
"enabled": 1,
"document_type": doctype,
- "service_level": "__Test Service Level",
+ "service_level": "__Test {} SLA".format(entity_type if entity_type else "Default"),
"default_service_level_agreement": default_service_level_agreement,
"default_priority": "Medium",
"holiday_list": holiday_list,
@@ -305,16 +339,6 @@
"workday": "Friday",
"start_time": "10:00:00",
"end_time": "18:00:00",
- },
- {
- "workday": "Saturday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
- },
- {
- "workday": "Sunday",
- "start_time": "10:00:00",
- "end_time": "18:00:00",
}
]
})
@@ -330,16 +354,11 @@
"entity": entity
})
- service_level_agreement_exists = frappe.db.exists("Service Level Agreement", filters)
+ sla = frappe.db.exists("Service Level Agreement", filters)
+ if sla:
+ frappe.delete_doc("Service Level Agreement", sla, force=1)
- if not service_level_agreement_exists:
- doc = frappe.get_doc(service_level_agreement).insert(ignore_permissions=True)
- else:
- doc = frappe.get_doc("Service Level Agreement", service_level_agreement_exists)
- doc.update(service_level_agreement)
- doc.save()
-
- return doc
+ return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True)
def create_customer():
diff --git a/erpnext/support/doctype/service_level_priority/service_level_priority.json b/erpnext/support/doctype/service_level_priority/service_level_priority.json
index 0367fc6..b410fe6 100644
--- a/erpnext/support/doctype/service_level_priority/service_level_priority.json
+++ b/erpnext/support/doctype/service_level_priority/service_level_priority.json
@@ -5,9 +5,9 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "priority",
- "cb_01",
"default_priority",
+ "cb_01",
+ "priority",
"sb_00",
"response_time",
"cb_00",
@@ -15,7 +15,7 @@
],
"fields": [
{
- "columns": 1,
+ "columns": 2,
"fieldname": "priority",
"fieldtype": "Link",
"in_list_view": 1,
@@ -64,7 +64,7 @@
],
"istable": 1,
"links": [],
- "modified": "2021-05-29 19:52:51.733248",
+ "modified": "2021-06-21 12:00:58.089962",
"modified_by": "Administrator",
"module": "Support",
"name": "Service Level Priority",
diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html
index 393c3a4..9050cc3 100644
--- a/erpnext/templates/generators/item_group.html
+++ b/erpnext/templates/generators/item_group.html
@@ -9,7 +9,7 @@
{% endblock %}
{% block page_content %}
-<div class="item-group-content" itemscope itemtype="http://schema.org/Product">
+<div class="item-group-content" itemscope itemtype="http://schema.org/Product" data-item-group="{{ name }}">
<div class="item-group-slideshow">
{% if slideshow %}<!-- slideshow -->
{{ web_block(
@@ -127,15 +127,36 @@
</script>
</div>
</div>
- <div class="row">
- <div class="col-12">
+ <div class="row mt-6">
+ <div class="col-3">
+ </div>
+ <div class="col-9">
{% if frappe.form_dict.start|int > 0 %}
- <button class="btn btn-outline-secondary btn-prev" data-start="{{ frappe.form_dict.start|int - page_length }}">{{ _("Prev") }}</button>
+ <button class="btn btn-outline-secondary btn-prev" data-start="{{ frappe.form_dict.start|int - page_length }}">
+ {{ _("Prev") }}
+ </button>
{% endif %}
{% if items|length >= page_length %}
- <button class="btn btn-outline-secondary btn-next" data-start="{{ frappe.form_dict.start|int + page_length }}">{{ _("Next") }}</button>
+ <button class="btn btn-outline-secondary btn-next" data-start="{{ frappe.form_dict.start|int + page_length }}"
+ style="float: right;">
+ {{ _("Next") }}
+ </button>
{% endif %}
</div>
</div>
</div>
+
+<script>
+ frappe.ready(() => {
+ $('.btn-prev, .btn-next').click((e) => {
+ const $btn = $(e.target);
+ $btn.prop('disabled', true);
+ const start = $btn.data('start');
+ let query_params = frappe.utils.get_query_params();
+ query_params.start = start;
+ let path = window.location.pathname + '?' + frappe.utils.get_url_from_dict(query_params);
+ window.location.href = path;
+ });
+ });
+</script>
{% endblock %}
\ No newline at end of file
diff --git a/erpnext/templates/includes/projects/project_timesheets.html b/erpnext/templates/includes/projects/project_timesheets.html
index fa5b2f9..b8e0682 100644
--- a/erpnext/templates/includes/projects/project_timesheets.html
+++ b/erpnext/templates/includes/projects/project_timesheets.html
@@ -12,7 +12,7 @@
.get_value("User", timesheet.modified_by, [
"full_name", "user_image"
], as_dict = True)
- %}
+ %}
{% if user_details.user_image %}
<span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
<img src="{{ user_details.user_image }}">
diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js
index 0721056..1c641b5 100644
--- a/erpnext/www/all-products/index.js
+++ b/erpnext/www/all-products/index.js
@@ -124,6 +124,10 @@
attribute_filters: if_key_exists(attribute_filters)
};
+ const item_group = $(".item-group-content").data('item-group');
+ if (item_group) {
+ Object.assign(field_filters, { item_group });
+ }
return new Promise((resolve, reject) => {
frappe.call('erpnext.portal.product_configurator.utils.get_products_html_for_website', args)
.then(r => {
diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py
index 7bfac89..4f45561 100644
--- a/erpnext/www/book_appointment/index.py
+++ b/erpnext/www/book_appointment/index.py
@@ -2,7 +2,7 @@
import datetime
import json
import pytz
-
+from frappe import _
WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
@@ -14,7 +14,8 @@
if is_enabled:
return context
else:
- frappe.local.flags.redirect_location = '/404'
+ frappe.redirect_to_message(_("Appointment Scheduling Disabled"), _("Appointment Scheduling has been disabled for this site"),
+ http_status_code=302, indicator_color="red")
raise frappe.Redirect
@frappe.whitelist(allow_guest=True)
@@ -146,4 +147,4 @@
def _datetime_to_deltatime(date_time):
midnight = datetime.datetime.combine(date_time.date(), datetime.time.min)
- return (date_time-midnight)
\ No newline at end of file
+ return (date_time-midnight)