diff --git a/.eslintrc b/.eslintrc
index e40502acd6c..cb45ce5f692 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/.git-blame-ignore-revs b/.git-blame-ignore-revs
index c820d233a5e..e7fa354a2e9 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -10,3 +10,6 @@
# Replace use of Class.extend with native JS class
1fe891b287a1b3f225d29ee3d07e7b1824aba9e7
+
+# This commit just changes spaces to tabs for indentation in some files
+5f473611bd6ed57703716244a054d3fb5ba9cd23
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 7b0f944c669..455ab861f93 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -42,5 +42,6 @@ sed -i 's/socketio:/# socketio:/g' Procfile
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 faab3344a62..d9603e89aa4 100644
--- a/.github/helper/semgrep_rules/frappe_correctness.yml
+++ b/.github/helper/semgrep_rules/frappe_correctness.yml
@@ -98,8 +98,6 @@ rules:
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 5a5098bf506..8b219792080 100644
--- a/.github/helper/semgrep_rules/security.yml
+++ b/.github/helper/semgrep_rules/security.yml
@@ -8,18 +8,3 @@ rules:
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 dabc66eb730..9322ae87bfc 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 7c6b8432b87..cc98f4544f8 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: "ankush/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 389524e9684..e27b406df05 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 00000000000..412a05b0a15
--- /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 63c51c49765..89f56263ffa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@ __pycache__
.idea/
.vscode/
node_modules/
+.backportrc.json
\ No newline at end of file
diff --git a/CODEOWNERS b/CODEOWNERS
index 7cf65a7a732..a4a14de1b8e 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 00000000000..02b10d893f8
--- /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 00000000000..da18d9352a1
--- /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 00000000000..3d6ed5d0d89
--- /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 00000000000..fcb7533a225
--- /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 00000000000..07d9804a733
--- /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 00000000000..7ddc80ab8dd
--- /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 00000000000..72070cc81c4
--- /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 00000000000..d90ebf6856d
--- /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 5808090db35..a181c2d42cb 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
-__version__ = '13.4.1'
+__version__ = '13.7.1'
def get_default_company(user=None):
'''Get default company for user'''
diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py
index 5e764037a74..c417a493c69 100644
--- a/erpnext/accounts/custom/address.py
+++ b/erpnext/accounts/custom/address.py
@@ -33,6 +33,8 @@ def get_shipping_address(company, address = None):
if address and frappe.db.get_value('Dynamic Link',
{'parent': address, 'link_name': company}):
filters.append(["Address", "name", "=", address])
+ if not address:
+ filters.append(["Address", "is_shipping_address", "=", 1])
address = frappe.get_all("Address", filters=filters, fields=fields) or {}
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index dd346bc2408..335e8a15ab0 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -263,6 +263,9 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
amount, base_amount = calculate_amount(doc, item, last_gl_entry,
total_days, total_booking_days, account_currency)
+ if not amount:
+ return
+
if via_journal_entry:
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
@@ -298,17 +301,21 @@ def process_deferred_accounting(posting_date=None):
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/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index 0ebf0eb541d..fac28c92397 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -19,7 +19,7 @@ class AccountingDimension(Document):
def validate(self):
if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project',
- 'Cost Center', 'Accounting Dimension Detail', 'Company') :
+ 'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') :
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg)
@@ -27,7 +27,7 @@ class AccountingDimension(Document):
exists = frappe.db.get_value("Accounting Dimension", {'document_type': self.document_type}, ['name'])
if exists and self.is_new():
- frappe.throw("Document Type already used as a dimension")
+ frappe.throw(_("Document Type already used as a dimension"))
if not self.is_new():
self.validate_document_type_change()
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 781f94e203a..703e93c0757 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -18,6 +18,7 @@
"delete_linked_ledger_entries",
"book_asset_depreciation_entry_automatically",
"unlink_advance_payment_on_cancelation_of_order",
+ "post_change_gl_entries",
"tax_settings_section",
"determine_address_tax_category_from",
"column_break_19",
@@ -253,6 +254,13 @@
{
"fieldname": "column_break_19",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "1",
+ "description": "If enabled, ledger entries will be posted for change amount in POS transactions",
+ "fieldname": "post_change_gl_entries",
+ "fieldtype": "Check",
+ "label": "Create Ledger Entries for Change Amount"
}
],
"icon": "icon-cog",
@@ -260,7 +268,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-04-30 15:25:10.381008",
+ "modified": "2021-06-17 20:26:03.721202",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
index 4d3388090dc..ac4a2d6f16d 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
@@ -5,6 +5,7 @@
from __future__ import unicode_literals
import frappe
+from frappe import _
from frappe.utils import cint
from frappe.model.document import Document
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
@@ -24,7 +25,7 @@ class AccountsSettings(Document):
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:
frappe.msgprint(
- "Stale Days should start from 1.", title='Error', indicator='red',
+ _("Stale Days should start from 1."), title='Error', indicator='red',
raise_exception=1)
def enable_payment_schedule_in_print(self):
diff --git a/erpnext/accounts/accounts b/erpnext/accounts/doctype/advance_taxes_and_charges/__init__.py
similarity index 100%
rename from erpnext/accounts/accounts
rename to erpnext/accounts/doctype/advance_taxes_and_charges/__init__.py
diff --git a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json
new file mode 100644
index 00000000000..4d634994319
--- /dev/null
+++ b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json
@@ -0,0 +1,197 @@
+{
+ "actions": [],
+ "creation": "2020-09-12 22:26:19.594367",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "add_deduct_tax",
+ "charge_type",
+ "row_id",
+ "account_head",
+ "col_break_1",
+ "description",
+ "included_in_paid_amount",
+ "accounting_dimensions_section",
+ "cost_center",
+ "dimension_col_break",
+ "section_break_8",
+ "rate",
+ "section_break_9",
+ "currency",
+ "tax_amount",
+ "total",
+ "allocated_amount",
+ "column_break_13",
+ "base_tax_amount",
+ "base_total",
+ "base_allocated_amount"
+ ],
+ "fields": [
+ {
+ "columns": 2,
+ "fieldname": "charge_type",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Type",
+ "oldfieldname": "charge_type",
+ "oldfieldtype": "Select",
+ "options": "\nActual\nOn Paid Amount\nOn Previous Row Amount\nOn Previous Row Total",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:[\"On Previous Row Amount\", \"On Previous Row Total\"].indexOf(doc.charge_type)!==-1",
+ "fieldname": "row_id",
+ "fieldtype": "Data",
+ "label": "Reference Row #",
+ "oldfieldname": "row_id",
+ "oldfieldtype": "Data"
+ },
+ {
+ "columns": 2,
+ "fieldname": "account_head",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Account Head",
+ "oldfieldname": "account_head",
+ "oldfieldtype": "Link",
+ "options": "Account",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "fieldname": "col_break_1",
+ "fieldtype": "Column Break",
+ "width": "50%"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Small Text",
+ "print_width": "300px",
+ "reqd": 1,
+ "width": "300px"
+ },
+ {
+ "fieldname": "accounting_dimensions_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Dimensions"
+ },
+ {
+ "default": ":Company",
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "oldfieldname": "cost_center_other_charges",
+ "oldfieldtype": "Link",
+ "options": "Cost Center"
+ },
+ {
+ "fieldname": "dimension_col_break",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_8",
+ "fieldtype": "Section Break"
+ },
+ {
+ "columns": 2,
+ "fieldname": "rate",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Rate",
+ "oldfieldname": "rate",
+ "oldfieldtype": "Currency"
+ },
+ {
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break"
+ },
+ {
+ "columns": 2,
+ "fieldname": "tax_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "options": "currency"
+ },
+ {
+ "columns": 2,
+ "fieldname": "total",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Total",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_13",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "base_tax_amount",
+ "fieldtype": "Currency",
+ "label": "Amount (Company Currency)",
+ "oldfieldname": "tax_amount",
+ "oldfieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "base_total",
+ "fieldtype": "Currency",
+ "label": "Total (Company Currency)",
+ "oldfieldname": "total",
+ "oldfieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "add_deduct_tax",
+ "fieldtype": "Select",
+ "label": "Add Or Deduct",
+ "options": "Add\nDeduct",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "included_in_paid_amount",
+ "fieldtype": "Check",
+ "label": "Considered In Paid Amount"
+ },
+ {
+ "fieldname": "allocated_amount",
+ "fieldtype": "Currency",
+ "label": "Allocated Amount",
+ "options": "currency"
+ },
+ {
+ "fieldname": "base_allocated_amount",
+ "fieldtype": "Currency",
+ "label": "Allocated Amount (Company Currency)",
+ "options": "Company:company:default_currency"
+ },
+ {
+ "fetch_from": "account_head.account_currency",
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "label": "Account Currency",
+ "options": "Currency",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-06-09 11:46:58.373170",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Advance Taxes and Charges",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "ASC"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.py b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.py
new file mode 100644
index 00000000000..597d2ccc625
--- /dev/null
+++ b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.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 AdvanceTaxesandCharges(Document):
+ pass
diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js
index 059e1d31588..19041a3f73d 100644
--- a/erpnext/accounts/doctype/bank/bank.js
+++ b/erpnext/accounts/doctype/bank/bank.js
@@ -120,4 +120,4 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
plaid_success(token, response) {
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
}
-};
+};
\ No newline at end of file
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 5f110e2727c..ffc9d1c4658 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 @@ class BankStatementImport(DataImport):
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 f96f59169e8..4fd8413d838 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 erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
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):
@@ -22,7 +23,7 @@ def validate_company(company):
'allow_account_creation_against_child_company'])
if parent_company and (not allow_account_creation_against_child_company):
- msg = _("{} is a child company. ").format(frappe.bold(company))
+ msg = _("{} is a child company.").format(frappe.bold(company)) + " "
msg += _("Please import accounts against parent company or enable {} in company master.").format(
frappe.bold('Allow Account Creation Against Child Company'))
frappe.throw(msg, title=_('Wrong Company'))
@@ -56,7 +57,7 @@ def get_file(file_name):
extension = extension.lstrip(".")
if extension not in ('csv', 'xlsx', 'xls'):
- frappe.throw("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload")
+ frappe.throw(_("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload"))
return file_doc, extension
@@ -293,7 +294,7 @@ def validate_accounts(file_name):
accounts_dict = {}
for account in accounts:
accounts_dict.setdefault(account["account_name"], account)
- if not hasattr(account, "parent_account"):
+ if "parent_account" not in account:
msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.")
msg += "
"
msg += _("Alternatively, you can download the template and fill your data in.")
@@ -301,28 +302,27 @@ def validate_accounts(file_name):
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 "
".join(error_messages)
+ frappe.throw("
".join(error_messages))
def get_root_types():
return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
@@ -356,7 +356,7 @@ def validate_account_types(accounts):
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 @@ def validate_account_types(accounts):
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 1a6dbedf560..1ef512a4894 100644
--- a/erpnext/accounts/doctype/dunning/dunning.py
+++ b/erpnext/accounts/doctype/dunning/dunning.py
@@ -25,7 +25,7 @@ class Dunning(AccountsController):
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'):
@@ -86,18 +86,18 @@ def resolve_dunning(doc, state):
for reference in doc.references:
if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0:
dunnings = frappe.get_list('Dunning', filters={
- 'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')})
+ 'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}, ignore_permissions=True)
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)
+ interest_amount = (interest_per_year * cint(overdue_days)) / 365
+ 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 e2d4d82e418..7fc2e4b3060 100644
--- a/erpnext/accounts/doctype/dunning/test_dunning.py
+++ b/erpnext/accounts/doctype/dunning/test_dunning.py
@@ -16,6 +16,7 @@ class TestDunning(unittest.TestCase):
@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 @@ class TestDunning(unittest.TestCase):
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 @@ def create_dunning():
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 @@ def create_dunning_type():
}
)
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 56193216a22..f2b0a8c08a6 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 @@ class ExchangeRateRevaluation(Document):
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 @@ class ExchangeRateRevaluation(Document):
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 @@ class ExchangeRateRevaluation(Document):
"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 @@ class ExchangeRateRevaluation(Document):
"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 @@ def get_account_details(account, company, posting_date, party_type=None, party=N
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/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index 948c51364ed..11465b711e3 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -121,8 +121,7 @@ class GLEntry(Document):
def check_pl_account(self):
if self.is_opening=='Yes' and \
- frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and \
- self.voucher_type not in ['Purchase Invoice', 'Sales Invoice']:
+ frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
.format(self.voucher_type, self.voucher_no, self.account))
diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
index d1e5afc48ff..b73d8bfbb11 100644
--- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
+++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
@@ -49,11 +49,11 @@ class InvoiceDiscounting(AccountsController):
self.make_gl_entries()
def on_cancel(self):
- self.set_status()
+ self.set_status(cancel=1)
self.update_sales_invoice()
self.make_gl_entries()
- def set_status(self, status=None):
+ def set_status(self, status=None, cancel=0):
if status:
self.status = status
self.db_set("status", status)
@@ -66,6 +66,9 @@ class InvoiceDiscounting(AccountsController):
elif self.docstatus == 2:
self.status = "Cancelled"
+ if cancel:
+ self.db_set('status', self.status, update_modified = True)
+
def update_sales_invoice(self):
for d in self.invoices:
if self.docstatus == 1:
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
index b2e86267c8f..a8c07d6bb9b 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
@@ -49,7 +49,15 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
doc: frm.doc,
btn: $(btn_primary),
method: "make_invoices",
- freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type])
+ freeze: 1,
+ freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
+ callback: function(r) {
+ if (r.message.length == 1) {
+ frappe.msgprint(__("{0} Invoice created successfully.", [frm.doc.invoice_type]));
+ } else if (r.message.length < 50) {
+ frappe.msgprint(__("{0} Invoices created successfully.", [frm.doc.invoice_type]));
+ }
+ }
});
});
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
index 29dc96e8c6f..d76d9099628 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
@@ -216,7 +216,8 @@ def start_import(invoices):
return names
def publish(index, total, doctype):
- if total < 5: return
+ if total < 50:
+ return
frappe.publish_realtime(
"opening_invoice_creation_progress",
dict(
@@ -241,4 +242,3 @@ def get_temporary_opening_account(company=None):
return accounts[0].name
-
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index b80e8ada38f..439b1edbce6 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -3,8 +3,12 @@
{% include "erpnext/public/js/controllers/accounts.js" %}
frappe.provide("erpnext.accounts.dimensions");
+cur_frm.cscript.tax_table = "Advance Taxes and Charges";
+
frappe.ui.form.on('Payment Entry', {
onload: function(frm) {
+ frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
+
if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null);
@@ -91,6 +95,16 @@ frappe.ui.form.on('Payment Entry', {
}
});
+ frm.set_query("advance_tax_account", function() {
+ return {
+ filters: {
+ "company": frm.doc.company,
+ "root_type": ["in", ["Asset", "Liability"]],
+ "is_group": 0
+ }
+ }
+ });
+
frm.set_query("reference_doctype", "references", function() {
if (frm.doc.party_type == "Customer") {
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
@@ -182,6 +196,8 @@ frappe.ui.form.on('Payment Entry', {
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency));
frm.toggle_display("base_paid_amount", frm.doc.paid_from_account_currency != company_currency);
+ frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
+ (frm.doc.paid_from_account_currency != company_currency));
frm.toggle_display("base_received_amount", (
frm.doc.paid_to_account_currency != company_currency
@@ -216,7 +232,7 @@ frappe.ui.form.on('Payment Entry', {
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount",
- "difference_amount"], company_currency);
+ "difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax"], company_currency);
frm.set_currency_labels(["paid_amount"], frm.doc.paid_from_account_currency);
frm.set_currency_labels(["received_amount"], frm.doc.paid_to_account_currency);
@@ -224,11 +240,13 @@ frappe.ui.form.on('Payment Entry', {
var party_account_currency = frm.doc.payment_type=="Receive" ?
frm.doc.paid_from_account_currency : frm.doc.paid_to_account_currency;
- frm.set_currency_labels(["total_allocated_amount", "unallocated_amount"], party_account_currency);
+ frm.set_currency_labels(["total_allocated_amount", "unallocated_amount",
+ "total_taxes_and_charges"], party_account_currency);
var currency_field = (frm.doc.payment_type=="Receive") ? "paid_from_account_currency" : "paid_to_account_currency"
frm.set_df_property("total_allocated_amount", "options", currency_field);
frm.set_df_property("unallocated_amount", "options", currency_field);
+ frm.set_df_property("total_taxes_and_charges", "options", currency_field);
frm.set_df_property("party_balance", "options", currency_field);
frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"],
@@ -364,6 +382,16 @@ frappe.ui.form.on('Payment Entry', {
}
},
+ apply_tax_withholding_amount: function(frm) {
+ if (!frm.doc.apply_tax_withholding_amount) {
+ frm.set_value("tax_withholding_category", '');
+ } else {
+ frappe.db.get_value('Supplier', frm.doc.party, 'tax_withholding_category', (values) => {
+ frm.set_value("tax_withholding_category", values.tax_withholding_category);
+ });
+ }
+ },
+
paid_from: function(frm) {
if(frm.set_party_account_based_on_party) return;
@@ -843,12 +871,12 @@ frappe.ui.form.on('Payment Entry', {
if(frm.doc.payment_type == "Receive"
&& frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions
&& frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) {
- unallocated_amount = (frm.doc.base_received_amount + total_deductions
- - frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate;
+ unallocated_amount = (frm.doc.base_received_amount + total_deductions + frm.doc.base_total_taxes_and_charges
+ + frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate;
} else if (frm.doc.payment_type == "Pay"
&& frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions
&& frm.doc.total_allocated_amount < frm.doc.received_amount + (total_deductions / frm.doc.target_exchange_rate)) {
- unallocated_amount = (frm.doc.base_paid_amount - (total_deductions
+ unallocated_amount = (frm.doc.base_paid_amount + frm.doc.base_total_taxes_and_charges - (total_deductions
+ frm.doc.base_total_allocated_amount)) / frm.doc.target_exchange_rate;
}
}
@@ -874,7 +902,8 @@ frappe.ui.form.on('Payment Entry', {
var total_deductions = frappe.utils.sum($.map(frm.doc.deductions || [],
function(d) { return flt(d.amount) }));
- frm.set_value("difference_amount", difference_amount - total_deductions);
+ frm.set_value("difference_amount", difference_amount - total_deductions +
+ frm.doc.base_total_taxes_and_charges);
frm.events.hide_unhide_fields(frm);
},
@@ -1002,7 +1031,266 @@ frappe.ui.form.on('Payment Entry', {
}
});
}
- }
+ },
+
+ sales_taxes_and_charges_template: function(frm) {
+ frm.trigger('fetch_taxes_from_template');
+ },
+
+ purchase_taxes_and_charges_template: function(frm) {
+ frm.trigger('fetch_taxes_from_template');
+ },
+
+ fetch_taxes_from_template: function(frm) {
+ let master_doctype = '';
+ let taxes_and_charges = '';
+
+ if (frm.doc.party_type == 'Supplier') {
+ master_doctype = 'Purchase Taxes and Charges Template';
+ taxes_and_charges = frm.doc.purchase_taxes_and_charges_template;
+ } else if (frm.doc.party_type == 'Customer') {
+ master_doctype = 'Sales Taxes and Charges Template';
+ taxes_and_charges = frm.doc.sales_taxes_and_charges_template;
+ }
+
+ if (!taxes_and_charges) {
+ return;
+ }
+
+ frappe.call({
+ method: "erpnext.controllers.accounts_controller.get_taxes_and_charges",
+ args: {
+ "master_doctype": master_doctype,
+ "master_name": taxes_and_charges
+ },
+ callback: function(r) {
+ if(!r.exc && r.message) {
+ // set taxes table
+ if(r.message) {
+ for (let tax of r.message) {
+ if (tax.charge_type === 'On Net Total') {
+ tax.charge_type = 'On Paid Amount';
+ }
+ me.frm.add_child("taxes", tax);
+ }
+ frm.events.apply_taxes(frm);
+ frm.events.set_unallocated_amount(frm);
+ }
+ }
+ }
+ });
+ },
+
+ apply_taxes: function(frm) {
+ frm.events.initialize_taxes(frm);
+ frm.events.determine_exclusive_rate(frm);
+ frm.events.calculate_taxes(frm);
+ },
+
+ initialize_taxes: function(frm) {
+ $.each(frm.doc["taxes"] || [], function(i, tax) {
+ frm.events.validate_taxes_and_charges(tax);
+ frm.events.validate_inclusive_tax(tax);
+ tax.item_wise_tax_detail = {};
+ let tax_fields = ["total", "tax_fraction_for_current_item",
+ "grand_total_fraction_for_current_item"];
+
+ if (cstr(tax.charge_type) != "Actual") {
+ tax_fields.push("tax_amount");
+ }
+
+ $.each(tax_fields, function(i, fieldname) { tax[fieldname] = 0.0; });
+
+ frm.doc.paid_amount_after_tax = frm.doc.paid_amount;
+ });
+ },
+
+ validate_taxes_and_charges: function(d) {
+ let msg = "";
+
+ if (d.account_head && !d.description) {
+ // set description from account head
+ d.description = d.account_head.split(' - ').slice(0, -1).join(' - ');
+ }
+
+ if (!d.charge_type && (d.row_id || d.rate || d.tax_amount)) {
+ msg = __("Please select Charge Type first");
+ d.row_id = "";
+ d.rate = d.tax_amount = 0.0;
+ } else if ((d.charge_type == 'Actual' || d.charge_type == 'On Net Total' || d.charge_type == 'On Paid Amount') && d.row_id) {
+ msg = __("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'");
+ d.row_id = "";
+ } else if ((d.charge_type == 'On Previous Row Amount' || d.charge_type == 'On Previous Row Total') && d.row_id) {
+ if (d.idx == 1) {
+ msg = __("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row");
+ d.charge_type = '';
+ } else if (!d.row_id) {
+ msg = __("Please specify a valid Row ID for row {0} in table {1}", [d.idx, __(d.doctype)]);
+ d.row_id = "";
+ } else if (d.row_id && d.row_id >= d.idx) {
+ msg = __("Cannot refer row number greater than or equal to current row number for this Charge type");
+ d.row_id = "";
+ }
+ }
+ if (msg) {
+ frappe.validated = false;
+ refresh_field("taxes");
+ frappe.throw(msg);
+ }
+
+ },
+
+ validate_inclusive_tax: function(tax) {
+ let actual_type_error = function() {
+ let msg = __("Actual type tax cannot be included in Item rate in row {0}", [tax.idx])
+ frappe.throw(msg);
+ };
+
+ let on_previous_row_error = function(row_range) {
+ let msg = __("For row {0} in {1}. To include {2} in Item rate, rows {3} must also be included",
+ [tax.idx, __(tax.doctype), tax.charge_type, row_range])
+ frappe.throw(msg);
+ };
+
+ if(cint(tax.included_in_paid_amount)) {
+ if(tax.charge_type == "Actual") {
+ // inclusive tax cannot be of type Actual
+ actual_type_error();
+ } else if(tax.charge_type == "On Previous Row Amount" &&
+ !cint(this.frm.doc["taxes"][tax.row_id - 1].included_in_paid_amount)
+ ) {
+ // referred row should also be an inclusive tax
+ on_previous_row_error(tax.row_id);
+ } else if(tax.charge_type == "On Previous Row Total") {
+ let taxes_not_included = $.map(this.frm.doc["taxes"].slice(0, tax.row_id),
+ function(t) { return cint(t.included_in_paid_amount) ? null : t; });
+ if(taxes_not_included.length > 0) {
+ // all rows above this tax should be inclusive
+ on_previous_row_error(tax.row_id == 1 ? "1" : "1 - " + tax.row_id);
+ }
+ }
+ }
+ },
+
+ determine_exclusive_rate: function(frm) {
+ let has_inclusive_tax = false;
+ $.each(frm.doc["taxes"] || [], function(i, row) {
+ if(cint(row.included_in_paid_amount)) has_inclusive_tax = true;
+ });
+ if(has_inclusive_tax==false) return;
+
+ let cumulated_tax_fraction = 0.0;
+ $.each(frm.doc["taxes"] || [], function(i, tax) {
+ tax.tax_fraction_for_current_item = frm.events.get_current_tax_fraction(frm, tax);
+
+ if(i==0) {
+ tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item;
+ } else {
+ tax.grand_total_fraction_for_current_item =
+ me.frm.doc["taxes"][i-1].grand_total_fraction_for_current_item +
+ tax.tax_fraction_for_current_item;
+ }
+
+ cumulated_tax_fraction += tax.tax_fraction_for_current_item;
+ frm.doc.paid_amount_after_tax = flt(frm.doc.paid_amount/(1+cumulated_tax_fraction))
+ });
+ },
+
+ get_current_tax_fraction: function(frm, tax) {
+ let current_tax_fraction = 0.0;
+
+ if(cint(tax.included_in_paid_amount)) {
+ let tax_rate = tax.rate;
+
+ if(tax.charge_type == "On Paid Amount") {
+ current_tax_fraction = (tax_rate / 100.0);
+ } else if(tax.charge_type == "On Previous Row Amount") {
+ current_tax_fraction = (tax_rate / 100.0) *
+ frm.doc["taxes"][cint(tax.row_id) - 1].tax_fraction_for_current_item;
+ } else if(tax.charge_type == "On Previous Row Total") {
+ current_tax_fraction = (tax_rate / 100.0) *
+ frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_fraction_for_current_item;
+ }
+ }
+
+ if(tax.add_deduct_tax && tax.add_deduct_tax == "Deduct") {
+ current_tax_fraction *= -1;
+ }
+ return current_tax_fraction;
+ },
+
+
+ calculate_taxes: function(frm) {
+ frm.doc.total_taxes_and_charges = 0.0;
+ frm.doc.base_total_taxes_and_charges = 0.0;
+
+ let actual_tax_dict = {};
+
+ // maintain actual tax rate based on idx
+ $.each(frm.doc["taxes"] || [], function(i, tax) {
+ if (tax.charge_type == "Actual") {
+ actual_tax_dict[tax.idx] = flt(tax.tax_amount, precision("tax_amount", tax));
+ }
+ });
+
+ $.each(me.frm.doc["taxes"] || [], function(i, tax) {
+ let current_tax_amount = frm.events.get_current_tax_amount(frm, tax);
+
+ // Adjust divisional loss to the last item
+ if (tax.charge_type == "Actual") {
+ actual_tax_dict[tax.idx] -= current_tax_amount;
+ if (i == frm.doc["taxes"].length - 1) {
+ current_tax_amount += actual_tax_dict[tax.idx];
+ }
+ }
+
+ tax.tax_amount = current_tax_amount;
+ tax.base_tax_amount = tax.tax_amount * frm.doc.source_exchange_rate;
+ current_tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0;
+
+ if(i==0) {
+ tax.total = flt(frm.doc.paid_amount_after_tax + current_tax_amount, precision("total", tax));
+ } else {
+ tax.total = flt(frm.doc["taxes"][i-1].total + current_tax_amount, precision("total", tax));
+ }
+
+ tax.base_total = tax.total * frm.doc.source_exchange_rate;
+ frm.doc.total_taxes_and_charges += current_tax_amount;
+ frm.doc.base_total_taxes_and_charges += current_tax_amount * frm.doc.source_exchange_rate;
+
+ frm.refresh_field('taxes');
+ frm.refresh_field('total_taxes_and_charges');
+ frm.refresh_field('base_total_taxes_and_charges');
+ });
+ },
+
+ get_current_tax_amount: function(frm, tax) {
+ let tax_rate = tax.rate;
+ let current_tax_amount = 0.0;
+
+ // To set row_id by default as previous row.
+ if(["On Previous Row Amount", "On Previous Row Total"].includes(tax.charge_type)) {
+ if (tax.idx === 1) {
+ frappe.throw(
+ __("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"));
+ }
+ }
+
+ if(tax.charge_type == "Actual") {
+ current_tax_amount = flt(tax.tax_amount, precision("tax_amount", tax))
+ } else if(tax.charge_type == "On Paid Amount") {
+ current_tax_amount = flt((tax_rate / 100.0) * frm.doc.paid_amount_after_tax);
+ } else if(tax.charge_type == "On Previous Row Amount") {
+ current_tax_amount = flt((tax_rate / 100.0) *
+ frm.doc["taxes"][cint(tax.row_id) - 1].tax_amount);
+
+ } else if(tax.charge_type == "On Previous Row Total") {
+ current_tax_amount = flt((tax_rate / 100.0) *
+ frm.doc["taxes"][cint(tax.row_id) - 1].total);
+ }
+
+ return current_tax_amount;
+ },
});
@@ -1049,6 +1337,38 @@ frappe.ui.form.on('Payment Entry Reference', {
}
})
+frappe.ui.form.on('Advance Taxes and Charges', {
+ rate: function(frm) {
+ frm.events.apply_taxes(frm);
+ frm.events.set_unallocated_amount(frm);
+ },
+
+ tax_amount : function(frm) {
+ frm.events.apply_taxes(frm);
+ frm.events.set_unallocated_amount(frm);
+ },
+
+ row_id: function(frm) {
+ frm.events.apply_taxes(frm);
+ frm.events.set_unallocated_amount(frm);
+ },
+
+ taxes_remove: function(frm) {
+ frm.events.apply_taxes(frm);
+ frm.events.set_unallocated_amount(frm);
+ },
+
+ included_in_paid_amount: function(frm) {
+ frm.events.apply_taxes(frm);
+ frm.events.set_unallocated_amount(frm);
+ },
+
+ charge_type: function(frm) {
+ frm.events.apply_taxes(frm);
+ frm.events.set_unallocated_amount(frm);
+ }
+})
+
frappe.ui.form.on('Payment Entry Deduction', {
amount: function(frm) {
frm.events.set_unallocated_amount(frm);
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index 328584a61a0..6f362c1fbb9 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -35,12 +35,16 @@
"paid_to_account_balance",
"payment_amounts_section",
"paid_amount",
+ "paid_amount_after_tax",
"source_exchange_rate",
"base_paid_amount",
+ "base_paid_amount_after_tax",
"column_break_21",
"received_amount",
+ "received_amount_after_tax",
"target_exchange_rate",
"base_received_amount",
+ "base_received_amount_after_tax",
"section_break_14",
"get_outstanding_invoice",
"references",
@@ -52,6 +56,17 @@
"unallocated_amount",
"difference_amount",
"write_off_difference_amount",
+ "taxes_and_charges_section",
+ "purchase_taxes_and_charges_template",
+ "sales_taxes_and_charges_template",
+ "advance_tax_account",
+ "column_break_55",
+ "apply_tax_withholding_amount",
+ "tax_withholding_category",
+ "section_break_56",
+ "taxes",
+ "base_total_taxes_and_charges",
+ "total_taxes_and_charges",
"deductions_or_loss_section",
"deductions",
"transaction_references",
@@ -320,6 +335,7 @@
"reqd": 1
},
{
+ "depends_on": "doc.received_amount",
"fieldname": "base_received_amount",
"fieldtype": "Currency",
"label": "Received Amount (Company Currency)",
@@ -584,12 +600,119 @@
"fieldname": "custom_remarks",
"fieldtype": "Check",
"label": "Custom Remarks"
+ },
+ {
+ "depends_on": "eval:doc.apply_tax_withholding_amount",
+ "fieldname": "tax_withholding_category",
+ "fieldtype": "Link",
+ "label": "Tax Withholding Category",
+ "mandatory_depends_on": "eval:doc.apply_tax_withholding_amount",
+ "options": "Tax Withholding Category"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.party_type == 'Supplier'",
+ "fieldname": "apply_tax_withholding_amount",
+ "fieldtype": "Check",
+ "label": "Apply Tax Withholding Amount"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "taxes_and_charges_section",
+ "fieldtype": "Section Break",
+ "label": "Taxes and Charges"
+ },
+ {
+ "depends_on": "eval:doc.party_type == 'Supplier'",
+ "fieldname": "purchase_taxes_and_charges_template",
+ "fieldtype": "Link",
+ "label": "Taxes and Charges Template",
+ "options": "Purchase Taxes and Charges Template"
+ },
+ {
+ "depends_on": "eval: doc.party_type == 'Customer'",
+ "fieldname": "sales_taxes_and_charges_template",
+ "fieldtype": "Link",
+ "label": "Taxes and Charges Template",
+ "options": "Sales Taxes and Charges Template"
+ },
+ {
+ "depends_on": "eval: doc.party_type == 'Supplier' || doc.party_type == 'Customer'",
+ "fieldname": "taxes",
+ "fieldtype": "Table",
+ "label": "Advance Taxes and Charges",
+ "options": "Advance Taxes and Charges"
+ },
+ {
+ "fieldname": "base_total_taxes_and_charges",
+ "fieldtype": "Currency",
+ "label": "Total Taxes and Charges (Company Currency)",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_taxes_and_charges",
+ "fieldtype": "Currency",
+ "label": "Total Taxes and Charges",
+ "read_only": 1
+ },
+ {
+ "fieldname": "paid_amount_after_tax",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Paid Amount After Tax",
+ "options": "paid_from_account_currency",
+ "read_only": 1
+ },
+ {
+ "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
+ },
+ {
+ "fieldname": "column_break_55",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_56",
+ "fieldtype": "Section Break",
+ "hide_border": 1
+ },
+ {
+ "depends_on": "eval:doc.apply_tax_withholding_amount",
+ "description": "Provisional tax account for advance tax. Taxes are parked in this account until payments are allocated to invoices",
+ "fieldname": "advance_tax_account",
+ "fieldtype": "Link",
+ "label": "Advance Tax Account",
+ "mandatory_depends_on": "eval:doc.apply_tax_withholding_amount",
+ "options": "Account"
+ },
+ {
+ "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",
+ "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",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-03-08 13:05:16.958866",
+ "modified": "2021-07-09 08:58:15.008761",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
@@ -633,4 +756,4 @@
"sort_order": "DESC",
"title_field": "title",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 544d5305513..46904f7c571 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -4,8 +4,8 @@
from __future__ import unicode_literals
import frappe, erpnext, json
-from frappe import _, scrub, ValidationError
-from frappe.utils import flt, comma_or, nowdate, getdate
+from frappe import _, scrub, ValidationError, throw
+from frappe.utils import flt, comma_or, nowdate, getdate, cint
from erpnext.accounts.utils import get_outstanding_invoices, get_account_currency, get_balance_on
from erpnext.accounts.party import get_party_account
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
@@ -15,9 +15,11 @@ from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amo
from erpnext.accounts.doctype.bank_account.bank_account import get_party_bank_account, get_bank_account_details
from erpnext.controllers.accounts_controller import AccountsController, get_supplier_block_status
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting
-
+from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from six import string_types, iteritems
+from erpnext.controllers.accounts_controller import validate_taxes_and_charges
+
class InvalidPaymentEntry(ValidationError):
pass
@@ -52,6 +54,8 @@ class PaymentEntry(AccountsController):
self.set_exchange_rate()
self.validate_mandatory()
self.validate_reference_documents()
+ self.set_tax_withholding()
+ self.apply_taxes()
self.set_amounts()
self.clear_unallocated_reference_document_rows()
self.validate_payment_against_negative_invoice()
@@ -65,7 +69,6 @@ class PaymentEntry(AccountsController):
self.set_status()
def on_submit(self):
- self.setup_party_account_field()
if self.difference_amount:
frappe.throw(_("Difference Amount must be zero"))
self.make_gl_entries()
@@ -78,7 +81,6 @@ class PaymentEntry(AccountsController):
def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
- self.setup_party_account_field()
self.make_gl_entries(cancel=1)
self.update_outstanding_amounts()
self.update_advance_paid()
@@ -122,6 +124,11 @@ class PaymentEntry(AccountsController):
if flt(d.allocated_amount) > flt(d.outstanding_amount):
frappe.throw(_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx))
+ # Check for negative outstanding invoices as well
+ if flt(d.allocated_amount) < 0:
+ if flt(d.allocated_amount) < flt(d.outstanding_amount):
+ frappe.throw(_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx))
+
def delink_advance_entry_references(self):
for reference in self.references:
if reference.reference_doctype in ("Sales Invoice", "Purchase Invoice"):
@@ -176,8 +183,15 @@ class PaymentEntry(AccountsController):
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.set(field, value)
+ d.db_set(field, value)
def validate_payment_type(self):
if self.payment_type not in ("Receive", "Pay", "Internal Transfer"):
@@ -307,7 +321,6 @@ class PaymentEntry(AccountsController):
+ "
" + _("If this is undesirable please cancel the corresponding Payment Entry."),
title=_("Warning"), indicator="orange")
-
def validate_journal_entry(self):
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype == "Journal Entry":
@@ -386,12 +399,104 @@ class PaymentEntry(AccountsController):
else:
self.status = 'Draft'
+ self.db_set('status', self.status, update_modified = True)
+
+ def set_tax_withholding(self):
+ if not self.party_type == 'Supplier':
+ return
+
+ if not self.apply_tax_withholding_amount:
+ return
+
+ if not self.advance_tax_account:
+ frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction"))
+
+ net_total = self.paid_amount
+
+ 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({
+ 'company': self.company,
+ 'doctype': 'Purchase Invoice',
+ 'supplier': self.party,
+ 'posting_date': self.posting_date,
+ 'net_total': net_total
+ })
+
+ tax_withholding_details = get_party_tax_withholding_details(args, self.tax_withholding_category)
+
+ if not tax_withholding_details:
+ return
+
+ tax_withholding_details.update({
+ 'add_deduct_tax': 'Add',
+ 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company)
+ })
+
+ accounts = []
+ for d in self.taxes:
+ if d.account_head == tax_withholding_details.get("account_head"):
+
+ # Preserve user updated included in paid amount
+ if d.included_in_paid_amount:
+ tax_withholding_details.update({'included_in_paid_amount': d.included_in_paid_amount})
+
+ d.update(tax_withholding_details)
+ accounts.append(d.account_head)
+
+ if not accounts or tax_withholding_details.get("account_head") not in accounts:
+ self.append("taxes", tax_withholding_details)
+
+ to_remove = [d for d in self.taxes
+ if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")]
+
+ for d in to_remove:
+ self.remove(d)
+
+ def apply_taxes(self):
+ self.initialize_taxes()
+ self.determine_exclusive_rate()
+ self.calculate_taxes()
+
def set_amounts(self):
+ self.set_received_amount()
self.set_amounts_in_company_currency()
+ self.set_amounts_after_tax()
self.set_total_allocated_amount()
self.set_unallocated_amount()
self.set_difference_amount()
+ def set_received_amount(self):
+ self.base_received_amount = self.base_paid_amount
+
+ def set_amounts_after_tax(self):
+ applicable_tax = 0
+ base_applicable_tax = 0
+ for tax in self.get('taxes'):
+ if not tax.included_in_paid_amount:
+ amount = -1 * tax.tax_amount if tax.add_deduct_tax == 'Deduct' else tax.tax_amount
+ base_amount = -1 * tax.base_tax_amount if tax.add_deduct_tax == 'Deduct' else tax.base_tax_amount
+
+ applicable_tax += amount
+ base_applicable_tax += base_amount
+
+ self.paid_amount_after_tax = flt(flt(self.paid_amount) + flt(applicable_tax),
+ self.precision("paid_amount_after_tax"))
+ self.base_paid_amount_after_tax = flt(flt(self.paid_amount_after_tax) * flt(self.source_exchange_rate),
+ self.precision("base_paid_amount_after_tax"))
+
+ self.received_amount_after_tax = flt(flt(self.received_amount) + flt(applicable_tax),
+ self.precision("paid_amount_after_tax"))
+ self.base_received_amount_after_tax = flt(flt(self.received_amount_after_tax) * flt(self.target_exchange_rate),
+ self.precision("base_paid_amount_after_tax"))
+
def set_amounts_in_company_currency(self):
self.base_paid_amount, self.base_received_amount, self.difference_amount = 0, 0, 0
if self.paid_amount:
@@ -420,16 +525,19 @@ class PaymentEntry(AccountsController):
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 + total_deductions \
and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate):
- self.unallocated_amount = (self.base_received_amount + total_deductions -
- self.base_total_allocated_amount) / self.source_exchange_rate
+ self.unallocated_amount = (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 - 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 = (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)
@@ -445,10 +553,22 @@ class PaymentEntry(AccountsController):
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):
@@ -532,6 +652,7 @@ class PaymentEntry(AccountsController):
self.add_party_gl_entries(gl_entries)
self.add_bank_gl_entries(gl_entries)
self.add_deductions_gl_entries(gl_entries)
+ self.add_tax_gl_entries(gl_entries)
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
@@ -571,8 +692,8 @@ class PaymentEntry(AccountsController):
gl_entries.append(gle)
if self.unallocated_amount:
- base_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()
@@ -607,6 +728,51 @@ class PaymentEntry(AccountsController):
}, item=self)
)
+ def add_tax_gl_entries(self, gl_entries):
+ for d in self.get('taxes'):
+ account_currency = get_account_currency(d.account_head)
+ if account_currency != self.company_currency:
+ frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency))
+
+ if self.payment_type in ('Pay', 'Internal Transfer'):
+ 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": 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
+ 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"):
if d.amount:
@@ -625,6 +791,14 @@ class PaymentEntry(AccountsController):
}, item=d)
)
+ def get_party_account_for_taxes(self):
+ if self.advance_tax_account:
+ return self.advance_tax_account
+ elif self.payment_type == 'Receive':
+ 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:
for d in self.get("references"):
@@ -667,10 +841,150 @@ class PaymentEntry(AccountsController):
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)
+ validate_inclusive_tax(tax, self)
+
+ tax_fields = ["total", "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"]
+
+ if tax.charge_type != "Actual":
+ tax_fields.append("tax_amount")
+
+ for fieldname in tax_fields:
+ tax.set(fieldname, 0.0)
+
+ self.paid_amount_after_tax = self.paid_amount
+
+ def determine_exclusive_rate(self):
+ if not any((cint(tax.included_in_paid_amount) for tax in self.get("taxes"))):
+ return
+
+ cumulated_tax_fraction = 0
+ for i, tax in enumerate(self.get("taxes")):
+ tax.tax_fraction_for_current_item = self.get_current_tax_fraction(tax)
+ if i==0:
+ tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item
+ else:
+ tax.grand_total_fraction_for_current_item = \
+ self.get("taxes")[i-1].grand_total_fraction_for_current_item \
+ + tax.tax_fraction_for_current_item
+
+ cumulated_tax_fraction += tax.tax_fraction_for_current_item
+
+ self.paid_amount_after_tax = flt(self.paid_amount/(1+cumulated_tax_fraction))
+
+ def calculate_taxes(self):
+ self.total_taxes_and_charges = 0.0
+ self.base_total_taxes_and_charges = 0.0
+
+ actual_tax_dict = dict([[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))]
+ for tax in self.get("taxes") if tax.charge_type == "Actual"])
+
+ for i, tax in enumerate(self.get('taxes')):
+ current_tax_amount = self.get_current_tax_amount(tax)
+
+ if tax.charge_type == "Actual":
+ actual_tax_dict[tax.idx] -= current_tax_amount
+ if i == len(self.get("taxes")) - 1:
+ current_tax_amount += actual_tax_dict[tax.idx]
+
+ tax.tax_amount = current_tax_amount
+ tax.base_tax_amount = tax.tax_amount * self.source_exchange_rate
+
+ if tax.add_deduct_tax == "Deduct":
+ current_tax_amount *= -1.0
+ else:
+ current_tax_amount *= 1.0
+
+ if i == 0:
+ tax.total = flt(self.paid_amount_after_tax + current_tax_amount, self.precision("total", tax))
+ else:
+ tax.total = flt(self.get('taxes')[i-1].total + current_tax_amount, self.precision("total", tax))
+
+ tax.base_total = tax.total * self.source_exchange_rate
+
+ self.total_taxes_and_charges += current_tax_amount
+ self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate
+
+ if self.get('taxes'):
+ self.paid_amount_after_tax = self.get('taxes')[-1].base_total
+
+ def get_current_tax_amount(self, tax):
+ tax_rate = tax.rate
+
+ # To set row_id by default as previous row.
+ if tax.charge_type in ["On Previous Row Amount", "On Previous Row Total"]:
+ if tax.idx == 1:
+ frappe.throw(_("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"))
+
+ if not tax.row_id:
+ tax.row_id = tax.idx - 1
+
+ if tax.charge_type == "Actual":
+ current_tax_amount = flt(tax.tax_amount, self.precision("tax_amount", tax))
+ elif tax.charge_type == "On Paid Amount":
+ current_tax_amount = (tax_rate / 100.0) * self.paid_amount_after_tax
+ elif tax.charge_type == "On Previous Row Amount":
+ current_tax_amount = (tax_rate / 100.0) * \
+ self.get('taxes')[cint(tax.row_id) - 1].tax_amount
+
+ elif tax.charge_type == "On Previous Row Total":
+ current_tax_amount = (tax_rate / 100.0) * \
+ self.get('taxes')[cint(tax.row_id) - 1].total
+
+ return current_tax_amount
+
+ def get_current_tax_fraction(self, tax):
+ current_tax_fraction = 0
+
+ if cint(tax.included_in_paid_amount):
+ tax_rate = tax.rate
+
+ if tax.charge_type == "On Paid Amount":
+ current_tax_fraction = tax_rate / 100.0
+ elif tax.charge_type == "On Previous Row Amount":
+ current_tax_fraction = (tax_rate / 100.0) * \
+ self.get("taxes")[cint(tax.row_id) - 1].tax_fraction_for_current_item
+ elif tax.charge_type == "On Previous Row Total":
+ current_tax_fraction = (tax_rate / 100.0) * \
+ self.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item
+
+ if getattr(tax, "add_deduct_tax", None) and tax.add_deduct_tax == "Deduct":
+ current_tax_fraction *= -1.0
+
+ return current_tax_fraction
+
+def validate_inclusive_tax(tax, doc):
+ def _on_previous_row_error(row_range):
+ throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range))
+
+ if cint(getattr(tax, "included_in_paid_amount", None)):
+ if tax.charge_type == "Actual":
+ # inclusive tax cannot be of type Actual
+ throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx))
+ elif tax.charge_type == "On Previous Row Amount" and \
+ not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_paid_amount):
+ # referred row should also be inclusive
+ _on_previous_row_error(tax.row_id)
+ elif tax.charge_type == "On Previous Row Total" and \
+ not all([cint(t.included_in_paid_amount for t in doc.get("taxes")[:cint(tax.row_id) - 1])]):
+ # all rows about the referred tax should be inclusive
+ _on_previous_row_error("1 - %d" % (cint(tax.row_id),))
+ elif tax.get("category") == "Valuation":
+ frappe.throw(_("Valuation type charges can not be marked as Inclusive"))
+
@frappe.whitelist()
def get_outstanding_reference_documents(args):
@@ -791,7 +1105,7 @@ def split_invoices_based_on_payment_terms(outstanding_invoices):
outstanding_invoices.pop(idx - 1)
outstanding_invoices += invoice_ref_based_on_payment_terms[idx]
-
+
return outstanding_invoices
def get_orders_to_be_billed(posting_date, party_type, party,
@@ -989,6 +1303,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
outstanding_amount = ref_doc.get("outstanding_amount")
elif reference_doctype == "Donation":
total_amount = ref_doc.get("amount")
+ outstanding_amount = total_amount
exchange_rate = 1
elif reference_doctype == "Dunning":
total_amount = ref_doc.get("dunning_amount")
@@ -1045,9 +1360,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
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
})
@@ -1235,6 +1550,13 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
})
pe.set_difference_amount()
+ if doc.doctype == 'Purchase Order' and doc.apply_tds:
+ pe.apply_tax_withholding_amount = 1
+ pe.tax_withholding_category = doc.tax_withholding_category
+
+ if not pe.advance_tax_account:
+ pe.advance_tax_account = frappe.db.get_value('Company', pe.company, 'unrealized_profit_loss_account')
+
return pe
def get_bank_cash_account(doc, bank_account):
@@ -1353,6 +1675,7 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
paid_amount = received_amount * doc.get('conversion_rate', 1)
if dt == "Employee Advance":
paid_amount = received_amount * 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 4641d6b5ffa..d1302f5ae78 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 @@ class TestPaymentEntry(unittest.TestCase):
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_deduction/payment_entry_deduction.json b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
index 7060d116913..61a1462dd7a 100644
--- a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
+++ b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
@@ -1,140 +1,70 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2016-06-15 15:56:30.815503",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
+ "actions": [],
+ "creation": "2016-06-15 15:56:30.815503",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "field_order": [
+ "account",
+ "cost_center",
+ "amount",
+ "column_break_2",
+ "description"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "account",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Account",
+ "options": "Account",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "cost_center",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Cost Center",
- "length": 0,
- "no_copy": 0,
- "options": "Cost Center",
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Cost Center",
+ "options": "Cost Center",
+ "print_hide": 1,
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Amount",
- "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": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Description",
+ "show_days": 1,
+ "show_seconds": 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": 1,
- "max_attachments": 0,
- "modified": "2019-01-07 16:52:07.040146",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Payment Entry Deduction",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-09-12 20:38:08.110674",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Payment Entry Deduction",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
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 912ad0977a2..43eb0b6e2aa 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/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 520e87a6ce1..438951db627 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -101,7 +101,7 @@ class PaymentRequest(Document):
controller.validate_transaction_currency(self.currency)
controller.request_for_payment(**payment_record)
-
+
def get_request_amount(self):
data_of_completed_requests = frappe.get_all("Integration Request", filters={
'reference_doctype': self.doctype,
@@ -492,7 +492,6 @@ def update_payment_req_status(doc, method):
status = 'Requested'
pay_req_doc.db_set('status', status)
- frappe.db.commit()
def get_dummy_message(doc):
return frappe.render_template("""{% if doc.contact_person -%}
diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
index 80e3348d817..39627eb376a 100644
--- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
+++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
@@ -26,7 +26,7 @@ class PaymentTermsTemplate(Document):
def check_duplicate_terms(self):
terms = []
for term in self.terms:
- term_info = (term.credit_days, term.credit_months, term.due_date_based_on)
+ term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on)
if term_info in terms:
frappe.msgprint(
_('The Payment Term at row {0} is possibly a duplicate.').format(term.idx),
diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json
index 47546c07a43..84c941ecc10 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json
+++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json
@@ -1,350 +1,138 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "ACC-PCV-.YYYY.-.#####",
- "beta": 0,
- "creation": "2013-01-10 16:34:07",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "editable_grid": 0,
- "engine": "InnoDB",
+ "actions": [],
+ "autoname": "ACC-PCV-.YYYY.-.#####",
+ "creation": "2013-01-10 16:34:07",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "transaction_date",
+ "posting_date",
+ "fiscal_year",
+ "amended_from",
+ "company",
+ "cost_center_wise_pnl",
+ "column_break1",
+ "closing_account_head",
+ "remarks"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "transaction_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": "Transaction Date",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "transaction_date",
- "oldfieldtype": "Date",
- "permlevel": 0,
- "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": "transaction_date",
+ "fieldtype": "Date",
+ "label": "Transaction Date",
+ "oldfieldname": "transaction_date",
+ "oldfieldtype": "Date"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "posting_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": "Posting Date",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "posting_date",
- "oldfieldtype": "Date",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "label": "Posting Date",
+ "oldfieldname": "posting_date",
+ "oldfieldtype": "Date",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "fiscal_year",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Closing Fiscal Year",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "fiscal_year",
- "oldfieldtype": "Select",
- "options": "Fiscal Year",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "fiscal_year",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Closing Fiscal Year",
+ "oldfieldname": "fiscal_year",
+ "oldfieldtype": "Select",
+ "options": "Fiscal Year",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "amended_from",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 1,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Amended From",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "amended_from",
- "oldfieldtype": "Data",
- "options": "Period Closing Voucher",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "label": "Amended From",
+ "no_copy": 1,
+ "oldfieldname": "amended_from",
+ "oldfieldtype": "Data",
+ "options": "Period Closing Voucher",
+ "read_only": 1
+ },
{
- "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,
- "oldfieldname": "company",
- "oldfieldtype": "Select",
- "options": "Company",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "oldfieldname": "company",
+ "oldfieldtype": "Select",
+ "options": "Company",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break1",
- "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,
- "oldfieldtype": "Column Break",
- "permlevel": 0,
- "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_break1",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "The account head under Liability or Equity, in which Profit/Loss will be booked",
- "fieldname": "closing_account_head",
- "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": "Closing Account Head",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "closing_account_head",
- "oldfieldtype": "Link",
- "options": "Account",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "description": "The account head under Liability or Equity, in which Profit/Loss will be booked",
+ "fieldname": "closing_account_head",
+ "fieldtype": "Link",
+ "label": "Closing Account Head",
+ "oldfieldname": "closing_account_head",
+ "oldfieldtype": "Link",
+ "options": "Account",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "remarks",
- "fieldtype": "Small Text",
- "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": "Remarks",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "remarks",
- "oldfieldtype": "Small Text",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "remarks",
+ "fieldtype": "Small Text",
+ "label": "Remarks",
+ "oldfieldname": "remarks",
+ "oldfieldtype": "Small Text",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "cost_center_wise_pnl",
+ "fieldtype": "Check",
+ "label": "Book Cost Center Wise Profit/Loss"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-file-text",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2020-09-18 17:26:09.703215",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Period Closing Voucher",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-file-text",
+ "idx": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2021-05-20 15:27:37.210458",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Period Closing Voucher",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
"write": 1
- },
+ },
{
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "submit": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "search_fields": "posting_date, fiscal_year",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "closing_account_head",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "search_fields": "posting_date, fiscal_year",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "closing_account_head"
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
index fdb4f6fc716..9cfb47876c8 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
@@ -51,63 +51,96 @@ class PeriodClosingVoucher(AccountsController):
def make_gl_entries(self):
gl_entries = []
- net_pl_balance = 0
- dimension_fields = ['t1.cost_center']
+ net_pl_balance = 0
- accounting_dimensions = get_accounting_dimensions()
- for dimension in accounting_dimensions:
- dimension_fields.append('t1.{0}'.format(dimension))
-
- dimension_filters, default_dimensions = get_dimensions()
-
- pl_accounts = self.get_pl_balances(dimension_fields)
+ pl_accounts = self.get_pl_balances()
for acc in pl_accounts:
- if flt(acc.balance_in_company_currency):
+ if flt(acc.bal_in_company_currency):
gl_entries.append(self.get_gl_dict({
"account": acc.account,
"cost_center": acc.cost_center,
"account_currency": acc.account_currency,
- "debit_in_account_currency": abs(flt(acc.balance_in_account_currency)) \
- if flt(acc.balance_in_account_currency) < 0 else 0,
- "debit": abs(flt(acc.balance_in_company_currency)) \
- if flt(acc.balance_in_company_currency) < 0 else 0,
- "credit_in_account_currency": abs(flt(acc.balance_in_account_currency)) \
- if flt(acc.balance_in_account_currency) > 0 else 0,
- "credit": abs(flt(acc.balance_in_company_currency)) \
- if flt(acc.balance_in_company_currency) > 0 else 0
+ "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0,
+ "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
+ "credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0,
+ "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0
}, item=acc))
- net_pl_balance += flt(acc.balance_in_company_currency)
+ net_pl_balance += flt(acc.bal_in_company_currency)
if net_pl_balance:
- cost_center = frappe.db.get_value("Company", self.company, "cost_center")
- gl_entry = self.get_gl_dict({
- "account": self.closing_account_head,
- "debit_in_account_currency": abs(net_pl_balance) if net_pl_balance > 0 else 0,
- "debit": abs(net_pl_balance) if net_pl_balance > 0 else 0,
- "credit_in_account_currency": abs(net_pl_balance) if net_pl_balance < 0 else 0,
- "credit": abs(net_pl_balance) if net_pl_balance < 0 else 0,
- "cost_center": cost_center
- })
-
- for dimension in accounting_dimensions:
- gl_entry.update({
- dimension: default_dimensions.get(self.company, {}).get(dimension)
- })
-
- gl_entries.append(gl_entry)
+ if self.cost_center_wise_pnl:
+ costcenter_wise_gl_entries = self.get_costcenter_wise_pnl_gl_entries(pl_accounts)
+ gl_entries += costcenter_wise_gl_entries
+ else:
+ gl_entry = self.get_pnl_gl_entry(net_pl_balance)
+ gl_entries.append(gl_entry)
from erpnext.accounts.general_ledger import make_gl_entries
make_gl_entries(gl_entries)
+
+ def get_pnl_gl_entry(self, net_pl_balance):
+ cost_center = frappe.db.get_value("Company", self.company, "cost_center")
+ gl_entry = self.get_gl_dict({
+ "account": self.closing_account_head,
+ "debit_in_account_currency": abs(net_pl_balance) if net_pl_balance > 0 else 0,
+ "debit": abs(net_pl_balance) if net_pl_balance > 0 else 0,
+ "credit_in_account_currency": abs(net_pl_balance) if net_pl_balance < 0 else 0,
+ "credit": abs(net_pl_balance) if net_pl_balance < 0 else 0,
+ "cost_center": cost_center
+ })
+
+ self.update_default_dimensions(gl_entry)
+
+ return gl_entry
+
+ def get_costcenter_wise_pnl_gl_entries(self, pl_accounts):
+ company_cost_center = frappe.db.get_value("Company", self.company, "cost_center")
+ gl_entries = []
+
+ for acc in pl_accounts:
+ if flt(acc.bal_in_company_currency):
+ gl_entry = self.get_gl_dict({
+ "account": self.closing_account_head,
+ "cost_center": acc.cost_center or company_cost_center,
+ "account_currency": acc.account_currency,
+ "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0,
+ "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
+ "credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0,
+ "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0
+ }, item=acc)
+
+ self.update_default_dimensions(gl_entry)
+
+ gl_entries.append(gl_entry)
+
+ return gl_entries
+
+ def update_default_dimensions(self, gl_entry):
+ if not self.accounting_dimensions:
+ self.accounting_dimensions = get_accounting_dimensions()
+
+ _, default_dimensions = get_dimensions()
+ for dimension in self.accounting_dimensions:
+ gl_entry.update({
+ dimension: default_dimensions.get(self.company, {}).get(dimension)
+ })
+
+ def get_pl_balances(self):
+ """Get balance for dimension-wise pl accounts"""
+
+ dimension_fields = ['t1.cost_center']
+
+ self.accounting_dimensions = get_accounting_dimensions()
+ for dimension in self.accounting_dimensions:
+ dimension_fields.append('t1.{0}'.format(dimension))
- def get_pl_balances(self, dimension_fields):
- """Get balance for Profit and Loss accounts, only including valid transactions (not cancelled)"""
return frappe.db.sql("""
select
t1.account, t2.account_currency, {dimension_fields},
- sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as balance_in_account_currency,
- sum(t1.debit) - sum(t1.credit) as balance_in_company_currency
+ sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
+ sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
from `tabGL Entry` t1, `tabAccount` t2
where t1.is_cancelled = 0 and t1.account = t2.name and t2.report_type = 'Profit and Loss'
and t2.docstatus < 2 and t2.company = %s
diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
index eb02d97b789..2f29372b01c 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
@@ -8,6 +8,7 @@ import frappe
from frappe.utils import flt, today
from erpnext.accounts.utils import get_fiscal_year, now
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
class TestPeriodClosingVoucher(unittest.TestCase):
def test_closing_entry(self):
@@ -65,6 +66,58 @@ class TestPeriodClosingVoucher(unittest.TestCase):
self.assertEqual(gle_for_random_expense_account[0].amount_in_account_currency,
-1*random_expense_account[0].balance_in_account_currency)
+ def test_cost_center_wise_posting(self):
+ frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
+
+ company = create_company()
+ surplus_account = create_account()
+
+ cost_center1 = create_cost_center("Test Cost Center 1")
+ cost_center2 = create_cost_center("Test Cost Center 2")
+
+ create_sales_invoice(
+ company=company,
+ cost_center=cost_center1,
+ income_account="Sales - TPC",
+ expense_account="Cost of Goods Sold - TPC",
+ rate=400,
+ debit_to="Debtors - TPC"
+ )
+ create_sales_invoice(
+ company=company,
+ cost_center=cost_center2,
+ income_account="Sales - TPC",
+ expense_account="Cost of Goods Sold - TPC",
+ rate=200,
+ debit_to="Debtors - TPC"
+ )
+
+ pcv = frappe.get_doc({
+ "transaction_date": today(),
+ "posting_date": today(),
+ "fiscal_year": get_fiscal_year(today())[0],
+ "company": "Test PCV Company",
+ "cost_center_wise_pnl": 1,
+ "closing_account_head": surplus_account,
+ "remarks": "Test",
+ "doctype": "Period Closing Voucher"
+ })
+ pcv.insert()
+ pcv.submit()
+
+ expected_gle = (
+ ('Sales - TPC', 200.0, 0.0, cost_center2),
+ (surplus_account, 0.0, 200.0, cost_center2),
+ ('Sales - TPC', 400.0, 0.0, cost_center1),
+ (surplus_account, 0.0, 400.0, cost_center1)
+ )
+
+ pcv_gle = frappe.db.sql("""
+ select account, debit, credit, cost_center from `tabGL Entry` where voucher_no=%s
+ """, (pcv.name))
+
+ self.assertTrue(pcv_gle, expected_gle)
+
def make_period_closing_voucher(self):
pcv = frappe.get_doc({
"doctype": "Period Closing Voucher",
@@ -80,6 +133,38 @@ class TestPeriodClosingVoucher(unittest.TestCase):
return pcv
+def create_company():
+ company = frappe.get_doc({
+ 'doctype': 'Company',
+ 'company_name': "Test PCV Company",
+ 'country': 'United States',
+ 'default_currency': 'USD'
+ })
+ company.insert(ignore_if_duplicate = True)
+ return company.name
+
+def create_account():
+ account = frappe.get_doc({
+ "account_name": "Reserve and Surplus",
+ "is_group": 0,
+ "company": "Test PCV Company",
+ "root_type": "Liability",
+ "report_type": "Balance Sheet",
+ "account_currency": "USD",
+ "parent_account": "Current Liabilities - TPC",
+ "doctype": "Account"
+ }).insert(ignore_if_duplicate = True)
+ return account.name
+
+def create_cost_center(cc_name):
+ costcenter = frappe.get_doc({
+ "company": "Test PCV Company",
+ "cost_center_name": cc_name,
+ "doctype": "Cost Center",
+ "parent_cost_center": "Test PCV Company - TPC"
+ })
+ costcenter.insert(ignore_if_duplicate = True)
+ return costcenter.name
test_dependencies = ["Customer", "Cost Center"]
test_records = frappe.get_test_records("Period Closing Voucher")
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
index 8c5a34a0d8e..6418d730903 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
@@ -107,7 +107,7 @@ frappe.ui.form.on('POS Closing Entry', {
frm.set_value("taxes", []);
for (let row of frm.doc.payment_reconciliation) {
- row.expected_amount = 0;
+ row.expected_amount = row.opening_amount;
}
for (let row of frm.doc.pos_transactions) {
@@ -154,6 +154,9 @@ function add_to_pos_transaction(d, frm) {
function refresh_payments(d, frm) {
d.payments.forEach(p => {
const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment);
+ if (p.account == d.account_for_change_amount) {
+ p.amount -= flt(d.change_amount);
+ }
if (payment) {
payment.expected_amount += flt(p.amount);
payment.difference = payment.closing_amount - payment.expected_amount;
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index f55fdab21c3..8ec4ef224cd 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -140,6 +140,7 @@ class POSInvoice(SalesInvoice):
return
available_stock = get_stock_availability(d.item_code, d.warehouse)
+
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
if flt(available_stock) <= 0:
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.')
@@ -213,8 +214,9 @@ class POSInvoice(SalesInvoice):
for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
if not is_stock_item:
- frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ")
- .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
+ if not frappe.db.exists('Product Bundle', d.item_code):
+ frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.")
+ .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
def validate_mode_of_payment(self):
if len(self.payments) == 0:
@@ -455,15 +457,36 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
+ if frappe.db.get_value('Item', item_code, 'is_stock_item'):
+ bin_qty = get_bin_qty(item_code, warehouse)
+ pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
+ return bin_qty - pos_sales_qty
+ else:
+ if frappe.db.exists('Product Bundle', item_code):
+ return get_bundle_availability(item_code, warehouse)
+
+def get_bundle_availability(bundle_item_code, warehouse):
+ product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)
+
+ bundle_bin_qty = 1000000
+ for item in product_bundle.items:
+ item_bin_qty = get_bin_qty(item.item_code, warehouse)
+ item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
+ available_qty = item_bin_qty - item_pos_reserved_qty
+
+ max_available_bundles = available_qty / item.qty
+ if bundle_bin_qty > max_available_bundles:
+ bundle_bin_qty = max_available_bundles
+
+ pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse)
+ return bundle_bin_qty - pos_sales_qty
+
+def get_bin_qty(item_code, warehouse):
bin_qty = frappe.db.sql("""select actual_qty from `tabBin`
where item_code = %s and warehouse = %s
limit 1""", (item_code, warehouse), as_dict=1)
- pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
-
- bin_qty = bin_qty[0].actual_qty or 0 if bin_qty else 0
-
- return bin_qty - pos_sales_qty
+ return bin_qty[0].actual_qty or 0 if bin_qty else 0
def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = frappe.db.sql("""select sum(p_item.qty) as qty
@@ -522,4 +545,4 @@ def add_return_modes(doc, pos_profile):
mode_of_payment = pos_payment_method.mode_of_payment
if pos_payment_method.allow_in_returns and not [d for d in doc.get('payments') if d.mode_of_payment == mode_of_payment]:
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
- append_payment(payment_mode[0])
\ No newline at end of file
+ append_payment(payment_mode[0])
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
index 6dc46430e06..088c190f451 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
@@ -19,7 +19,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
frappe.show_alert({message: __('Emails Queued'), indicator: 'blue'});
}
else{
- frappe.msgprint('No Records for these settings.')
+ frappe.msgprint(__('No Records for these settings.'))
}
}
});
@@ -33,7 +33,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
type: 'GET',
success: function(result) {
if(jQuery.isEmptyObject(result)){
- frappe.msgprint('No Records for these settings.');
+ frappe.msgprint(__('No Records for these settings.'));
}
else{
window.location = url;
@@ -92,7 +92,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
frm.refresh_field('customers');
}
else{
- frappe.throw('No Customers found with selected options.');
+ frappe.throw(__('No Customers found with selected options.'));
}
}
}
@@ -129,4 +129,4 @@ frappe.ui.form.on('Process Statement Of Accounts Customer', {
}
})
}
-});
\ No newline at end of file
+});
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 0b0ee904ff9..500952e38ad 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 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
@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.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 617b5b49d4c..7562418fd2f 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -27,10 +27,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
});
}
- company() {
- erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
- }
-
onload() {
super.onload();
@@ -569,5 +565,9 @@ frappe.ui.form.on("Purchase Invoice", {
frm: frm,
freeze_message: __("Creating Purchase Receipt ...")
})
- }
+ },
+
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
})
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index d3d3ffa17fa..00ef7d5c184 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -592,11 +592,12 @@
"label": "Raw Materials Supplied"
},
{
+ "depends_on": "update_stock",
"fieldname": "supplied_items",
"fieldtype": "Table",
"label": "Supplied Items",
- "options": "Purchase Receipt Item Supplied",
- "read_only": 1
+ "no_copy": 1,
+ "options": "Purchase Receipt Item Supplied"
},
{
"fieldname": "section_break_26",
@@ -837,6 +838,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment (Company Currency)",
@@ -883,6 +885,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment",
@@ -1380,7 +1383,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-30 22:45:58.334107",
+ "modified": "2021-06-15 18:20:56.806195",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 83e9f7583e9..b99d75ec496 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -68,9 +68,6 @@ class PurchaseInvoice(BuyingController):
super(PurchaseInvoice, self).validate()
- # apply tax withholding only if checked and applicable
- self.set_tax_withholding()
-
if not self.is_return:
self.po_required()
self.pr_required()
@@ -251,11 +248,9 @@ class PurchaseInvoice(BuyingController):
if self.update_stock and (not item.from_warehouse):
if for_validate and item.expense_account and item.expense_account != warehouse_account[item.warehouse]["account"]:
- msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]))
- msg += _("because account {} is not linked to warehouse {} ").format(frappe.bold(item.expense_account), frappe.bold(item.warehouse))
- msg += _("or it is not the default inventory account")
+ msg = _("Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account").format(
+ item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]), frappe.bold(item.expense_account), frappe.bold(item.warehouse))
frappe.msgprint(msg, title=_("Expense Head Changed"))
-
item.expense_account = warehouse_account[item.warehouse]["account"]
else:
# check if 'Stock Received But Not Billed' account is credited in Purchase receipt or not
@@ -266,8 +261,8 @@ class PurchaseInvoice(BuyingController):
if negative_expense_booked_in_pr:
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
- msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
- msg += _("because expense is booked against this account in Purchase Receipt {}").format(frappe.bold(item.purchase_receipt))
+ msg = _("Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}").format(
+ item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt))
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
@@ -275,8 +270,9 @@ class PurchaseInvoice(BuyingController):
# If no purchase receipt present then book expense in 'Stock Received But Not Billed'
# This is done in cases when Purchase Invoice is created before Purchase Receipt
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
- msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
- msg += _("as no Purchase Receipt is created against Item {}. ").format(frappe.bold(item.item_code))
+ msg = _("Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}.").format(
+ item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code))
+ msg += "
"
msg += _("This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice")
frappe.msgprint(msg, title=_("Expense Head Changed"))
@@ -308,8 +304,8 @@ class PurchaseInvoice(BuyingController):
if not d.purchase_order:
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
msg += "
"
- msg += _("To submit the invoice without purchase order please set {} ").format(frappe.bold(_('Purchase Order Required')))
- msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
+ msg += _("To submit the invoice without purchase order please set {0} as {1} in {2}").format(
+ frappe.bold(_('Purchase Order Required')), frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
throw(msg, title=_("Mandatory Purchase Order"))
def pr_required(self):
@@ -323,8 +319,8 @@ class PurchaseInvoice(BuyingController):
if not d.purchase_receipt and d.item_code in stock_items:
msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code))
msg += "
"
- msg += _("To submit the invoice without purchase receipt please set {} ").format(frappe.bold(_('Purchase Receipt Required')))
- msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
+ msg += _("To submit the invoice without purchase receipt please set {0} as {1} in {2}").format(
+ frappe.bold(_('Purchase Receipt Required')), frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
throw(msg, title=_("Mandatory Purchase Receipt"))
def validate_write_off_account(self):
@@ -404,6 +400,7 @@ class PurchaseInvoice(BuyingController):
# because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1:
self.update_stock_ledger()
+ self.set_consumed_qty_in_po()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
@@ -454,8 +451,11 @@ class PurchaseInvoice(BuyingController):
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)
+
gl_entries = make_regional_gl_entries(gl_entries, self)
gl_entries = merge_similar_entries(gl_entries)
@@ -518,6 +518,8 @@ class PurchaseInvoice(BuyingController):
if d.category in ('Valuation', 'Total and Valuation')
and flt(d.base_tax_amount_after_discount_amount)]
+ exchange_rate_map, net_rate_map = get_purchase_document_details(self)
+
for item in self.get("items"):
if flt(item.base_net_amount):
account_currency = get_account_currency(item.expense_account)
@@ -635,6 +637,34 @@ class PurchaseInvoice(BuyingController):
"project": item.project or self.project
}, account_currency, item=item))
+ # check if the exchange rate has changed
+ if item.get('purchase_receipt'):
+ if exchange_rate_map[item.purchase_receipt] and \
+ self.conversion_rate != exchange_rate_map[item.purchase_receipt] and \
+ item.net_rate == net_rate_map[item.pr_detail]:
+
+ discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * \
+ (exchange_rate_map[item.purchase_receipt] - self.conversion_rate)
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": expense_account,
+ "against": self.supplier,
+ "debit": discrepancy_caused_by_exchange_rate_difference,
+ "cost_center": item.cost_center,
+ "project": item.project or self.project
+ }, account_currency, item=item)
+ )
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": self.get_company_default("exchange_gain_loss_account"),
+ "against": self.supplier,
+ "credit": discrepancy_caused_by_exchange_rate_difference,
+ "cost_center": item.cost_center,
+ "project": item.project or self.project
+ }, account_currency, item=item)
+ )
+
# If asset is bought through this document and not linked to PR
if self.update_stock and item.landed_cost_voucher_amount:
expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation")
@@ -1000,6 +1030,7 @@ class PurchaseInvoice(BuyingController):
if self.update_stock == 1:
self.update_stock_ledger()
self.delete_auto_created_batches()
+ self.set_consumed_qty_in_po()
self.make_gl_entries_on_cancel()
@@ -1090,6 +1121,7 @@ class PurchaseInvoice(BuyingController):
for d in self.taxes:
if d.account_head == tax_withholding_details.get("account_head"):
d.update(tax_withholding_details)
+
accounts.append(d.account_head)
if not accounts or tax_withholding_details.get("account_head") not in accounts:
@@ -1140,6 +1172,36 @@ class PurchaseInvoice(BuyingController):
if update:
self.db_set('status', self.status, update_modified = update_modified)
+# to get details of purchase invoice/receipt from which this doc was created for exchange rate difference handling
+def get_purchase_document_details(doc):
+ if doc.doctype == 'Purchase Invoice':
+ doc_reference = 'purchase_receipt'
+ items_reference = 'pr_detail'
+ parent_doctype = 'Purchase Receipt'
+ child_doctype = 'Purchase Receipt Item'
+ else:
+ doc_reference = 'purchase_invoice'
+ items_reference = 'purchase_invoice_item'
+ parent_doctype = 'Purchase Invoice'
+ child_doctype = 'Purchase Invoice Item'
+
+ purchase_receipts_or_invoices = []
+ items = []
+
+ for item in doc.get('items'):
+ if item.get(doc_reference):
+ purchase_receipts_or_invoices.append(item.get(doc_reference))
+ if item.get(items_reference):
+ items.append(item.get(items_reference))
+
+ exchange_rate_map = frappe._dict(frappe.get_all(parent_doctype, filters={'name': ('in',
+ purchase_receipts_or_invoices)}, fields=['name', 'conversion_rate'], as_list=1))
+
+ net_rate_map = frappe._dict(frappe.get_all(child_doctype, filters={'name': ('in',
+ items)}, fields=['name', 'net_rate'], as_list=1))
+
+ return exchange_rate_map, net_rate_map
+
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
list_context = get_list_context(context)
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 6dc0f56148b..db6f143eb80 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -16,6 +16,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_tra
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account
from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.buying.doctype.supplier.test_supplier import create_supplier
test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"]
test_ignore = ["Serial No"]
@@ -229,6 +230,27 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
+ def test_purchase_invoice_with_exchange_rate_difference(self):
+ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as create_purchase_invoice
+
+ 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()
+
+ # Get exchnage gain and loss account
+ exchange_gain_loss_account = frappe.db.get_value('Company', pi.company, 'exchange_gain_loss_account')
+
+ # 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(discrepancy_caused_by_exchange_rate_diff, amount)
+
def test_purchase_invoice_change_naming_series(self):
pi = frappe.copy_doc(test_records[1])
pi.insert()
@@ -620,8 +642,10 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(actual_qty_0, get_qty_after_transaction())
def test_subcontracting_via_purchase_invoice(self):
+ from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+ update_backflush_based_on('BOM')
make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC",
qty=100, basic_rate=100)
@@ -950,6 +974,217 @@ class TestPurchaseInvoice(unittest.TestCase):
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
+ from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
+
+ # create a new supplier to test
+ supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier',
+ tax_withholding_category = 'TDS - 194 - Dividends - Individual')
+
+ # Update tax withholding category with current fiscal year and rate details
+ update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
+
+ # Create Purchase Order with TDS applied
+ po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item')
+ po.apply_tds = 1
+ po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
+ po.save()
+ po.submit()
+
+ # Update Unrealized Profit / Loss Account which is used as default advance tax account
+ frappe.db.set_value('Company', '_Test Company', 'unrealized_profit_loss_account', '_Test Account Excise Duty - _TC')
+
+ # Create Payment Entry Against the order
+ payment_entry = get_payment_entry(dt='Purchase Order', dn=po.name)
+ payment_entry.paid_from = 'Cash - _TC'
+ payment_entry.save()
+ payment_entry.submit()
+
+ # Check GLE for Payment Entry
+ expected_gle = [
+ ['_Test Account Excise Duty - _TC', 3000, 0],
+ ['Cash - _TC', 0, 27000],
+ ['Creditors - _TC', 27000, 0],
+ ['TDS Payable - _TC', 0, 3000],
+ ]
+
+ gl_entries = frappe.db.sql("""select account, debit, credit
+ from `tabGL Entry`
+ where voucher_type='Payment Entry' and voucher_no=%s
+ order by account asc""", (payment_entry.name), as_dict=1)
+
+ 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)
+
+ # Create Purchase Invoice against Purchase Order
+ purchase_invoice = get_mapped_purchase_invoice(po.name)
+ purchase_invoice.allocate_advances_automatically = 1
+ purchase_invoice.items[0].item_code = '_Test Non Stock Item'
+ purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC'
+ purchase_invoice.save()
+ purchase_invoice.submit()
+
+ # Check GLE for Purchase Invoice
+ # Zero net effect on final TDS Payable on invoice
+ expected_gle = [
+ ['_Test Account Cost for Goods Sold - _TC', 30000],
+ ['_Test Account Excise Duty - _TC', -3000],
+ ['Creditors - _TC', -27000],
+ ['TDS Payable - _TC', 0]
+ ]
+
+ 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.amount)
+
+def update_tax_witholding_category(company, account, date):
+ from erpnext.accounts.utils import get_fiscal_year
+
+ fiscal_year = get_fiscal_year(date=date, company=company)
+
+ if not frappe.db.get_value('Tax Withholding Rate',
+ {'parent': 'TDS - 194 - Dividends - Individual', 'fiscal_year': fiscal_year[0]}):
+ tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual')
+ tds_category.append('rates', {
+ 'fiscal_year': fiscal_year[0],
+ 'tax_withholding_rate': 10,
+ 'single_threshold': 2500,
+ 'cumulative_threshold': 0
+ })
+ tds_category.save()
+
+ if not frappe.db.get_value('Tax Withholding Account',
+ {'parent': 'TDS - 194 - Dividends - Individual', 'account': account}):
+ tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual')
+ tds_category.append('accounts', {
+ 'company': company,
+ 'account': account
+ })
+ tds_category.save()
def unlink_payment_on_cancel_of_invoice(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
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 5801b17f66f..63dfff8921f 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/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index 10e1c73ea90..b39022dd758 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -272,7 +272,7 @@
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
- "label": "Rate ",
+ "label": "Rate",
"oldfieldname": "import_rate",
"oldfieldtype": "Currency",
"options": "currency",
@@ -854,7 +854,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-03-30 09:02:39.256602",
+ "modified": "2021-06-16 19:43:51.099386",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json
index f9fdc4b605a..1fa68e0a8a8 100644
--- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json
+++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json
@@ -12,6 +12,7 @@
"charge_type",
"row_id",
"included_in_print_rate",
+ "included_in_paid_amount",
"col_break1",
"account_head",
"description",
@@ -21,6 +22,7 @@
"cost_center",
"dimension_col_break",
"section_break_9",
+ "currency",
"tax_amount",
"tax_amount_after_discount_amount",
"total",
@@ -205,12 +207,28 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "account_head.account_currency",
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "label": "Account Currency",
+ "options": "Currency",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
+ "description": "If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry",
+ "fieldname": "included_in_paid_amount",
+ "fieldtype": "Check",
+ "label": "Considered In Paid Amount"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-06-14 01:43:50.750455",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index b735fde1ab7..bb556516702 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -17,7 +17,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
var me = this;
super.onload();
- this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet'];
+ this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', 'POS Closing Entry'];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format
this.frm.set_df_property("debit_to", "print_hide", 0);
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index e7dd6b8a606..0a9a105b7ca 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 023f4b049c1..88899130a24 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.accounts.utils import get_account_currency
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 @@ class SalesInvoice(SellingController):
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):
@@ -531,7 +531,7 @@ class SalesInvoice(SellingController):
# set pos values in items
for item in self.get("items"):
if item.get('item_code'):
- profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos)
+ profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos, update_data=True)
for fname, val in iteritems(profile_details):
if (not for_validate) or (for_validate and not item.get(fname)):
item.set(fname, val)
@@ -840,8 +840,11 @@ class SalesInvoice(SellingController):
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)
+
self.make_item_gl_entries(gl_entries)
# merge gl entries before adding pos entries
@@ -849,7 +852,6 @@ class SalesInvoice(SellingController):
self.make_loyalty_point_redemption_gle(gl_entries)
self.make_pos_gl_entries(gl_entries)
- self.make_gle_for_change_amount(gl_entries)
self.make_write_off_gl_entry(gl_entries)
self.make_gle_for_rounding_adjustment(gl_entries)
@@ -916,22 +918,33 @@ class SalesInvoice(SellingController):
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():
@@ -957,6 +970,12 @@ class SalesInvoice(SellingController):
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(
@@ -983,7 +1002,13 @@ class SalesInvoice(SellingController):
def make_pos_gl_entries(self, gl_entries):
if cint(self.is_pos):
+
+ skip_change_gl_entries = not cint(frappe.db.get_single_value('Accounts Settings', 'post_change_gl_entries'))
+
for payment_mode in self.payments:
+ if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
+ payment_mode.base_amount -= flt(self.change_amount)
+
if payment_mode.amount:
# POS, make payment entries
gl_entries.append(
@@ -1015,8 +1040,11 @@ class SalesInvoice(SellingController):
}, payment_mode_account_currency, item=self)
)
+ if not skip_change_gl_entries:
+ self.make_gle_for_change_amount(gl_entries)
+
def make_gle_for_change_amount(self, gl_entries):
- if cint(self.is_pos) and self.change_amount:
+ if self.change_amount:
if self.account_for_change_amount:
gl_entries.append(
self.get_gl_dict({
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index df6d4839041..c6e6e3da6f1 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 frappe.model.dynamic_links import get_dynamic_link_map
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction
from erpnext.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
@@ -713,7 +714,7 @@ class TestSalesInvoice(unittest.TestCase):
si.submit()
self.assertEqual(si.paid_amount, 100.0)
- self.pos_gl_entry(si, pos, 50)
+ self.validate_pos_gl_entry(si, pos, 50)
def test_pos_returns_with_repayment(self):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
@@ -749,7 +750,7 @@ class TestSalesInvoice(unittest.TestCase):
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
- pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",
+ make_purchase_receipt(company= "_Test Company with perpetual inventory",
item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
@@ -770,7 +771,45 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(pos.grand_total, 100.0)
self.assertEqual(pos.write_off_amount, -5)
- def pos_gl_entry(self, si, pos, cash_amount):
+ def test_pos_with_no_gl_entry_for_change_amount(self):
+ frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 0)
+
+ make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
+ expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
+
+ make_purchase_receipt(company= "_Test Company with perpetual inventory",
+ item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
+
+ pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
+ debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1",
+ income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1",
+ cost_center = "Main - TCP1", do_not_save=True)
+
+ pos.is_pos = 1
+ pos.update_stock = 1
+
+ taxes = get_taxes_and_charges()
+ pos.taxes = []
+ for tax in taxes:
+ pos.append("taxes", tax)
+
+ pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
+ pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 60})
+
+ pos.insert()
+ pos.submit()
+
+ self.assertEqual(pos.grand_total, 100.0)
+ self.assertEqual(pos.change_amount, 10)
+
+ self.validate_pos_gl_entry(pos, pos, 60, validate_without_change_gle=True)
+
+ frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 1)
+
+ def validate_pos_gl_entry(self, si, pos, cash_amount, validate_without_change_gle=False):
+ if validate_without_change_gle:
+ cash_amount -= pos.change_amount
+
# check stock ledger entries
sle = frappe.db.sql("""select * from `tabStock Ledger Entry`
where voucher_type = 'Sales Invoice' and voucher_no = %s""",
@@ -1031,6 +1070,36 @@ class TestSalesInvoice(unittest.TestCase):
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])
@@ -1870,6 +1939,8 @@ class TestSalesInvoice(unittest.TestCase):
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
@@ -1899,69 +1970,80 @@ class TestSalesInvoice(unittest.TestCase):
frappe.flags.country = country
def test_einvoice_json(self):
- from erpnext.regional.india.e_invoice.utils import make_einvoice
+ from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
- si = make_sales_invoice_for_ewaybill()
- si.naming_series = 'INV-2020-.#####'
- si.items = []
- si.append("items", {
- "item_code": "_Test Item",
- "uom": "Nos",
- "warehouse": "_Test Warehouse - _TC",
- "qty": 2000,
- "rate": 12,
- "income_account": "Sales - _TC",
- "expense_account": "Cost of Goods Sold - _TC",
- "cost_center": "_Test Cost Center - _TC",
- })
- si.append("items", {
- "item_code": "_Test Item 2",
- "uom": "Nos",
- "warehouse": "_Test Warehouse - _TC",
- "qty": 420,
- "rate": 15,
- "income_account": "Sales - _TC",
- "expense_account": "Cost of Goods Sold - _TC",
- "cost_center": "_Test Cost Center - _TC",
- })
+ si = get_sales_invoice_for_e_invoice()
si.discount_amount = 100
si.save()
einvoice = make_einvoice(si)
-
- total_item_ass_value = 0
- total_item_cgst_value = 0
- total_item_sgst_value = 0
- total_item_igst_value = 0
- total_item_value = 0
-
- for item in einvoice['ItemList']:
- total_item_ass_value += item['AssAmt']
- total_item_cgst_value += item['CgstAmt']
- total_item_sgst_value += item['SgstAmt']
- total_item_igst_value += item['IgstAmt']
- total_item_value += item['TotItemVal']
-
- self.assertTrue(item['AssAmt'], item['TotAmt'] - item['Discount'])
- self.assertTrue(item['TotItemVal'], item['AssAmt'] + item['CgstAmt'] + item['SgstAmt'] + item['IgstAmt'])
-
- value_details = einvoice['ValDtls']
-
- self.assertEqual(einvoice['Version'], '1.1')
- self.assertEqual(value_details['AssVal'], total_item_ass_value)
- self.assertEqual(value_details['CgstVal'], total_item_cgst_value)
- self.assertEqual(value_details['SgstVal'], total_item_sgst_value)
- self.assertEqual(value_details['IgstVal'], total_item_igst_value)
-
- calculated_invoice_value = \
- value_details['AssVal'] + value_details['CgstVal'] \
- + value_details['SgstVal'] + value_details['IgstVal'] \
- + value_details['OthChrg'] - value_details['Discount']
-
- self.assertTrue(value_details['TotInvVal'] - calculated_invoice_value < 0.1)
-
- self.assertEqual(value_details['TotInvVal'], si.base_grand_total)
self.assertTrue(einvoice['EwbDtls'])
+ validate_totals(einvoice)
+
+ si.apply_discount_on = 'Net Total'
+ si.save()
+ einvoice = make_einvoice(si)
+ validate_totals(einvoice)
+
+ [d.set('included_in_print_rate', 1) for d in si.taxes]
+ si.save()
+ einvoice = make_einvoice(si)
+ validate_totals(einvoice)
+
+ def test_item_tax_net_range(self):
+ item = create_item("T Shirt")
+
+ item.set('taxes', [])
+ item.append("taxes", {
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
+ "minimum_net_rate": 0,
+ "maximum_net_rate": 500
+ })
+
+ item.append("taxes", {
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
+ "minimum_net_rate": 501,
+ "maximum_net_rate": 1000
+ })
+
+ item.save()
+
+ sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
+ self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
+
+ # Apply discount
+ sales_invoice.apply_discount_on = 'Net Total'
+ sales_invoice.discount_amount = 300
+ sales_invoice.save()
+ self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
+
+def get_sales_invoice_for_e_invoice():
+ si = make_sales_invoice_for_ewaybill()
+ si.naming_series = 'INV-2020-.#####'
+ si.items = []
+ si.append("items", {
+ "item_code": "_Test Item",
+ "uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 2000,
+ "rate": 12,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ })
+
+ si.append("items", {
+ "item_code": "_Test Item 2",
+ "uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 420,
+ "rate": 15,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ })
+
+ return si
def make_test_address_for_ewaybill():
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
@@ -2012,6 +2094,30 @@ def make_test_address_for_ewaybill():
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({
@@ -2038,9 +2144,9 @@ def make_sales_invoice_for_ewaybill():
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()
@@ -2050,6 +2156,7 @@ def make_sales_invoice_for_ewaybill():
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'
@@ -2057,7 +2164,7 @@ def make_sales_invoice_for_ewaybill():
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
@@ -2065,7 +2172,7 @@ def make_sales_invoice_for_ewaybill():
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
@@ -2085,27 +2192,6 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
doc.assertEqual(expected_gle[i][2], gle.credit)
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
- def test_item_tax_validity(self):
- item = frappe.get_doc("Item", "_Test Item 2")
-
- if item.taxes:
- item.taxes = []
- item.save()
-
- item.append("taxes", {
- "item_tax_template": "_Test Item Tax Template 1 - _TC",
- "valid_from": add_days(nowdate(), 1)
- })
-
- item.save()
-
- sales_invoice = create_sales_invoice(item = "_Test Item 2", do_not_save=1)
- sales_invoice.items[0].item_tax_template = "_Test Item Tax Template 1 - _TC"
- self.assertRaises(frappe.ValidationError, sales_invoice.save)
-
- item.taxes = []
- item.save()
-
def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice")
args = frappe._dict(args)
@@ -2136,6 +2222,7 @@ def create_sales_invoice(**args):
"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 14bf4d81330..29422d68cf6 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 8e6952a93c4..6690bdafc34 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 3c8cb6b8518..1b7a0fe562e 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
@@ -1,8 +1,10 @@
{
+ "actions": [],
"creation": "2013-04-24 11:39:32",
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
+ "engine": "InnoDB",
"field_order": [
"charge_type",
"row_id",
@@ -10,12 +12,14 @@
"col_break_1",
"description",
"included_in_print_rate",
+ "included_in_paid_amount",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"section_break_8",
"rate",
"section_break_9",
+ "currency",
"tax_amount",
"total",
"tax_amount_after_discount_amount",
@@ -23,8 +27,7 @@
"base_tax_amount",
"base_total",
"base_tax_amount_after_discount_amount",
- "item_wise_tax_detail",
- "parenttype"
+ "item_wise_tax_detail"
],
"fields": [
{
@@ -173,17 +176,6 @@
"oldfieldtype": "Small Text",
"read_only": 1
},
- {
- "fieldname": "parenttype",
- "fieldtype": "Data",
- "hidden": 1,
- "in_filter": 1,
- "label": "Parenttype",
- "oldfieldname": "parenttype",
- "oldfieldtype": "Data",
- "print_hide": 1,
- "search_index": 1
- },
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
@@ -192,15 +184,34 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "account_head.account_currency",
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "label": "Account Currency",
+ "options": "Currency",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
+ "description": "If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry",
+ "fieldname": "included_in_paid_amount",
+ "fieldtype": "Check",
+ "label": "Considered In Paid Amount"
}
],
"idx": 1,
+ "index_web_pages_for_search": 1,
"istable": 1,
- "modified": "2019-05-25 22:59:38.740883",
+ "links": [],
+ "modified": "2021-06-14 01:44:36.899147",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges",
"owner": "Administrator",
"permissions": [],
+ "sort_field": "modified",
"sort_order": "ASC"
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.js b/erpnext/accounts/doctype/tax_rule/tax_rule.js
index 370890e4d8e..bc497163e8b 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 ef155381c0b..27467484329 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 ac1ffd9e750..cf7226822ed 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 @@ class TestTaxRule(unittest.TestCase):
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/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 5c1cbaa4aaa..b9ee4a0963f 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -49,7 +49,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
if not parties:
parties.append(party)
- fiscal_year = get_fiscal_year(inv.posting_date, company=inv.company)
+ fiscal_year = get_fiscal_year(inv.get('posting_date') or inv.get('transaction_date'), company=inv.company)
tax_details = get_tax_withholding_details(tax_withholding_category, fiscal_year[0], inv.company)
if not tax_details:
@@ -154,7 +154,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, p
tax_deducted = get_deducted_tax(taxable_vouchers, fiscal_year, tax_details)
tax_amount = 0
- posting_date = inv.posting_date
+ posting_date = inv.get('posting_date') or inv.get('transaction_date')
if party_type == 'Supplier':
ldc = get_lower_deduction_certificate(fiscal_year, pan_no)
if tax_deducted:
@@ -257,7 +257,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
if ldc and is_valid_certificate(
ldc.valid_from, ldc.valid_upto,
- inv.posting_date, tax_deducted,
+ inv.get('posting_date') or inv.get('transaction_date'), tax_deducted,
inv.net_total, ldc.certificate_limit
):
tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 968ab49b312..25d2cf10bd4 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -101,7 +101,7 @@ def merge_similar_entries(gl_map, precision=None):
def check_if_in_list(gle, gl_map, dimensions=None):
account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type',
- 'cost_center', 'project']
+ 'cost_center', 'project', 'voucher_detail_no']
if dimensions:
account_head_fieldnames = account_head_fieldnames + dimensions
@@ -185,10 +185,10 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
for d in gl_map:
if d.account == round_off_account:
round_off_gle = d
- if d.debit_in_account_currency:
- debit_credit_diff -= flt(d.debit_in_account_currency)
+ if d.debit:
+ debit_credit_diff -= flt(d.debit)
else:
- debit_credit_diff += flt(d.credit_in_account_currency)
+ debit_credit_diff += flt(d.credit)
round_off_account_exists = True
if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)):
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index e025fc69054..b97dc401e6a 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -542,6 +542,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
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 db605f7285a..b54646fd27a 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -99,7 +99,6 @@ class ReceivablePayableReport(object):
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,11 +578,12 @@ class ReceivablePayableReport(object):
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
docstatus < 2
+ and is_cancelled = 0
and party_type=%s
and (party is not null and party != '')
{1} {2} {3}"""
@@ -791,8 +791,6 @@ class ReceivablePayableReport(object):
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 9c9ada871c8..f1b231b6901 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 @@ def get_chart_data(filters, columns, data):
{'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 7793af737f9..56a67bb0989 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 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
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.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index 84f786814de..4a551b80124 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -36,16 +36,12 @@ frappe.query_reports["General Ledger"] = {
{
"fieldname":"account",
"label": __("Account"),
- "fieldtype": "Link",
+ "fieldtype": "MultiSelectList",
"options": "Account",
- "get_query": function() {
- var company = frappe.query_report.get_filter_value('company');
- return {
- "doctype": "Account",
- "filters": {
- "company": company,
- }
- }
+ get_data: function(txt) {
+ return frappe.db.get_link_options('Account', txt, {
+ company: frappe.query_report.get_filter_value("company")
+ });
}
},
{
@@ -135,7 +131,9 @@ frappe.query_reports["General Ledger"] = {
"label": __("Cost Center"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
- return frappe.db.get_link_options('Cost Center', txt);
+ return frappe.db.get_link_options('Cost Center', txt, {
+ company: frappe.query_report.get_filter_value("company")
+ });
}
},
{
@@ -143,7 +141,9 @@ frappe.query_reports["General Ledger"] = {
"label": __("Project"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
- return frappe.db.get_link_options('Project', txt);
+ return frappe.db.get_link_options('Project', txt, {
+ company: frappe.query_report.get_filter_value("company")
+ });
}
},
{
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 562df4f6f7d..1759fa3a48f 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -48,13 +48,18 @@ def validate_filters(filters, account_details):
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"))))
+
+ 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 not account_details.get(filters.account):
- frappe.throw(_("Account {0} does not exists").format(filters.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')]):
@@ -87,7 +92,19 @@ def set_account_currency(filters):
account_currency = None
if filters.get("account"):
- account_currency = get_account_currency(filters.account)
+ if len(filters.get("account")) == 1:
+ account_currency = get_account_currency(filters.account[0])
+ else:
+ currency = get_account_currency(filters.account[0])
+ is_same_account_currency = True
+ for account in filters.get("account"):
+ if get_account_currency(account) != currency:
+ is_same_account_currency = False
+ break
+
+ if is_same_account_currency:
+ account_currency = currency
+
elif filters.get("party"):
gle_currency = frappe.db.get_value(
"GL Entry", {
@@ -205,10 +222,10 @@ def get_gl_entries(filters, accounting_dimensions):
def get_conditions(filters):
conditions = []
- if filters.get("account") and not filters.get("include_dimensions"):
- lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"])
- conditions.append("""account in (select name from tabAccount
- where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt))
+
+ if filters.get("account"):
+ filters.account = get_accounts_with_children(filters.account)
+ conditions.append("account in %(account)s")
if filters.get("cost_center"):
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
@@ -266,6 +283,20 @@ def get_conditions(filters):
return "and {}".format(" and ".join(conditions)) if conditions else ""
+def get_accounts_with_children(accounts):
+ if not isinstance(accounts, list):
+ accounts = [d.strip() for d in accounts.strip().split(',') if d]
+
+ all_accounts = []
+ for d in accounts:
+ if frappe.db.exists("Account", d):
+ lft, rgt = frappe.db.get_value("Account", d, ["lft", "rgt"])
+ children = frappe.get_all("Account", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
+ all_accounts += [c.name for c in children]
+ else:
+ frappe.throw(_("Account: {0} does not exist").format(d))
+
+ return list(set(all_accounts))
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
data = []
diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py
index 60e675f2f1f..48bd7308bca 100644
--- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py
+++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py
@@ -168,21 +168,24 @@ def get_columns(filters):
"label": _("Income"),
"fieldtype": "Currency",
"options": "currency",
- "width": 120
+ "width": 305
+
},
{
"fieldname": "expense",
"label": _("Expense"),
"fieldtype": "Currency",
"options": "currency",
- "width": 120
+ "width": 305
+
},
{
"fieldname": "gross_profit_loss",
"label": _("Gross Profit / Loss"),
"fieldtype": "Currency",
"options": "currency",
- "width": 120
+ "width": 307
+
}
]
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 e15715dccd8..6b9df41f54e 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 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f
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/report/tds_payable_monthly/tds_payable_monthly.js b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js
index 344539eef6d..72de318a48c 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js
+++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js
@@ -54,6 +54,32 @@ frappe.query_reports["TDS Payable Monthly"] = {
frappe.query_report.refresh();
}
},
+ {
+ "fieldname":"purchase_order",
+ "label": __("Purchase Order"),
+ "fieldtype": "Link",
+ "options": "Purchase Order",
+ "get_query": function() {
+ return {
+ "filters": {
+ "name": ["in", frappe.query_report.invoices]
+ }
+ }
+ },
+ on_change: function() {
+ let supplier = frappe.query_report.get_filter_value('supplier');
+ if(!supplier) return; // return if no supplier selected
+
+ // filter invoices based on selected supplier
+ let invoices = [];
+ frappe.query_report.invoice_data.map(d => {
+ if(d.supplier==supplier)
+ invoices.push(d.name)
+ });
+ frappe.query_report.invoices = invoices;
+ frappe.query_report.refresh();
+ }
+ },
{
"fieldname":"from_date",
"label": __("From Date"),
@@ -75,15 +101,17 @@ frappe.query_reports["TDS Payable Monthly"] = {
onload: function(report) {
// fetch all tds applied invoices
frappe.call({
- "method": "erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly.get_tds_invoices",
+ "method": "erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly.get_tds_invoices_and_orders",
callback: function(r) {
let invoices = [];
+
r.message.map(d => {
invoices.push(d.name);
});
- report["invoice_data"] = r.message;
+ report["invoice_data"] = r.message.invoices;
report["invoices"] = invoices;
+
}
});
}
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
index a9fb237a048..ceefa31cfa1 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
+++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
@@ -11,11 +11,14 @@ def execute(filters=None):
validate_filters(filters)
set_filters(filters)
+ # TDS payment entries
+ payment_entries = get_payment_entires(filters)
+
columns = get_columns(filters)
- if not filters["invoices"]:
+ if not filters.get("invoices"):
return columns, []
- res = get_result(filters)
+ res = get_result(filters, payment_entries)
return columns, res
@@ -27,8 +30,9 @@ def validate_filters(filters):
def set_filters(filters):
invoices = []
- if not filters["invoices"]:
- filters["invoices"] = get_tds_invoices()
+ if not filters.get("invoices"):
+ filters["invoices"] = get_tds_invoices_and_orders()
+
if filters.supplier and filters.purchase_invoice:
for d in filters["invoices"]:
if d.name == filters.purchase_invoice and d.supplier == filters.supplier:
@@ -41,13 +45,29 @@ def set_filters(filters):
for d in filters["invoices"]:
if d.name == filters.purchase_invoice:
invoices.append(d)
+ elif filters.supplier and filters.purchase_order:
+ for d in filters.get("invoices"):
+ if d.name == filters.purchase_order and d.supplier == filters.supplier:
+ invoices.append(d)
+ elif filters.supplier and not filters.purchase_order:
+ for d in filters.get("invoices"):
+ if d.supplier == filters.supplier:
+ invoices.append(d)
+ elif filters.purchase_order and not filters.supplier:
+ for d in filters.get("invoices"):
+ if d.name == filters.purchase_order:
+ invoices.append(d)
filters["invoices"] = invoices if invoices else filters["invoices"]
filters.naming_series = frappe.db.get_single_value('Buying Settings', 'supp_master_name')
-def get_result(filters):
- supplier_map, tds_docs = get_supplier_map(filters)
- gle_map = get_gle_map(filters)
+ #print(filters.get('invoices'))
+
+def get_result(filters, payment_entries):
+ supplier_map, tds_docs = get_supplier_map(filters, payment_entries)
+ documents = [d.get('name') for d in filters.get('invoices')] + [d.get('name') for d in payment_entries]
+
+ gle_map = get_gle_map(filters, documents)
out = []
for d in gle_map:
@@ -62,10 +82,11 @@ def get_result(filters):
for k in gle_map[d]:
if k.party == supplier_map[d] and k.credit > 0:
- total_amount_credited += k.credit
- elif account_list and k.account == account and k.credit > 0:
- tds_deducted = k.credit
- total_amount_credited += k.credit
+ total_amount_credited += (k.credit - k.debit)
+ elif account_list and k.account == account and (k.credit - k.debit) > 0:
+ tds_deducted = (k.credit - k.debit)
+ total_amount_credited += (k.credit - k.debit)
+ voucher_type = k.voucher_type
rate = [i.tax_withholding_rate for i in tds_doc.rates
if i.fiscal_year == gle_map[d][0].fiscal_year]
@@ -73,32 +94,36 @@ def get_result(filters):
if rate and len(rate) > 0 and tds_deducted:
rate = rate[0]
- if getdate(filters.from_date) <= gle_map[d][0].posting_date \
- and getdate(filters.to_date) >= gle_map[d][0].posting_date:
- row = [supplier.pan, supplier.name]
+ row = [supplier.pan, supplier.name]
- if filters.naming_series == 'Naming Series':
- row.append(supplier.supplier_name)
+ if filters.naming_series == 'Naming Series':
+ row.append(supplier.supplier_name)
- row.extend([tds_doc.name, supplier.supplier_type, rate, total_amount_credited,
- tds_deducted, gle_map[d][0].posting_date, "Purchase Invoice", d])
- out.append(row)
+ row.extend([tds_doc.name, supplier.supplier_type, rate, total_amount_credited,
+ tds_deducted, gle_map[d][0].posting_date, voucher_type, d])
+ out.append(row)
return out
-def get_supplier_map(filters):
+def get_supplier_map(filters, payment_entries):
# create a supplier_map of the form {"purchase_invoice": {supplier_name, pan, tds_name}}
# pre-fetch all distinct applicable tds docs
supplier_map, tds_docs = {}, {}
pan = "pan" if frappe.db.has_column("Supplier", "pan") else "tax_id"
+ supplier_list = [d.supplier for d in filters["invoices"]]
+
supplier_detail = frappe.db.get_all('Supplier',
- {"name": ["in", [d.supplier for d in filters["invoices"]]]},
+ {"name": ["in", supplier_list]},
["tax_withholding_category", "name", pan+" as pan", "supplier_type", "supplier_name"])
for d in filters["invoices"]:
supplier_map[d.get("name")] = [k for k in supplier_detail
if k.name == d.get("supplier")][0]
+ for d in payment_entries:
+ supplier_map[d.get("name")] = [k for k in supplier_detail
+ if k.name == d.get("supplier")][0]
+
for d in supplier_detail:
if d.get("tax_withholding_category") not in tds_docs:
tds_docs[d.get("tax_withholding_category")] = \
@@ -106,13 +131,19 @@ def get_supplier_map(filters):
return supplier_map, tds_docs
-def get_gle_map(filters):
+def get_gle_map(filters, documents):
# create gle_map of the form
# {"purchase_invoice": list of dict of all gle created for this invoice}
gle_map = {}
- gle = frappe.db.get_all('GL Entry',\
- {"voucher_no": ["in", [d.get("name") for d in filters["invoices"]]], 'is_cancelled': 0},
- ["fiscal_year", "credit", "debit", "account", "voucher_no", "posting_date"])
+
+ gle = frappe.db.get_all('GL Entry',
+ {
+ "voucher_no": ["in", documents],
+ 'is_cancelled': 0,
+ 'posting_date': ("between", [filters.get('from_date'), filters.get('to_date')]),
+ },
+ ["fiscal_year", "credit", "debit", "account", "voucher_no", "posting_date", "voucher_type"],
+ )
for d in gle:
if not d.voucher_no in gle_map:
@@ -201,8 +232,26 @@ def get_columns(filters):
return columns
+def get_payment_entires(filters):
+ filter_dict = {
+ 'posting_date': ("between", [filters.get('from_date'), filters.get('to_date')]),
+ 'party_type': 'Supplier',
+ 'apply_tax_withholding_amount': 1
+ }
+
+ if filters.get('purchase_invoice') or filters.get('purchase_order'):
+ parent = frappe.db.get_all('Payment Entry Reference',
+ {'reference_name': ('in', [d.get('name') for d in filters.get('invoices')])}, ['parent'])
+
+ filter_dict.update({'name': ('in', [d.get('parent') for d in parent])})
+
+ payment_entries = frappe.get_all('Payment Entry', fields=['name', 'party_name as supplier'],
+ filters=filter_dict)
+
+ return payment_entries
+
@frappe.whitelist()
-def get_tds_invoices():
+def get_tds_invoices_and_orders():
# fetch tds applicable supplier and fetch invoices for these suppliers
suppliers = [d.name for d in frappe.db.get_list("Supplier",
{"tax_withholding_category": ["!=", ""]}, ["name"])]
@@ -210,7 +259,12 @@ def get_tds_invoices():
invoices = frappe.db.get_list("Purchase Invoice",
{"supplier": ["in", suppliers]}, ["name", "supplier"])
+ orders = frappe.db.get_list("Purchase Order",
+ {"supplier": ["in", suppliers]}, ["name", "supplier"])
+
+ invoices = invoices + orders
invoices = [d for d in invoices if d.supplier]
+
frappe.cache().hset("invoices", frappe.session.user, invoices)
return invoices
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 66a9b601257..1cdbd8d38a6 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -472,7 +472,8 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
"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 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
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 @@ def get_children(doctype, parent, company, is_root=False):
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/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json
index b04b03ca79c..821fa4d2af4 100644
--- a/erpnext/accounts/workspace/accounting/accounting.json
+++ b/erpnext/accounts/workspace/accounting/accounting.json
@@ -707,6 +707,7 @@
"link_to": "GST Settings",
"link_type": "DocType",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -717,6 +718,7 @@
"link_to": "GST HSN Code",
"link_type": "DocType",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -727,6 +729,7 @@
"link_to": "GSTR-1",
"link_type": "Report",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -737,6 +740,7 @@
"link_to": "GSTR-2",
"link_type": "Report",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -747,6 +751,7 @@
"link_to": "GSTR 3B Report",
"link_type": "DocType",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -757,6 +762,7 @@
"link_to": "GST Sales Register",
"link_type": "Report",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -767,6 +773,7 @@
"link_to": "GST Purchase Register",
"link_type": "Report",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -777,6 +784,7 @@
"link_to": "GST Itemised Sales Register",
"link_type": "Report",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -787,6 +795,7 @@
"link_to": "GST Itemised Purchase Register",
"link_type": "Report",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -797,6 +806,7 @@
"link_to": "C-Form",
"link_type": "DocType",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -807,6 +817,7 @@
"link_to": "Lower Deduction Certificate",
"link_type": "DocType",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -1065,7 +1076,7 @@
"type": "Link"
}
],
- "modified": "2021-05-13 13:44:56.249888",
+ "modified": "2021-06-10 03:17:31.427945",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
@@ -1120,4 +1131,4 @@
"type": "Dashboard"
}
]
-}
+}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 6f1bb28f370..922cc4a7b26 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -82,24 +82,46 @@ frappe.ui.form.on('Asset', {
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 @@ frappe.ui.form.on('Asset', {
"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 @@ frappe.ui.form.on('Asset', {
})
},
+ 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 421b9a6c378..de060757e2e 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 8799275fc4e..66f0bdcd588 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -168,17 +168,24 @@ class Asset(AccountsController):
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 @@ class Asset(AccountsController):
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 @@ class Asset(AccountsController):
# 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 @@ class Asset(AccountsController):
"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 @@ class Asset(AccountsController):
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 -
@@ -625,9 +648,18 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan
})
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 @@ def get_depreciation_amount(asset, depreciable_value, row):
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 8f0afb42b2c..8fdbbf95d47 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -176,22 +176,34 @@ def restore_asset(asset_name):
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 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None)
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 8845f24d104..59fbe3b0301 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -125,7 +125,6 @@ class TestAsset(unittest.TestCase):
"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 @@ class TestAsset(unittest.TestCase):
"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 @@ class TestAsset(unittest.TestCase):
"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 @@ class TestAsset(unittest.TestCase):
"depreciation_start_date": "2030-12-31"
})
- asset.insert()
asset.save()
expected_schedules = [
@@ -247,7 +244,6 @@ class TestAsset(unittest.TestCase):
"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 @@ class TestAsset(unittest.TestCase):
"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 @@ class TestAsset(unittest.TestCase):
"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 @@ class TestAsset(unittest.TestCase):
"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 @@ class TestAsset(unittest.TestCase):
"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 @@ def create_asset(**args):
"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 @@ def create_asset(**args):
"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 d9b7b695f7f..e5a5f194c1b 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 4ba2b4474a9..1cebfff66e5 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 @@ frappe.ui.form.on('Asset Repair', {
}
});
}
+
+ 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 d338fc0fb79..ba3189887ce 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.json
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.json
@@ -7,38 +7,43 @@
"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",
@@ -50,18 +55,6 @@
"fieldname": "column_break_2",
"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",
@@ -74,33 +67,20 @@
"label": "Failure Date",
"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 049b931b5e8..d32fdf7054f 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 3d325a9683c..30bbb37851e 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/assets/doctype/asset_repair_consumed_item/__init__.py b/erpnext/assets/doctype/asset_repair_consumed_item/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
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 00000000000..528f0ec986a
--- /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 00000000000..fa22a5712f4
--- /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/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 630a1dc8cd5..b9c77d59b18 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -9,13 +9,14 @@
"supp_master_name",
"supplier_group",
"buying_price_list",
+ "maintain_same_rate_action",
+ "role_to_override_stop_action",
"column_break_3",
"po_required",
"pr_required",
"maintain_same_rate",
- "maintain_same_rate_action",
- "role_to_override_stop_action",
"allow_multiple_items",
+ "bill_for_rejected_quantity_in_purchase_invoice",
"subcontract",
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
@@ -108,6 +109,13 @@
"fieldtype": "Link",
"label": "Role Allowed to Override Stop Action",
"options": "Role"
+ },
+ {
+ "default": "1",
+ "description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.",
+ "fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
+ "fieldtype": "Check",
+ "label": "Bill for Rejected Quantity in Purchase Invoice"
}
],
"icon": "fa fa-cog",
@@ -115,7 +123,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-04-04 20:01:44.087066",
+ "modified": "2021-06-24 10:38:28.934525",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 384bbc53857..521432d296b 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -45,6 +45,47 @@ frappe.ui.form.on("Purchase Order", {
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+ },
+
+ apply_tds: function(frm) {
+ if (!frm.doc.apply_tds) {
+ frm.set_value("tax_withholding_category", '');
+ } else {
+ frm.set_value("tax_withholding_category", frm.supplier_tds);
+ }
+ },
+
+ refresh: function(frm) {
+ frm.trigger('get_materials_from_supplier');
+ },
+
+ get_materials_from_supplier: function(frm) {
+ let po_details = [];
+
+ if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) {
+ frm.doc.supplied_items.forEach(d => {
+ if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
+ po_details.push(d.name)
+ }
+ });
+ }
+
+ if (po_details && po_details.length) {
+ frm.add_custom_button(__('Return of Components'), () => {
+ frm.call({
+ method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier',
+ freeze: true,
+ freeze_message: __('Creating Stock Entry'),
+ args: { purchase_order: frm.doc.name, po_details: po_details },
+ callback: function(r) {
+ if (r && r.message) {
+ const doc = frappe.model.sync(r.message);
+ frappe.set_route("Form", doc[0].doctype, doc[0].name);
+ }
+ }
+ });
+ }, __('Create'));
+ }
}
});
@@ -209,7 +250,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
}
has_unsupplied_items() {
- return this.frm.doc['supplied_items'].some(item => item.required_qty != item.supplied_qty)
+ return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty);
}
make_stock_entry() {
@@ -313,7 +354,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
if(me.values) {
me.values.sub_con_rm_items.map((row,i) => {
if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) {
- frappe.throw(__("Item Code, warehouse, quantity are required on row {0}", [i+1]));
+ let row_id = i+1;
+ frappe.throw(__("Item Code, warehouse and quantity are required on row {0}", [row_id]));
}
})
me._make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children())
@@ -504,12 +546,14 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
],
primary_action: function() {
var data = d.get_values();
+ let reason_for_hold = 'Reason for hold: ' + data.reason_for_hold;
+
frappe.call({
method: "frappe.desk.form.utils.add_comment",
args: {
reference_doctype: me.frm.doctype,
reference_name: me.frm.docname,
- content: __('Reason for hold: ')+data.reason_for_hold,
+ content: __(reason_for_hold),
comment_email: frappe.session.user,
comment_by: frappe.session.user_fullname
},
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index ee2beea67f9..bb0ad60cabc 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -14,6 +14,8 @@
"supplier",
"get_items_from_open_material_requests",
"supplier_name",
+ "apply_tds",
+ "tax_withholding_category",
"column_break1",
"company",
"transaction_date",
@@ -142,7 +144,9 @@
{
"fieldname": "supplier_section",
"fieldtype": "Section Break",
- "options": "fa fa-user"
+ "options": "fa fa-user",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
@@ -152,7 +156,9 @@
"hidden": 1,
"label": "Title",
"no_copy": 1,
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "naming_series",
@@ -164,7 +170,9 @@
"options": "PUR-ORD-.YYYY.-",
"print_hide": 1,
"reqd": 1,
- "set_only_once": 1
+ "set_only_once": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"bold": 1,
@@ -178,14 +186,18 @@
"options": "Supplier",
"print_hide": 1,
"reqd": 1,
- "search_index": 1
+ "search_index": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:doc.supplier && doc.docstatus===0 && (!(doc.items && doc.items.length) || (doc.items.length==1 && !doc.items[0].item_code))",
"description": "Fetch items based on Default Supplier.",
"fieldname": "get_items_from_open_material_requests",
"fieldtype": "Button",
- "label": "Get Items from Open Material Requests"
+ "label": "Get Items from Open Material Requests",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"bold": 1,
@@ -194,7 +206,9 @@
"fieldtype": "Data",
"in_global_search": 1,
"label": "Supplier Name",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "company",
@@ -206,13 +220,17 @@
"options": "Company",
"print_hide": 1,
"remember_last_selected_value": 1,
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break1",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break",
"print_width": "50%",
+ "show_days": 1,
+ "show_seconds": 1,
"width": "50%"
},
{
@@ -224,27 +242,35 @@
"oldfieldname": "transaction_date",
"oldfieldtype": "Date",
"reqd": 1,
- "search_index": 1
+ "search_index": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
"fieldname": "schedule_date",
"fieldtype": "Date",
- "label": "Required By"
+ "label": "Required By",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.docstatus===1",
"fieldname": "order_confirmation_no",
"fieldtype": "Data",
- "label": "Order Confirmation No"
+ "label": "Order Confirmation No",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.order_confirmation_no",
"fieldname": "order_confirmation_date",
"fieldtype": "Date",
- "label": "Order Confirmation Date"
+ "label": "Order Confirmation Date",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "amended_from",
@@ -256,19 +282,25 @@
"oldfieldtype": "Data",
"options": "Purchase Order",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "drop_ship",
"fieldtype": "Section Break",
- "label": "Drop Ship"
+ "label": "Drop Ship",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "customer",
"fieldtype": "Link",
"label": "Customer",
"options": "Customer",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"bold": 1,
@@ -276,31 +308,41 @@
"fieldtype": "Data",
"label": "Customer Name",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_19",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "customer_contact_person",
"fieldtype": "Link",
"label": "Customer Contact",
- "options": "Contact"
+ "options": "Contact",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "customer_contact_display",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Customer Contact",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "customer_contact_mobile",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Customer Mobile No",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "customer_contact_email",
@@ -308,27 +350,35 @@
"hidden": 1,
"label": "Customer Contact Email",
"options": "Email",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "section_addresses",
"fieldtype": "Section Break",
- "label": "Address and Contact"
+ "label": "Address and Contact",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "supplier_address",
"fieldtype": "Link",
"label": "Supplier Address",
"options": "Address",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "contact_person",
"fieldtype": "Link",
"label": "Supplier Contact",
"options": "Contact",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "address_display",
@@ -355,32 +405,42 @@
"label": "Contact Email",
"options": "Email",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "col_break_address",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "shipping_address",
"fieldtype": "Link",
"label": "Company Shipping Address",
"options": "Address",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "shipping_address_display",
"fieldtype": "Small Text",
"label": "Shipping Address Details",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "currency_and_price_list",
"fieldtype": "Section Break",
"label": "Currency and Price List",
- "options": "fa fa-tag"
+ "options": "fa fa-tag",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "currency",
@@ -390,7 +450,9 @@
"oldfieldtype": "Select",
"options": "Currency",
"print_hide": 1,
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "conversion_rate",
@@ -400,18 +462,24 @@
"oldfieldtype": "Currency",
"precision": "9",
"print_hide": 1,
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "cb_price_list",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "buying_price_list",
"fieldtype": "Link",
"label": "Price List",
"options": "Price List",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "price_list_currency",
@@ -419,14 +487,18 @@
"label": "Price List Currency",
"options": "Currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "plc_conversion_rate",
"fieldtype": "Float",
"label": "Price List Exchange Rate",
"precision": "9",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
@@ -435,7 +507,9 @@
"label": "Ignore Pricing Rule",
"no_copy": 1,
"permlevel": 1,
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "sec_warehouse",
@@ -448,11 +522,15 @@
"fieldtype": "Link",
"label": "Set Target Warehouse",
"options": "Warehouse",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "col_break_warehouse",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "No",
@@ -461,26 +539,34 @@
"in_standard_filter": 1,
"label": "Supply Raw Materials",
"options": "No\nYes",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:doc.is_subcontracted==\"Yes\"",
"fieldname": "supplier_warehouse",
"fieldtype": "Link",
"label": "Supplier Warehouse",
- "options": "Warehouse"
+ "options": "Warehouse",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "items_section",
"fieldtype": "Section Break",
"hide_border": 1,
"oldfieldtype": "Section Break",
- "options": "fa fa-shopping-cart"
+ "options": "fa fa-shopping-cart",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "scan_barcode",
"fieldtype": "Data",
- "label": "Scan Barcode"
+ "label": "Scan Barcode",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_bulk_edit": 1,
@@ -490,46 +576,61 @@
"oldfieldname": "po_details",
"oldfieldtype": "Table",
"options": "Purchase Order Item",
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "section_break_48",
"fieldtype": "Section Break",
- "label": "Pricing Rules"
+ "label": "Pricing Rules",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "pricing_rules",
"fieldtype": "Table",
"label": "Purchase Order Pricing Rule",
"options": "Pricing Rule Detail",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible_depends_on": "supplied_items",
"fieldname": "raw_material_details",
"fieldtype": "Section Break",
- "label": "Raw Materials Supplied"
+ "label": "Raw Materials Supplied",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "supplied_items",
"fieldtype": "Table",
"label": "Supplied Items",
+ "no_copy": 1,
"oldfieldname": "po_raw_material_details",
"oldfieldtype": "Table",
"options": "Purchase Order Item Supplied",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "sb_last_purchase",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "total_qty",
"fieldtype": "Float",
"label": "Total Quantity",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "base_total",
@@ -537,7 +638,9 @@
"label": "Total (Company Currency)",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "base_net_total",
@@ -548,18 +651,24 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_26",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "total",
"fieldtype": "Currency",
"label": "Total",
"options": "currency",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "net_total",
@@ -569,20 +678,26 @@
"oldfieldtype": "Currency",
"options": "currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "total_net_weight",
"fieldtype": "Float",
"label": "Total Net Weight",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "taxes_section",
"fieldtype": "Section Break",
"oldfieldtype": "Section Break",
- "options": "fa fa-money"
+ "options": "fa fa-money",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "taxes_and_charges",
@@ -591,18 +706,24 @@
"oldfieldname": "purchase_other_charges",
"oldfieldtype": "Link",
"options": "Purchase Taxes and Charges Template",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_50",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "shipping_rule",
"fieldtype": "Link",
"label": "Shipping Rule",
"options": "Shipping Rule",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "section_break_52",
@@ -615,13 +736,17 @@
"label": "Purchase Taxes and Charges",
"oldfieldname": "purchase_tax_details",
"oldfieldtype": "Table",
- "options": "Purchase Taxes and Charges"
+ "options": "Purchase Taxes and Charges",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "sec_tax_breakup",
"fieldtype": "Section Break",
- "label": "Tax Breakup"
+ "label": "Tax Breakup",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "other_charges_calculation",
@@ -630,14 +755,18 @@
"no_copy": 1,
"oldfieldtype": "HTML",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "totals",
"fieldtype": "Section Break",
"label": "Taxes and Charges",
"oldfieldtype": "Section Break",
- "options": "fa fa-money"
+ "options": "fa fa-money",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "base_taxes_and_charges_added",
@@ -648,7 +777,9 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "base_taxes_and_charges_deducted",
@@ -659,7 +790,9 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "base_total_taxes_and_charges",
@@ -671,11 +804,15 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_39",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "taxes_and_charges_added",
@@ -686,7 +823,9 @@
"oldfieldtype": "Currency",
"options": "currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "taxes_and_charges_deducted",
@@ -697,7 +836,9 @@
"oldfieldtype": "Currency",
"options": "currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "total_taxes_and_charges",
@@ -706,14 +847,18 @@
"label": "Total Taxes and Charges",
"options": "currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "apply_discount_on",
"fieldname": "discount_section",
"fieldtype": "Section Break",
- "label": "Additional Discount"
+ "label": "Additional Discount",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "Grand Total",
@@ -721,7 +866,9 @@
"fieldtype": "Select",
"label": "Apply Additional Discount On",
"options": "\nGrand Total\nNet Total",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "base_discount_amount",
@@ -729,24 +876,32 @@
"label": "Additional Discount Amount (Company Currency)",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_45",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "additional_discount_percentage",
"fieldtype": "Float",
"label": "Additional Discount Percentage",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "discount_amount",
"fieldtype": "Currency",
"label": "Additional Discount Amount",
"options": "currency",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "totals_section",
@@ -762,16 +917,21 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
+ "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment (Company Currency)",
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"description": "In Words will be visible once you save the Purchase Order.",
@@ -782,7 +942,9 @@
"oldfieldname": "in_words",
"oldfieldtype": "Data",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "base_rounded_total",
@@ -792,12 +954,16 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break4",
"fieldtype": "Column Break",
- "oldfieldtype": "Column Break"
+ "oldfieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "grand_total",
@@ -807,29 +973,38 @@
"oldfieldname": "grand_total_import",
"oldfieldtype": "Currency",
"options": "currency",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
+ "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment",
"no_copy": 1,
"options": "currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "rounded_total",
"fieldtype": "Currency",
"label": "Rounded Total",
"options": "currency",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
- "label": "Disable Rounded Total"
+ "label": "Disable Rounded Total",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "in_words",
@@ -839,7 +1014,9 @@
"oldfieldname": "in_words_import",
"oldfieldtype": "Data",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "advance_paid",
@@ -848,19 +1025,25 @@
"no_copy": 1,
"options": "party_account_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "payment_schedule_section",
"fieldtype": "Section Break",
- "label": "Payment Terms"
+ "label": "Payment Terms",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "payment_terms_template",
"fieldtype": "Link",
"label": "Payment Terms Template",
- "options": "Payment Terms Template"
+ "options": "Payment Terms Template",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "payment_schedule",
@@ -868,7 +1051,9 @@
"label": "Payment Schedule",
"no_copy": 1,
"options": "Payment Schedule",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
@@ -877,7 +1062,9 @@
"fieldtype": "Section Break",
"label": "Terms and Conditions",
"oldfieldtype": "Section Break",
- "options": "fa fa-legal"
+ "options": "fa fa-legal",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "tc_name",
@@ -886,21 +1073,27 @@
"oldfieldname": "tc_name",
"oldfieldtype": "Link",
"options": "Terms and Conditions",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "terms",
"fieldtype": "Text Editor",
"label": "Terms and Conditions",
"oldfieldname": "terms",
- "oldfieldtype": "Text Editor"
+ "oldfieldtype": "Text Editor",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "more_info",
"fieldtype": "Section Break",
"label": "More Information",
- "oldfieldtype": "Section Break"
+ "oldfieldtype": "Section Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "Draft",
@@ -915,7 +1108,9 @@
"print_hide": 1,
"read_only": 1,
"reqd": 1,
- "search_index": 1
+ "search_index": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "ref_sq",
@@ -926,7 +1121,9 @@
"oldfieldtype": "Data",
"options": "Supplier Quotation",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "party_account_currency",
@@ -936,18 +1133,24 @@
"no_copy": 1,
"options": "Currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "inter_company_order_reference",
"fieldtype": "Link",
"label": "Inter Company Order Reference",
"options": "Sales Order",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_74",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:!doc.__islocal",
@@ -957,7 +1160,9 @@
"label": "% Received",
"no_copy": 1,
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:!doc.__islocal",
@@ -967,7 +1172,9 @@
"label": "% Billed",
"no_copy": 1,
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
@@ -977,6 +1184,8 @@
"oldfieldtype": "Column Break",
"print_hide": 1,
"print_width": "50%",
+ "show_days": 1,
+ "show_seconds": 1,
"width": "50%"
},
{
@@ -987,7 +1196,9 @@
"oldfieldname": "letter_head",
"oldfieldtype": "Select",
"options": "Letter Head",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
@@ -999,11 +1210,15 @@
"oldfieldtype": "Link",
"options": "Print Heading",
"print_hide": 1,
- "report_hide": 1
+ "report_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_86",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
@@ -1011,19 +1226,25 @@
"fieldname": "group_same_items",
"fieldtype": "Check",
"label": "Group same items",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "language",
"fieldtype": "Data",
"label": "Print Language",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "subscription_section",
"fieldtype": "Section Break",
- "label": "Subscription Section"
+ "label": "Subscription Section",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
@@ -1031,7 +1252,9 @@
"fieldtype": "Date",
"label": "From Date",
"no_copy": 1,
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
@@ -1039,11 +1262,15 @@
"fieldtype": "Date",
"label": "To Date",
"no_copy": 1,
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_97",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "auto_repeat",
@@ -1052,27 +1279,35 @@
"no_copy": 1,
"options": "Auto Repeat",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
"depends_on": "eval: doc.auto_repeat",
"fieldname": "update_auto_repeat_reference",
"fieldtype": "Button",
- "label": "Update Auto Repeat Reference"
+ "label": "Update Auto Repeat Reference",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "tax_category",
"fieldtype": "Link",
"label": "Tax Category",
- "options": "Tax Category"
+ "options": "Tax Category",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "supplied_items",
"fieldname": "set_reserve_warehouse",
"fieldtype": "Link",
"label": "Set Reserve Warehouse",
- "options": "Warehouse"
+ "options": "Warehouse",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
@@ -1082,7 +1317,9 @@
},
{
"fieldname": "column_break_75",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "billing_address",
@@ -1118,13 +1355,30 @@
"label": "Represents Company",
"options": "Company",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "apply_tds",
+ "fieldtype": "Check",
+ "label": "Apply Tax Withholding Amount",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "depends_on": "eval: doc.apply_tds",
+ "fieldname": "tax_withholding_category",
+ "fieldtype": "Link",
+ "label": "Tax Withholding Category",
+ "options": "Tax Withholding Category",
+ "show_days": 1,
+ "show_seconds": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2021-01-20 22:07:23.487138",
+ "modified": "2021-05-30 15:17:53.663648",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index c023accbfc0..eaa502ff7f0 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -14,11 +14,11 @@ from frappe.desk.notifications import clear_doctype_notifications
from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status
from erpnext.stock.utils import get_bin
from erpnext.accounts.party import get_party_account_currency
-from six import string_types
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
-from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\
- unlink_inter_company_doc
+from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
+from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_party,
+ update_linked_doc, unlink_inter_company_doc)
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
@@ -39,11 +39,18 @@ class PurchaseOrder(BuyingController):
'percent_join_field': 'material_request'
}]
+ def onload(self):
+ supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
+ self.set_onload("supplier_tds", supplier_tds)
+
def validate(self):
super(PurchaseOrder, self).validate()
self.set_status()
+ # apply tax withholding only if checked and applicable
+ self.set_tax_withholding()
+
self.validate_supplier()
self.validate_schedule_date()
validate_for_items(self)
@@ -87,6 +94,33 @@ class PurchaseOrder(BuyingController):
if cint(frappe.db.get_single_value('Buying Settings', 'maintain_same_rate')):
self.validate_rate_with_reference_doc([["Supplier Quotation", "supplier_quotation", "supplier_quotation_item"]])
+ def set_tax_withholding(self):
+ if not self.apply_tds:
+ return
+
+ tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category)
+
+ if not tax_withholding_details:
+ return
+
+ accounts = []
+ for d in self.taxes:
+ if d.account_head == tax_withholding_details.get("account_head"):
+ d.update(tax_withholding_details)
+ accounts.append(d.account_head)
+
+ if not accounts or tax_withholding_details.get("account_head") not in accounts:
+ self.append("taxes", tax_withholding_details)
+
+ to_remove = [d for d in self.taxes
+ if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")]
+
+ for d in to_remove:
+ self.remove(d)
+
+ # calculate totals again after applying TDS
+ self.calculate_taxes_and_totals()
+
def validate_supplier(self):
prevent_po = frappe.db.get_value("Supplier", self.supplier, 'prevent_pos')
if prevent_po:
@@ -468,9 +502,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
@frappe.whitelist()
def make_rm_stock_entry(purchase_order, rm_items):
- if isinstance(rm_items, string_types):
+ rm_items_list = rm_items
+
+ if isinstance(rm_items, str):
rm_items_list = json.loads(rm_items)
- else:
+ elif not rm_items:
frappe.throw(_("No Items available for transfer"))
if rm_items_list:
@@ -508,6 +544,8 @@ def make_rm_stock_entry(purchase_order, rm_items):
'qty': rm_item_data["qty"],
'from_warehouse': rm_item_data["warehouse"],
'stock_uom': rm_item_data["stock_uom"],
+ 'serial_no': rm_item_data.get('serial_no'),
+ 'batch_no': rm_item_data.get('batch_no'),
'main_item_code': rm_item_data["item_code"],
'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item')
}
@@ -547,3 +585,58 @@ def update_status(status, name):
def make_inter_company_sales_order(source_name, target_doc=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
return make_inter_company_transaction("Purchase Order", source_name, target_doc)
+
+@frappe.whitelist()
+def get_materials_from_supplier(purchase_order, po_details):
+ if isinstance(po_details, str):
+ po_details = json.loads(po_details)
+
+ doc = frappe.get_cached_doc('Purchase Order', purchase_order)
+ doc.initialized_fields()
+ doc.purchase_orders = [doc.name]
+ doc.get_available_materials()
+
+ if not doc.available_materials:
+ frappe.throw(_('Materials are already received against the purchase order {0}')
+ .format(purchase_order))
+
+ return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details)
+
+def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details):
+ ste_doc = frappe.new_doc('Stock Entry')
+ ste_doc.purpose = 'Material Transfer'
+ ste_doc.purchase_order = po_doc.name
+ ste_doc.company = po_doc.company
+ ste_doc.is_return = 1
+
+ for key, value in available_materials.items():
+ if not value.qty:
+ continue
+
+ if value.batch_no:
+ for batch_no, qty in value.batch_no.items():
+ if qty > 0:
+ add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no)
+ else:
+ add_items_in_ste(ste_doc, value, value.qty, po_details)
+
+ ste_doc.set_stock_entry_type()
+ ste_doc.calculate_rate_and_amount()
+
+ return ste_doc
+
+def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None):
+ item = ste_doc.append('items', row.item_details)
+
+ po_detail = list(set(row.po_details).intersection(po_details))
+ item.update({
+ 'qty': qty,
+ 'batch_no': batch_no,
+ 'basic_rate': row.item_details['rate'],
+ 'po_detail': po_detail[0] if po_detail else '',
+ 's_warehouse': row.item_details['t_warehouse'],
+ 't_warehouse': row.item_details['s_warehouse'],
+ 'item_code': row.item_details['rm_item_code'],
+ 'subcontracted_item': row.item_details['main_item_code'],
+ 'serial_no': '\n'.join(row.serial_no) if row.serial_no else ''
+ })
\ No newline at end of file
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 565fc920b8e..8563b97ab74 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -20,7 +20,6 @@ from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
from erpnext.stock.doctype.batch.test_batch import make_new_batch
-from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials
class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_receipt(self):
@@ -771,7 +770,7 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
def test_exploded_items_in_subcontracted(self):
- item_code = "_Test Subcontracted FG Item 1"
+ item_code = "_Test Subcontracted FG Item 11"
make_subcontracted_item(item_code=item_code)
po = create_purchase_order(item_code=item_code, qty=1,
@@ -848,79 +847,6 @@ class TestPurchaseOrder(unittest.TestCase):
for item in rm_items:
transferred_rm_map[item.get('rm_item_code')] = item
- for item in pr.get('supplied_items'):
- self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty)
-
- update_backflush_based_on("BOM")
-
- def test_backflushed_based_on_for_multiple_batches(self):
- item_code = "_Test Subcontracted FG Item 2"
- make_item('Sub Contracted Raw Material 2', {
- 'is_stock_item': 1,
- 'is_sub_contracted_item': 1
- })
-
- make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1,
- raw_materials=["Sub Contracted Raw Material 2"])
-
- update_backflush_based_on("Material Transferred for Subcontract")
-
- order_qty = 500
- po = create_purchase_order(item_code=item_code, qty=order_qty,
- is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
-
- make_stock_entry(target="_Test Warehouse - _TC",
- item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100)
-
- rm_items = [
- {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item",
- "qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}]
-
- rm_item_string = json.dumps(rm_items)
- se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
- se.submit()
-
- for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]:
- make_new_batch(batch_id=batch, item_code=item_code)
-
- pr = make_purchase_receipt(po.name)
-
- # partial receipt
- pr.get('items')[0].qty = 30
- pr.get('items')[0].batch_no = "ABCD1"
-
- purchase_order = po.name
- purchase_order_item = po.items[0].name
-
- for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items():
- pr.append("items", {
- "item_code": pr.get('items')[0].item_code,
- "item_name": pr.get('items')[0].item_name,
- "uom": pr.get('items')[0].uom,
- "stock_uom": pr.get('items')[0].stock_uom,
- "warehouse": pr.get('items')[0].warehouse,
- "conversion_factor": pr.get('items')[0].conversion_factor,
- "cost_center": pr.get('items')[0].cost_center,
- "rate": pr.get('items')[0].rate,
- "qty": qty,
- "batch_no": batch_no,
- "purchase_order": purchase_order,
- "purchase_order_item": purchase_order_item
- })
-
- pr.submit()
-
- pr1 = make_purchase_receipt(po.name)
- pr1.get('items')[0].qty = 300
- pr1.get('items')[0].batch_no = "ABCD1"
- pr1.save()
-
- pr_key = ("Sub Contracted Raw Material 2", po.name)
- consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key)
-
- self.assertTrue(pr1.supplied_items[0].consumed_qty > 0)
- self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty))
-
update_backflush_based_on("BOM")
def test_supplied_qty_against_subcontracted_po(self):
@@ -1111,28 +1037,35 @@ def create_purchase_order(**args):
po.schedule_date = add_days(nowdate(), 1)
po.company = args.company or "_Test Company"
- po.supplier = args.customer or "_Test Supplier"
+ po.supplier = args.supplier or "_Test Supplier"
po.is_subcontracted = args.is_subcontracted or "No"
po.currency = args.currency or frappe.get_cached_value('Company', po.company, "default_currency")
po.conversion_factor = args.conversion_factor or 1
po.supplier_warehouse = args.supplier_warehouse or None
- po.append("items", {
- "item_code": args.item or args.item_code or "_Test Item",
- "warehouse": args.warehouse or "_Test Warehouse - _TC",
- "qty": args.qty or 10,
- "rate": args.rate or 500,
- "schedule_date": add_days(nowdate(), 1),
- "include_exploded_items": args.get('include_exploded_items', 1),
- "against_blanket_order": args.against_blanket_order
- })
+ if args.rm_items:
+ for row in args.rm_items:
+ po.append("items", row)
+ else:
+ po.append("items", {
+ "item_code": args.item or args.item_code or "_Test Item",
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "qty": args.qty or 10,
+ "rate": args.rate or 500,
+ "schedule_date": add_days(nowdate(), 1),
+ "include_exploded_items": args.get('include_exploded_items', 1),
+ "against_blanket_order": args.against_blanket_order
+ })
+
+ po.set_missing_values()
if not args.do_not_save:
po.insert()
if not args.do_not_submit:
if po.is_subcontracted == "Yes":
supp_items = po.get("supplied_items")
for d in supp_items:
- d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
+ if not d.reserve_warehouse:
+ d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
po.submit()
return po
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 1dbd7c60c3e..132dd1769c5 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/purchase_order_item_supplied/purchase_order_item_supplied.json b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json
index d7ea9c1ccc8..60247bd90bb 100644
--- a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json
+++ b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json
@@ -6,21 +6,25 @@
"engine": "InnoDB",
"field_order": [
"main_item_code",
- "bom_detail_no",
+ "rm_item_code",
+ "column_break_3",
"stock_uom",
+ "reserve_warehouse",
"conversion_factor",
"column_break_6",
- "rm_item_code",
+ "bom_detail_no",
"reference_name",
- "reserve_warehouse",
"section_break2",
"rate",
"col_break2",
"amount",
"section_break1",
"required_qty",
+ "supplied_qty",
"col_break1",
- "supplied_qty"
+ "consumed_qty",
+ "returned_qty",
+ "total_supplied_qty"
],
"fields": [
{
@@ -125,6 +129,8 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Supplied Qty",
+ "no_copy": 1,
+ "print_hide": 1,
"read_only": 1
},
{
@@ -142,13 +148,42 @@
{
"fieldname": "col_break2",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "consumed_qty",
+ "fieldtype": "Float",
+ "label": "Consumed Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "returned_qty",
+ "fieldtype": "Float",
+ "label": "Returned Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_supplied_qty",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "label": "Total Supplied Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
}
],
"hide_toolbar": 1,
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-06-09 15:17:58.128242",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item Supplied",
diff --git a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json
index dc00bca5cc5..f9cd72015a6 100644
--- a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json
+++ b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json
@@ -6,10 +6,11 @@
"engine": "InnoDB",
"field_order": [
"main_item_code",
- "description",
+ "rm_item_code",
+ "item_name",
"bom_detail_no",
"col_break1",
- "rm_item_code",
+ "description",
"stock_uom",
"conversion_factor",
"reference_name",
@@ -25,7 +26,8 @@
"secbreak_3",
"batch_no",
"col_break4",
- "serial_no"
+ "serial_no",
+ "purchase_order"
],
"fields": [
{
@@ -52,7 +54,6 @@
"fieldname": "description",
"fieldtype": "Text Editor",
"in_global_search": 1,
- "in_list_view": 1,
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Data",
@@ -81,18 +82,20 @@
"fieldname": "required_qty",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Required Qty",
+ "label": "Available Qty For Consumption",
"oldfieldname": "required_qty",
"oldfieldtype": "Currency",
+ "print_hide": 1,
"read_only": 1
},
{
+ "columns": 2,
"fieldname": "consumed_qty",
"fieldtype": "Float",
- "label": "Consumed Qty",
+ "in_list_view": 1,
+ "label": "Qty to Be Consumed",
"oldfieldname": "consumed_qty",
"oldfieldtype": "Currency",
- "read_only": 1,
"reqd": 1
},
{
@@ -183,12 +186,28 @@
{
"fieldname": "col_break4",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "purchase_order",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Purchase Order",
+ "no_copy": 1,
+ "options": "Purchase Order",
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-06-19 19:33:04.431213",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Receipt Item Supplied",
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index 0127eb81630..a4ce84e1cf9 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -317,19 +317,21 @@ def add_items(sq_doc, supplier, items):
create_rfq_items(sq_doc, supplier, data)
def create_rfq_items(sq_doc, supplier, data):
- sq_doc.append('items', {
- "item_code": data.item_code,
- "item_name": data.item_name,
- "description": data.description,
- "qty": data.qty,
- "rate": data.rate,
- "conversion_factor": data.conversion_factor if data.conversion_factor else None,
- "supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"),
- "warehouse": data.warehouse or '',
+ args = {}
+
+ for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor',
+ 'warehouse', 'material_request', 'material_request_item', 'stock_qty']:
+ args[field] = data.get(field)
+
+ args.update({
"request_for_quotation_item": data.name,
- "request_for_quotation": data.parent
+ "request_for_quotation": data.parent,
+ "supplier_part_no": frappe.db.get_value("Item Supplier",
+ {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no")
})
+ sq_doc.append('items', args)
+
@frappe.whitelist()
def get_pdf(doctype, name, supplier):
doc = get_rfq_doc(doctype, name, supplier)
diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js
index 4ddc458175b..1766c2c80cc 100644
--- a/erpnext/buying/doctype/supplier/supplier.js
+++ b/erpnext/buying/doctype/supplier/supplier.js
@@ -60,10 +60,23 @@ frappe.ui.form.on("Supplier", {
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 edeb135d951..fd16b23c220 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -51,6 +51,23 @@ class Supplier(TransactionBase):
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 @@ class Supplier(TransactionBase):
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 f9c8d35518d..89804662700 100644
--- a/erpnext/buying/doctype/supplier/test_supplier.py
+++ b/erpnext/buying/doctype/supplier/test_supplier.py
@@ -13,6 +13,30 @@ test_records = frappe.get_test_records('Supplier')
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 @@ def create_supplier(**args):
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/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
index 40fbe2c26e5..0a51a8e9a1c 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
@@ -576,6 +576,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment (Company Currency",
@@ -620,6 +621,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment",
@@ -802,7 +804,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-12-03 15:18:29.073368",
+ "modified": "2021-04-19 00:58:20.995491",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
diff --git a/erpnext/buying/report/subcontract_order_summary/__init__.py b/erpnext/buying/report/subcontract_order_summary/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
new file mode 100644
index 00000000000..5ba52f1b21e
--- /dev/null
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
@@ -0,0 +1,45 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Subcontract Order Summary"] = {
+ "filters": [
+ {
+ label: __("Company"),
+ fieldname: "company",
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ label: __("From Date"),
+ fieldname:"from_date",
+ fieldtype: "Date",
+ default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ reqd: 1
+ },
+ {
+ label: __("To Date"),
+ fieldname:"to_date",
+ fieldtype: "Date",
+ default: frappe.datetime.get_today(),
+ reqd: 1
+ },
+ {
+ label: __("Purchase Order"),
+ fieldname: "name",
+ fieldtype: "Link",
+ options: "Purchase Order",
+ get_query: function() {
+ return {
+ filters: {
+ docstatus: 1,
+ is_subcontracted: 'Yes',
+ company: frappe.query_report.get_filter_value('company')
+ }
+ }
+ }
+ }
+ ]
+};
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json
new file mode 100644
index 00000000000..526a8d8ad01
--- /dev/null
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json
@@ -0,0 +1,32 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-05-31 14:43:32.417694",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-05-31 14:43:32.417694",
+ "modified_by": "Administrator",
+ "module": "Buying",
+ "name": "Subcontract Order Summary",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Purchase Order",
+ "report_name": "Subcontract Order Summary",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Stock User"
+ },
+ {
+ "role": "Purchase Manager"
+ },
+ {
+ "role": "Purchase User"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py
new file mode 100644
index 00000000000..0c0d4f0531d
--- /dev/null
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py
@@ -0,0 +1,152 @@
+# 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 import _
+
+def execute(filters=None):
+ columns, data = [], []
+ columns = get_columns()
+ data = get_data(filters)
+
+ return columns, data
+
+def get_data(report_filters):
+ data = []
+ orders = get_subcontracted_orders(report_filters)
+
+ if orders:
+ supplied_items = get_supplied_items(orders, report_filters)
+ po_details = prepare_subcontracted_data(orders, supplied_items)
+ get_subcontracted_data(po_details, data)
+
+ return data
+
+def get_subcontracted_orders(report_filters):
+ fields = ['`tabPurchase Order Item`.`parent` as po_id', '`tabPurchase Order Item`.`item_code`',
+ '`tabPurchase Order Item`.`item_name`', '`tabPurchase Order Item`.`qty`', '`tabPurchase Order Item`.`name`',
+ '`tabPurchase Order Item`.`received_qty`', '`tabPurchase Order`.`status`']
+
+ filters = get_filters(report_filters)
+
+ return frappe.get_all('Purchase Order', fields = fields, filters=filters) or []
+
+def get_filters(report_filters):
+ filters = [['Purchase Order', 'docstatus', '=', 1], ['Purchase Order', 'is_subcontracted', '=', 'Yes'],
+ ['Purchase Order', 'transaction_date', 'between', (report_filters.from_date, report_filters.to_date)]]
+
+ for field in ['name', 'company']:
+ if report_filters.get(field):
+ filters.append(['Purchase Order', field, '=', report_filters.get(field)])
+
+ return filters
+
+def get_supplied_items(orders, report_filters):
+ if not orders:
+ return []
+
+ fields = ['parent', 'main_item_code', 'rm_item_code', 'required_qty',
+ 'supplied_qty', 'returned_qty', 'total_supplied_qty', 'consumed_qty', 'reference_name']
+
+ filters = {'parent': ('in', [d.po_id for d in orders]), 'docstatus': 1}
+
+ supplied_items = {}
+ for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters):
+ new_key = (row.parent, row.reference_name, row.main_item_code)
+
+ supplied_items.setdefault(new_key, []).append(row)
+
+ return supplied_items
+
+def prepare_subcontracted_data(orders, supplied_items):
+ po_details = {}
+ for row in orders:
+ key = (row.po_id, row.name, row.item_code)
+ if key not in po_details:
+ po_details.setdefault(key, frappe._dict({'po_item': row, 'supplied_items': []}))
+
+ details = po_details[key]
+
+ if supplied_items.get(key):
+ for supplied_item in supplied_items[key]:
+ details['supplied_items'].append(supplied_item)
+
+ return po_details
+
+def get_subcontracted_data(po_details, data):
+ for key, details in po_details.items():
+ res = details.po_item
+ for index, row in enumerate(details.supplied_items):
+ if index != 0:
+ res = {}
+
+ res.update(row)
+ data.append(res)
+
+def get_columns():
+ return [
+ {
+ "label": _("Purchase Order"),
+ "fieldname": "po_id",
+ "fieldtype": "Link",
+ "options": "Purchase Order",
+ "width": 100
+ },
+ {
+ "label": _("Status"),
+ "fieldname": "status",
+ "fieldtype": "Data",
+ "width": 80
+ },
+ {
+ "label": _("Subcontracted Item"),
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 160
+ },
+ {
+ "label": _("Order Qty"),
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "width": 90
+ },
+ {
+ "label": _("Received Qty"),
+ "fieldname": "received_qty",
+ "fieldtype": "Float",
+ "width": 110
+ },
+ {
+ "label": _("Supplied Item"),
+ "fieldname": "rm_item_code",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 160
+ },
+ {
+ "label": _("Required Qty"),
+ "fieldname": "required_qty",
+ "fieldtype": "Float",
+ "width": 110
+ },
+ {
+ "label": _("Supplied Qty"),
+ "fieldname": "supplied_qty",
+ "fieldtype": "Float",
+ "width": 110
+ },
+ {
+ "label": _("Consumed Qty"),
+ "fieldname": "consumed_qty",
+ "fieldtype": "Float",
+ "width": 120
+ },
+ {
+ "label": _("Returned Qty"),
+ "fieldname": "returned_qty",
+ "fieldtype": "Float",
+ "width": 110
+ }
+ ]
\ No newline at end of file
diff --git a/erpnext/change_log/v13/v13_5_0.md b/erpnext/change_log/v13/v13_5_0.md
new file mode 100644
index 00000000000..64c323a23e5
--- /dev/null
+++ b/erpnext/change_log/v13/v13_5_0.md
@@ -0,0 +1,54 @@
+# Version 13.5.0 Release Notes
+
+### Features & Enhancements
+
+- Tax deduction against advance payments ([#25831](https://github.com/frappe/erpnext/pull/25831))
+- Cost-center wise period closing entry ([#25766](https://github.com/frappe/erpnext/pull/25766))
+- Create Quality Inspections from account and stock documents ([#25221](https://github.com/frappe/erpnext/pull/25221))
+- Item Taxes based on net rate ([#25961](https://github.com/frappe/erpnext/pull/25961))
+- Enable/disable gl entry posting for change given in pos ([#25822](https://github.com/frappe/erpnext/pull/25822))
+- Add Inactive status to Employee ([#26029](https://github.com/frappe/erpnext/pull/26029))
+- Added check box to combine items with same BOM ([#25478](https://github.com/frappe/erpnext/pull/25478))
+- Item Tax Templates for Germany ([#25858](https://github.com/frappe/erpnext/pull/25858))
+- Refactored leave balance report ([#25771](https://github.com/frappe/erpnext/pull/25771))
+- Refactored Vehicle Expenses Report ([#25727](https://github.com/frappe/erpnext/pull/25727))
+- Refactored maintenance schedule and visit document ([#25358](https://github.com/frappe/erpnext/pull/25358))
+
+### Fixes
+
+- Cannot add same item with different rates ([#25849](https://github.com/frappe/erpnext/pull/25849))
+- Show only company addresses for ITC reversal entry ([#25866](https://github.com/frappe/erpnext/pull/25866))
+- Hiding Rounding Adjustment field ([#25380](https://github.com/frappe/erpnext/pull/25380))
+- Auto tax calculations in Payment Entry ([#26055](https://github.com/frappe/erpnext/pull/26055))
+- Not able to select the item code in work order ([#25915](https://github.com/frappe/erpnext/pull/25915))
+- Cannot reset plaid link for a bank account ([#25869](https://github.com/frappe/erpnext/pull/25869))
+- Student invalid password reset link ([#25826](https://github.com/frappe/erpnext/pull/25826))
+- Multiple pos issues ([#25928](https://github.com/frappe/erpnext/pull/25928))
+- Add Product Bundles to POS ([#25860](https://github.com/frappe/erpnext/pull/25860))
+- Enable Parallel tests ([#25862](https://github.com/frappe/erpnext/pull/25862))
+- Service item check on e-Invoicing ([#25986](https://github.com/frappe/erpnext/pull/25986))
+- Choose correct Salary Structure Assignment when getting data for formula eval ([#25981](https://github.com/frappe/erpnext/pull/25981))
+- Ignore internal transfer invoices from GST Reports ([#25969](https://github.com/frappe/erpnext/pull/25969))
+- Taxable value for invoices with additional discount ([#26056](https://github.com/frappe/erpnext/pull/26056))
+- Validate negative allocated amount in Payment Entry ([#25799](https://github.com/frappe/erpnext/pull/25799))
+- Allow all System Managers to delete company transactions ([#25834](https://github.com/frappe/erpnext/pull/25834))
+- Wrong round off gl entry posted in case of purchase invoice ([#25775](https://github.com/frappe/erpnext/pull/25775))
+- Use dictionary filter instead of list ([#25874](https://github.com/frappe/erpnext/pull/25874))
+- Ageing error in PSOA ([#25855](https://github.com/frappe/erpnext/pull/25855))
+- On click of duplicate button system has not copied the difference account ([#25988](https://github.com/frappe/erpnext/pull/25988))
+- Assign Product Bundle's conversion_factor to Pack… ([#25840](https://github.com/frappe/erpnext/pull/25840))
+- Rename Loan Management workspace to Loans ([#25856](https://github.com/frappe/erpnext/pull/25856))
+- Fix stock quantity calculation when negative_stock_allowe… ([#25859](https://github.com/frappe/erpnext/pull/25859))
+- Update cost center from pos profile ([#25971](https://github.com/frappe/erpnext/pull/25971))
+- Ensure website theme is applied correctly ([#25863](https://github.com/frappe/erpnext/pull/25863))
+- Only display GST card in Accounting Workspace if it's in India ([#26000](https://github.com/frappe/erpnext/pull/26000))
+- Incorrect gstin fetched incase of branch company address ([#25841](https://github.com/frappe/erpnext/pull/25841))
+- Sort account balances by account name ([#26009](https://github.com/frappe/erpnext/pull/26009))
+- Custom conversion factor field not mapped from job card to stock entry ([#25956](https://github.com/frappe/erpnext/pull/25956))
+- Chart of accounts importer always error ([#25882](https://github.com/frappe/erpnext/pull/25882))
+- Create POS Invoice for Product Bundles ([#25847](https://github.com/frappe/erpnext/pull/25847))
+- Wrap dates in getdate for leave application ([#25899](https://github.com/frappe/erpnext/pull/25899))
+- Closing entry shows incorrect expected amount ([#25868](https://github.com/frappe/erpnext/pull/25868))
+- Add Hold status column in the Issue Summary Report ([#25828](https://github.com/frappe/erpnext/pull/25828))
+- Rendering of broken image on pos ([#25872](https://github.com/frappe/erpnext/pull/25872))
+- Timeout error in the repost item valuation ([#25854](https://github.com/frappe/erpnext/pull/25854))
\ No newline at end of file
diff --git a/erpnext/change_log/v13/v13_6_0.md b/erpnext/change_log/v13/v13_6_0.md
new file mode 100644
index 00000000000..d881b279e3f
--- /dev/null
+++ b/erpnext/change_log/v13/v13_6_0.md
@@ -0,0 +1,72 @@
+# Version 13.6.0 Release Notes
+
+### Features & Enhancements
+
+- Job Card Enhancements ([#24523](https://github.com/frappe/erpnext/pull/24523))
+- Implement multi-account selection in General Ledger([#26044](https://github.com/frappe/erpnext/pull/26044))
+- Fetching of qty as per received qty from PR to PI ([#26184](https://github.com/frappe/erpnext/pull/26184))
+- Subcontract code refactor and enhancement ([#25878](https://github.com/frappe/erpnext/pull/25878))
+- Employee Grievance ([#25705](https://github.com/frappe/erpnext/pull/25705))
+- Add Inactive status to Employee ([#26030](https://github.com/frappe/erpnext/pull/26030))
+- Incorrect valuation rate report for serialized items ([#25696](https://github.com/frappe/erpnext/pull/25696))
+- Update cost updates operation time and hour rates in BOM ([#25891](https://github.com/frappe/erpnext/pull/25891))
+
+### Fixes
+
+- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046))
+- User is not able to change item tax template ([#26176](https://github.com/frappe/erpnext/pull/26176))
+- Insufficient permission for Dunning error ([#26092](https://github.com/frappe/erpnext/pull/26092))
+- Validate Product Bundle for existing transactions before deletion ([#25978](https://github.com/frappe/erpnext/pull/25978))
+- Auto unlink warehouse from item on delete ([#26073](https://github.com/frappe/erpnext/pull/26073))
+- Employee Inactive status implications ([#26245](https://github.com/frappe/erpnext/pull/26245))
+- Fetch batch items in stock reconciliation ([#26230](https://github.com/frappe/erpnext/pull/26230))
+- Disabled cancellation for sales order if linked to drafted sales invoice ([#26125](https://github.com/frappe/erpnext/pull/26125))
+- Sort website products by weightage mentioned in Item master ([#26134](https://github.com/frappe/erpnext/pull/26134))
+- Added freeze when trying to stop work order (#26192) ([#26196](https://github.com/frappe/erpnext/pull/26196))
+- Accounting Dimensions for payroll entry accrual Journal Entry ([#26083](https://github.com/frappe/erpnext/pull/26083))
+- Staffing plan vacancies data type issue ([#25941](https://github.com/frappe/erpnext/pull/25941))
+- Unable to enter score in Assessment Result details grid ([#25945](https://github.com/frappe/erpnext/pull/25945))
+- Report Subcontracted Raw Materials to be Transferred ([#26011](https://github.com/frappe/erpnext/pull/26011))
+- Label for enabling ledger posting of change amount ([#26070](https://github.com/frappe/erpnext/pull/26070))
+- Training event ([#26071](https://github.com/frappe/erpnext/pull/26071))
+- Rate not able to change in purchase order ([#26122](https://github.com/frappe/erpnext/pull/26122))
+- Error while fetching item taxes ([#26220](https://github.com/frappe/erpnext/pull/26220))
+- Check for duplicate payment terms in Payment Term Template ([#26003](https://github.com/frappe/erpnext/pull/26003))
+- Removed values out of sync validation from stock transactions ([#26229](https://github.com/frappe/erpnext/pull/26229))
+- Fetching employee in payroll entry ([#26269](https://github.com/frappe/erpnext/pull/26269))
+- Filter Cost Center and Project drop-down lists by Company ([#26045](https://github.com/frappe/erpnext/pull/26045))
+- Website item group logic for product listing in Item Group pages ([#26170](https://github.com/frappe/erpnext/pull/26170))
+- Chart not visible for First Response Time reports ([#26032](https://github.com/frappe/erpnext/pull/26032))
+- Incorrect billed qty in Sales Order analytics ([#26095](https://github.com/frappe/erpnext/pull/26095))
+- Material request and supplier quotation not linked if supplier quotation created from supplier portal ([#26023](https://github.com/frappe/erpnext/pull/26023))
+- Update leave allocation after submit ([#26191](https://github.com/frappe/erpnext/pull/26191))
+- Taxes on Internal Transfer payment entry ([#26188](https://github.com/frappe/erpnext/pull/26188))
+- Precision rate for packed items (bp #26046) ([#26217](https://github.com/frappe/erpnext/pull/26217))
+- Fixed rounding off ordered percent to 100 in condition ([#26152](https://github.com/frappe/erpnext/pull/26152))
+- Sanctioned loan amount limit check ([#26108](https://github.com/frappe/erpnext/pull/26108))
+- Purchase receipt gl entries with same item code ([#26202](https://github.com/frappe/erpnext/pull/26202))
+- Taxable value for invoices with additional discount ([#25906](https://github.com/frappe/erpnext/pull/25906))
+- Correct South Africa VAT Rate (Updated) ([#25894](https://github.com/frappe/erpnext/pull/25894))
+- Remove response_by and resolution_by if sla is removed ([#25997](https://github.com/frappe/erpnext/pull/25997))
+- POS loyalty card alignment ([#26051](https://github.com/frappe/erpnext/pull/26051))
+- Flaky test for Report Subcontracted Raw materials to be transferred ([#26043](https://github.com/frappe/erpnext/pull/26043))
+- Export invoices not visible in GSTR-1 report ([#26143](https://github.com/frappe/erpnext/pull/26143))
+- Account filter not working with accounting dimension filter ([#26211](https://github.com/frappe/erpnext/pull/26211))
+- Allow to select group warehouse while downloading materials from production plan ([#26126](https://github.com/frappe/erpnext/pull/26126))
+- Added freeze when trying to stop work order ([#26192](https://github.com/frappe/erpnext/pull/26192))
+- Time out while submit / cancel the stock transactions with more than 50 Items ([#26081](https://github.com/frappe/erpnext/pull/26081))
+- Address Card issues in e-commerce ([#26187](https://github.com/frappe/erpnext/pull/26187))
+- Error while booking deferred revenue ([#26195](https://github.com/frappe/erpnext/pull/26195))
+- Eliminate repeat creation of HSN codes ([#25947](https://github.com/frappe/erpnext/pull/25947))
+- Opening invoices can alter profit and loss of a closed year ([#25951](https://github.com/frappe/erpnext/pull/25951))
+- Payroll entry employee detail issue ([#25968](https://github.com/frappe/erpnext/pull/25968))
+- Auto tax calculations in Payment Entry ([#26037](https://github.com/frappe/erpnext/pull/26037))
+- Use pos invoice item name as unique identifier ([#26198](https://github.com/frappe/erpnext/pull/26198))
+- Billing address not fetched in Purchase Invoice ([#26100](https://github.com/frappe/erpnext/pull/26100))
+- Timeout while cancelling stock reconciliation ([#26098](https://github.com/frappe/erpnext/pull/26098))
+- Status indicator for delivery notes ([#26062](https://github.com/frappe/erpnext/pull/26062))
+- Unable to enter score in Assessment Result details grid ([#26031](https://github.com/frappe/erpnext/pull/26031))
+- Too many writes while renaming company abbreviation ([#26203](https://github.com/frappe/erpnext/pull/26203))
+- Chart not visible for First Response Time reports ([#26185](https://github.com/frappe/erpnext/pull/26185))
+- Job applicant link issue ([#25934](https://github.com/frappe/erpnext/pull/25934))
+- Fetch preferred shipping address (bp #26132) ([#26201](https://github.com/frappe/erpnext/pull/26201))
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 00000000000..589f610b939
--- /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 2fdc19e315a..cdd865ac4ac 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -116,12 +116,16 @@ class AccountsController(TransactionBase):
if self.doctype == 'Purchase Invoice':
self.calculate_paid_amount()
+ # apply tax withholding only if checked and applicable
+ self.set_tax_withholding()
if self.doctype in ['Purchase Invoice', 'Sales Invoice']:
pos_check_field = "is_pos" if self.doctype=="Sales Invoice" else "is_paid"
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:
@@ -225,7 +229,7 @@ class AccountsController(TransactionBase):
def validate_date_with_fiscal_year(self):
if self.meta.get_field("fiscal_year"):
- date_field = ""
+ date_field = None
if self.meta.get_field("posting_date"):
date_field = "posting_date"
elif self.meta.get_field("transaction_date"):
@@ -582,15 +586,18 @@ class AccountsController(TransactionBase):
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":
@@ -648,6 +655,66 @@ class AccountsController(TransactionBase):
"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:
@@ -688,7 +755,9 @@ class AccountsController(TransactionBase):
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)
@@ -700,6 +769,7 @@ class AccountsController(TransactionBase):
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
+ self.update_allocated_advance_taxes_on_cancel()
if frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'):
unlink_ref_doc_from_payment_entries(self)
@@ -707,6 +777,87 @@ class AccountsController(TransactionBase):
if frappe.db.get_single_value('Accounts Settings', 'unlink_advance_payment_on_cancelation_of_order'):
unlink_ref_doc_from_payment_entries(self)
+ def get_tax_map(self):
+ tax_map = {}
+ for tax in self.get('taxes'):
+ tax_map.setdefault(tax.account_head, 0.0)
+ tax_map[tax.account_head] += tax.tax_amount
+
+ return tax_map
+
+ def update_allocated_advance_taxes_on_cancel(self):
+ if self.get('advances'):
+ tax_accounts = [d.account_head for d in self.get('taxes')]
+ allocated_tax_map = frappe._dict(frappe.get_all('GL Entry', fields=['account', 'sum(credit - debit)'],
+ filters={'voucher_no': self.name, 'account': ('in', tax_accounts)},
+ group_by='account', as_list=1))
+
+ tax_map = self.get_tax_map()
+
+ for pe in self.get('advances'):
+ if pe.reference_type == 'Payment Entry':
+ pe = frappe.get_doc('Payment Entry', pe.reference_name)
+ for tax in pe.get('taxes'):
+ allocated_amount = tax_map.get(tax.account_head) - allocated_tax_map.get(tax.account_head)
+ if allocated_amount > tax.tax_amount:
+ allocated_amount = tax.tax_amount
+
+ if allocated_amount:
+ frappe.db.set_value('Advance Taxes and Charges', tax.name, 'allocated_amount',
+ tax.allocated_amount - allocated_amount)
+ tax_map[tax.account_head] -= allocated_amount
+ allocated_tax_map[tax.account_head] -= allocated_amount
+
+ def allocate_advance_taxes(self, gl_entries):
+ tax_map = self.get_tax_map()
+ for pe in self.get("advances"):
+ if pe.reference_type == "Payment Entry" and \
+ frappe.db.get_value('Payment Entry', pe.reference_name, 'advance_tax_account'):
+ pe = frappe.get_doc("Payment Entry", pe.reference_name)
+ for tax in pe.get("taxes"):
+ account_currency = get_account_currency(tax.account_head)
+
+ if self.doctype == "Purchase Invoice":
+ 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
+ if tax_map.get(tax.account_head):
+ amount = tax_map.get(tax.account_head)
+ if amount < unallocated_amount:
+ unallocated_amount = amount
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": tax.account_head,
+ "against": party,
+ dr_or_cr: unallocated_amount,
+ dr_or_cr + "_in_account_currency": unallocated_amount
+ if account_currency==self.company_currency
+ else unallocated_amount,
+ "cost_center": tax.cost_center
+ }, account_currency, item=tax))
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": pe.advance_tax_account,
+ "against": party,
+ rev_dr_cr: unallocated_amount,
+ rev_dr_cr + "_in_account_currency": unallocated_amount
+ if account_currency==self.company_currency
+ else unallocated_amount,
+ "cost_center": tax.cost_center
+ }, account_currency, item=tax))
+
+ frappe.db.set_value("Advance Taxes and Charges", tax.name, "allocated_amount",
+ tax.allocated_amount + unallocated_amount)
+
+ tax_map[tax.account_head] -= unallocated_amount
+
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield):
from erpnext.controllers.status_updater import get_allowance_for
item_allowance = {}
@@ -744,8 +895,14 @@ class AccountsController(TransactionBase):
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
- frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
- .format(item.item_code, item.idx, max_allowed_amt))
+ if self.doctype != "Purchase Invoice":
+ self.throw_overbill_exception(item, max_allowed_amt)
+ elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
+ self.throw_overbill_exception(item, max_allowed_amt)
+
+ def throw_overbill_exception(self, item, max_allowed_amt):
+ frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
+ .format(item.item_code, item.idx, max_allowed_amt))
def get_company_default(self, fieldname):
from erpnext.accounts.utils import get_company_default
@@ -955,8 +1112,11 @@ class AccountsController(TransactionBase):
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]
@@ -1108,7 +1268,7 @@ def validate_conversion_rate(currency, conversion_rate, conversion_rate_label, c
def validate_taxes_and_charges(tax):
- if tax.charge_type in ['Actual', 'On Net Total'] and tax.row_id:
+ if tax.charge_type in ['Actual', 'On Net Total', 'On Paid Amount'] and tax.row_id:
frappe.throw(_("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'"))
elif tax.charge_type in ['On Previous Row Amount', 'On Previous Row Total']:
if cint(tax.idx) == 1:
@@ -1125,20 +1285,19 @@ def validate_taxes_and_charges(tax):
def validate_inclusive_tax(tax, doc):
def _on_previous_row_error(row_range):
- throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx,
- row_range))
+ throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range))
if cint(getattr(tax, "included_in_print_rate", None)):
if tax.charge_type == "Actual":
# inclusive tax cannot be of type Actual
- throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate").format(tax.idx))
+ throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx))
elif tax.charge_type == "On Previous Row Amount" and \
not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_print_rate):
# referred row should also be inclusive
_on_previous_row_error(tax.row_id)
elif tax.charge_type == "On Previous Row Total" and \
not all([cint(t.included_in_print_rate) for t in doc.get("taxes")[:cint(tax.row_id) - 1]]):
- # all rows about the reffered tax should be inclusive
+ # all rows about the referred tax should be inclusive
_on_previous_row_error("1 - %d" % (tax.row_id,))
elif tax.get("category") == "Valuation":
frappe.throw(_("Valuation type charges can not be marked as Inclusive"))
@@ -1200,6 +1359,8 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
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 ""
@@ -1216,31 +1377,31 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
"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)
-
def update_invoice_status():
# Daily update the status of the invoices
@@ -1449,6 +1610,7 @@ def validate_and_delete_children(parent, data):
for d in deleted_children:
update_bin_on_delete(d, parent.doctype)
+
@frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
def check_doc_permissions(doc, perm_type='create'):
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index da819119b10..6a550e0e975 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -11,16 +11,17 @@ from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
from erpnext.stock.stock_ledger import get_valuation_rate
-from erpnext.stock.doctype.stock_entry.stock_entry import get_used_alternative_items
from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, auto_make_serial_nos, get_serial_nos
from frappe.contacts.doctype.address.address import get_address_display
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
-from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.stock.utils import get_incoming_rate
-class BuyingController(StockController):
+from erpnext.controllers.stock_controller import StockController
+from erpnext.controllers.subcontracting import Subcontracting
+
+class BuyingController(StockController, Subcontracting):
def get_feed(self):
if self.get("supplier_name"):
@@ -57,6 +58,11 @@ class BuyingController(StockController):
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
self.update_valuation_rate()
+ def onload(self):
+ super(BuyingController, self).onload()
+ self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings',
+ 'backflush_raw_materials_of_subcontract_based_on'))
+
def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate)
@@ -171,12 +177,13 @@ class BuyingController(StockController):
TODO: rename item_tax_amount to valuation_tax_amount
"""
+ stock_and_asset_items = []
stock_and_asset_items = self.get_stock_items() + self.get_asset_items()
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
last_item_idx = 1
for d in self.get("items"):
- if d.item_code and d.item_code in stock_and_asset_items:
+ if (d.item_code and d.item_code in stock_and_asset_items):
stock_and_asset_items_qty += flt(d.qty)
stock_and_asset_items_amount += flt(d.base_net_amount)
last_item_idx = d.idx
@@ -255,7 +262,7 @@ class BuyingController(StockController):
supplied_items_cost = 0.0
for d in self.get("supplied_items"):
if d.reference_name == item_row_id:
- if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'):
+ if reset_outgoing_rate and frappe.get_cached_value('Item', d.rm_item_code, 'is_stock_item'):
rate = get_incoming_rate({
"item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse,
@@ -285,11 +292,13 @@ class BuyingController(StockController):
if item in self.sub_contracted_items and not item.bom:
frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code))
- if self.doctype == "Purchase Order":
- for supplied_item in self.get("supplied_items"):
- if not supplied_item.reserve_warehouse:
- frappe.throw(_("Reserved Warehouse is mandatory for Item {0} in Raw Materials supplied").format(frappe.bold(supplied_item.rm_item_code)))
+ if self.doctype != "Purchase Order":
+ return
+ for row in self.get("supplied_items"):
+ if not row.reserve_warehouse:
+ msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied"
+ frappe.throw(_(msg))
else:
for item in self.get("items"):
if item.bom:
@@ -297,23 +306,7 @@ class BuyingController(StockController):
def create_raw_materials_supplied(self, raw_material_table):
if self.is_subcontracted=="Yes":
- parent_items = []
- backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings",
- "backflush_raw_materials_of_subcontract_based_on")
- if (self.doctype == 'Purchase Receipt' and
- backflush_raw_materials_based_on != 'BOM'):
- self.update_raw_materials_supplied_based_on_stock_entries()
- else:
- for item in self.get("items"):
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
- item.rm_supp_cost = 0.0
- if item.bom and item.item_code in self.sub_contracted_items:
- self.update_raw_materials_supplied_based_on_bom(item, raw_material_table)
-
- if [item.item_code, item.name] not in parent_items:
- parent_items.append([item.item_code, item.name])
-
- self.cleanup_raw_materials_supplied(parent_items, raw_material_table)
+ self.set_materials_for_subcontracted_items(raw_material_table)
elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
for item in self.get("items"):
@@ -322,176 +315,6 @@ class BuyingController(StockController):
if self.is_subcontracted == "No" and self.get("supplied_items"):
self.set('supplied_items', [])
- def update_raw_materials_supplied_based_on_stock_entries(self):
- self.set('supplied_items', [])
-
- purchase_orders = set(d.purchase_order for d in self.items)
-
- # qty of raw materials backflushed (for each item per purchase order)
- backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders)
-
- # qty of "finished good" item yet to be received
- qty_to_be_received_map = get_qty_to_be_received(purchase_orders)
-
- for item in self.get('items'):
- if not item.purchase_order:
- continue
-
- # reset raw_material cost
- item.rm_supp_cost = 0
-
- # qty of raw materials transferred to the supplier
- transferred_raw_materials = get_subcontracted_raw_materials_from_se(item.purchase_order, item.item_code)
-
- non_stock_items = get_non_stock_items(item.purchase_order, item.item_code)
-
- item_key = '{}{}'.format(item.item_code, item.purchase_order)
-
- fg_yet_to_be_received = qty_to_be_received_map.get(item_key)
-
- if not fg_yet_to_be_received:
- frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}")
- .format(item.idx, frappe.bold(item.item_code),
- frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)),
- title=_("Limit Crossed"))
-
- transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
- # backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
-
- for raw_material in transferred_raw_materials + non_stock_items:
- rm_item_key = (raw_material.rm_item_code, item.item_code, item.purchase_order)
- raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
-
- consumed_qty = raw_material_data.get('qty', 0)
- consumed_serial_nos = raw_material_data.get('serial_no', '')
- consumed_batch_nos = raw_material_data.get('batch_nos', '')
-
- transferred_qty = raw_material.qty
-
- rm_qty_to_be_consumed = transferred_qty - consumed_qty
-
- # backflush all remaining transferred qty in the last Purchase Receipt
- if fg_yet_to_be_received == item.qty:
- qty = rm_qty_to_be_consumed
- else:
- qty = (rm_qty_to_be_consumed / fg_yet_to_be_received) * item.qty
-
- if frappe.get_cached_value('UOM', raw_material.stock_uom, 'must_be_whole_number'):
- qty = frappe.utils.ceil(qty)
-
- if qty > rm_qty_to_be_consumed:
- qty = rm_qty_to_be_consumed
-
- if not qty: continue
-
- if raw_material.serial_nos:
- set_serial_nos(raw_material, consumed_serial_nos, qty)
-
- if raw_material.batch_nos:
- backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {})
-
- batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
- qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
-
- for batch_data in batches_qty:
- qty = batch_data['qty']
- raw_material.batch_no = batch_data['batch']
- if qty > 0:
- self.append_raw_material_to_be_backflushed(item, raw_material, qty)
- else:
- self.append_raw_material_to_be_backflushed(item, raw_material, qty)
-
- def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty):
- rm = self.append('supplied_items', {})
- rm.update(raw_material_data)
-
- if not rm.main_item_code:
- rm.main_item_code = fg_item_row.item_code
-
- rm.reference_name = fg_item_row.name
- rm.required_qty = qty
- rm.consumed_qty = qty
-
- def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
- exploded_item = 1
- if hasattr(item, 'include_exploded_items'):
- exploded_item = item.get('include_exploded_items')
-
- bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
-
- used_alternative_items = []
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order:
- used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
-
- raw_materials_cost = 0
- items = list(set([d.item_code for d in bom_items]))
- item_wh = frappe._dict(frappe.db.sql("""select i.item_code, id.default_warehouse
- from `tabItem` i, `tabItem Default` id
- where id.parent=i.name and id.company=%s and i.name in ({0})"""
- .format(", ".join(["%s"] * len(items))), [self.company] + items))
-
- for bom_item in bom_items:
- if self.doctype == "Purchase Order":
- reserve_warehouse = bom_item.source_warehouse or item_wh.get(bom_item.item_code)
- if frappe.db.get_value("Warehouse", reserve_warehouse, "company") != self.company:
- reserve_warehouse = None
-
- conversion_factor = item.conversion_factor
- if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and
- bom_item.item_code in used_alternative_items):
- alternative_item_data = used_alternative_items.get(bom_item.item_code)
- bom_item.item_code = alternative_item_data.item_code
- bom_item.item_name = alternative_item_data.item_name
- bom_item.stock_uom = alternative_item_data.stock_uom
- conversion_factor = alternative_item_data.conversion_factor
- bom_item.description = alternative_item_data.description
-
- # check if exists
- exists = 0
- for d in self.get(raw_material_table):
- if d.main_item_code == item.item_code and d.rm_item_code == bom_item.item_code \
- and d.reference_name == item.name:
- rm, exists = d, 1
- break
-
- if not exists:
- rm = self.append(raw_material_table, {})
-
- required_qty = flt(flt(bom_item.qty_consumed_per_unit) * (flt(item.qty) + getattr(item, 'rejected_qty', 0)) *
- flt(conversion_factor), rm.precision("required_qty"))
- rm.reference_name = item.name
- rm.bom_detail_no = bom_item.name
- rm.main_item_code = item.item_code
- rm.rm_item_code = bom_item.item_code
- rm.stock_uom = bom_item.stock_uom
- rm.required_qty = required_qty
- rm.rate = bom_item.rate
- rm.conversion_factor = conversion_factor
-
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
- rm.consumed_qty = required_qty
- rm.description = bom_item.description
- if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
- rm.batch_no = item.batch_no
- elif not rm.reserve_warehouse:
- rm.reserve_warehouse = reserve_warehouse
-
- def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
- """Remove all those child items which are no longer present in main item table"""
- delete_list = []
- for d in self.get(raw_material_table):
- if [d.main_item_code, d.reference_name] not in parent_items:
- # mark for deletion from doclist
- delete_list.append(d)
-
- # delete from doclist
- if delete_list:
- rm_supplied_details = self.get(raw_material_table)
- self.set(raw_material_table, [])
- for d in rm_supplied_details:
- if d not in delete_list:
- self.append(raw_material_table, d)
-
@property
def sub_contracted_items(self):
if not hasattr(self, "_sub_contracted_items"):
@@ -683,7 +506,8 @@ class BuyingController(StockController):
self.process_fixed_asset()
self.update_fixed_asset(field)
- update_last_purchase_rate(self, is_submit = 1)
+ if self.doctype in ['Purchase Order', 'Purchase Receipt']:
+ update_last_purchase_rate(self, is_submit = 1)
def on_cancel(self):
super(BuyingController, self).on_cancel()
@@ -691,7 +515,9 @@ class BuyingController(StockController):
if self.get('is_return'):
return
- update_last_purchase_rate(self, is_submit = 0)
+ if self.doctype in ['Purchase Order', 'Purchase Receipt']:
+ update_last_purchase_rate(self, is_submit = 0)
+
if self.doctype in ['Purchase Receipt', 'Purchase Invoice']:
field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt'
@@ -863,104 +689,6 @@ class BuyingController(StockController):
else:
validate_item_type(self, "is_purchase_item", "purchase")
-
-def get_items_from_bom(item_code, bom, exploded_item=1):
- doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
-
- bom_items = frappe.db.sql("""select t2.item_code, t2.name,
- t2.rate, t2.stock_uom, t2.source_warehouse, t2.description,
- t2.stock_qty / ifnull(t1.quantity, 1) as qty_consumed_per_unit
- from
- `tabBOM` t1, `tab{0}` t2, tabItem t3
- where
- t2.parent = t1.name and t1.item = %s
- and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s
- and t2.sourced_by_supplier = 0
- and t2.item_code = t3.name""".format(doctype),
- (item_code, bom), as_dict=1)
-
- if not bom_items:
- msgprint(_("Specified BOM {0} does not exist for Item {1}").format(bom, item_code), raise_exception=1)
-
- return bom_items
-
-def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
- common_query = """
- SELECT
- sed.item_code AS rm_item_code,
- SUM(sed.qty) AS qty,
- sed.description,
- sed.stock_uom,
- sed.subcontracted_item AS main_item_code,
- {serial_no_concat_syntax} AS serial_nos,
- {batch_no_concat_syntax} AS batch_nos
- FROM `tabStock Entry` se,`tabStock Entry Detail` sed
- WHERE
- se.name = sed.parent
- AND se.docstatus=1
- AND se.purpose='Send to Subcontractor'
- AND se.purchase_order = %s
- AND IFNULL(sed.t_warehouse, '') != ''
- AND IFNULL(sed.subcontracted_item, '') in ('', %s)
- GROUP BY sed.item_code, sed.subcontracted_item
- """
- raw_materials = frappe.db.multisql({
- 'mariadb': common_query.format(
- serial_no_concat_syntax="GROUP_CONCAT(sed.serial_no)",
- batch_no_concat_syntax="GROUP_CONCAT(sed.batch_no)"
- ),
- 'postgres': common_query.format(
- serial_no_concat_syntax="STRING_AGG(sed.serial_no, ',')",
- batch_no_concat_syntax="STRING_AGG(sed.batch_no, ',')"
- )
- }, (purchase_order, fg_item), as_dict=1)
-
- return raw_materials
-
-def get_backflushed_subcontracted_raw_materials(purchase_orders):
- purchase_receipts = frappe.get_all("Purchase Receipt Item",
- fields = ["purchase_order", "item_code", "name", "parent"],
- filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))})
-
- distinct_purchase_receipts = {}
- for pr in purchase_receipts:
- key = (pr.purchase_order, pr.item_code, pr.parent)
- distinct_purchase_receipts.setdefault(key, []).append(pr.name)
-
- backflushed_raw_materials_map = frappe._dict()
- for args, references in iteritems(distinct_purchase_receipts):
- purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references)
-
- for data in purchase_receipt_supplied_items:
- pr_key = (data.rm_item_code, data.main_item_code, args[0])
- if pr_key not in backflushed_raw_materials_map:
- backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({
- "qty": 0.0,
- "serial_no": [],
- "batch_no": [],
- "consumed_batch": {}
- }))
-
- row = backflushed_raw_materials_map.get(pr_key)
- row.qty += data.consumed_qty
-
- for field in ["serial_no", "batch_no"]:
- if data.get(field):
- row[field].append(data.get(field))
-
- if data.get("batch_no"):
- if data.get("batch_no") in row.consumed_batch:
- row.consumed_batch[data.get("batch_no")] += data.consumed_qty
- else:
- row.consumed_batch[data.get("batch_no")] = data.consumed_qty
-
- return backflushed_raw_materials_map
-
-def get_supplied_items(item_code, purchase_receipt, references):
- return frappe.get_all("Purchase Receipt Item Supplied",
- fields=["rm_item_code", "main_item_code", "consumed_qty", "serial_no", "batch_no"],
- filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)})
-
def get_asset_item_details(asset_items):
asset_items_data = {}
for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"],
@@ -992,135 +720,3 @@ def validate_item_type(doc, fieldname, message):
error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message)
frappe.throw(error_message)
-
-def get_qty_to_be_received(purchase_orders):
- return frappe._dict(frappe.db.sql("""
- SELECT CONCAT(poi.`item_code`, poi.`parent`) AS item_key,
- SUM(poi.`qty`) - SUM(poi.`received_qty`) AS qty_to_be_received
- FROM `tabPurchase Order Item` poi
- WHERE
- poi.`parent` in %s
- GROUP BY poi.`item_code`, poi.`parent`
- HAVING SUM(poi.`qty`) > SUM(poi.`received_qty`)
- """, (purchase_orders)))
-
-def get_non_stock_items(purchase_order, fg_item_code):
- return frappe.db.sql("""
- SELECT
- pois.main_item_code,
- pois.rm_item_code,
- item.description,
- pois.required_qty AS qty,
- pois.rate,
- 1 as non_stock_item,
- pois.stock_uom
- FROM `tabPurchase Order Item Supplied` pois, `tabItem` item
- WHERE
- pois.`rm_item_code` = item.`name`
- AND item.is_stock_item = 0
- AND pois.`parent` = %s
- AND pois.`main_item_code` = %s
- """, (purchase_order, fg_item_code), as_dict=1)
-
-
-def set_serial_nos(raw_material, consumed_serial_nos, qty):
- serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \
- set(get_serial_nos(consumed_serial_nos))
- if serial_nos and qty <= len(serial_nos):
- raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)])
-
-def get_transferred_batch_qty_map(purchase_order, fg_item):
- # returns
- # {
- # (item_code, fg_code): {
- # batch1: 10, # qty
- # batch2: 16
- # },
- # }
- transferred_batch_qty_map = {}
- transferred_batches = frappe.db.sql("""
- SELECT
- sed.batch_no,
- SUM(sed.qty) AS qty,
- sed.item_code,
- sed.subcontracted_item
- FROM `tabStock Entry` se,`tabStock Entry Detail` sed
- WHERE
- se.name = sed.parent
- AND se.docstatus=1
- AND se.purpose='Send to Subcontractor'
- AND se.purchase_order = %s
- AND ifnull(sed.subcontracted_item, '') in ('', %s)
- AND sed.batch_no IS NOT NULL
- GROUP BY
- sed.batch_no,
- sed.item_code
- """, (purchase_order, fg_item), as_dict=1)
-
- for batch_data in transferred_batches:
- key = ((batch_data.item_code, fg_item)
- if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
- transferred_batch_qty_map.setdefault(key, OrderedDict())
- transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
-
- return transferred_batch_qty_map
-
-def get_backflushed_batch_qty_map(purchase_order, fg_item):
- # returns
- # {
- # (item_code, fg_code): {
- # batch1: 10, # qty
- # batch2: 16
- # },
- # }
- backflushed_batch_qty_map = {}
- backflushed_batches = frappe.db.sql("""
- SELECT
- pris.batch_no,
- SUM(pris.consumed_qty) AS qty,
- pris.rm_item_code AS item_code
- FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` pris
- WHERE
- pr.name = pri.parent
- AND pri.parent = pris.parent
- AND pri.purchase_order = %s
- AND pri.item_code = pris.main_item_code
- AND pr.docstatus = 1
- AND pris.main_item_code = %s
- AND pris.batch_no IS NOT NULL
- GROUP BY
- pris.rm_item_code, pris.batch_no
- """, (purchase_order, fg_item), as_dict=1)
-
- for batch_data in backflushed_batches:
- backflushed_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
- backflushed_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
-
- return backflushed_batch_qty_map
-
-def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po):
- # Returns available batches to be backflushed based on requirements
- transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {})
- if not transferred_batches:
- transferred_batches = transferred_batch_qty_map.get((item_code, po), {})
-
- available_batches = []
-
- for (batch, transferred_qty) in transferred_batches.items():
- backflushed_qty = backflushed_batches.get(batch, 0)
- available_qty = transferred_qty - backflushed_qty
-
- if available_qty >= required_qty:
- available_batches.append({'batch': batch, 'qty': required_qty})
- break
- elif available_qty != 0:
- available_batches.append({'batch': batch, 'qty': available_qty})
- required_qty -= available_qty
-
- for row in available_batches:
- if backflushed_batches.get(row.get('batch'), 0) > 0:
- backflushed_batches[row.get('batch')] += row.get('qty')
- else:
- backflushed_batches[row.get('batch')] = row.get('qty')
-
- return available_batches
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 7bd739a6ad3..280319321f2 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -19,7 +19,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
fields = get_fields("Employee", ["name", "employee_name"])
return frappe.db.sql("""select {fields} from `tabEmployee`
- where status = 'Active'
+ where status in ('Active', 'Suspended')
and docstatus < 2
and ({key} like %(txt)s
or employee_name like %(txt)s)
@@ -315,7 +315,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql("""select {fields} from `tabProject`
where
`tabProject`.status not in ("Completed", "Cancelled")
- and {cond} {match_cond} {scond}
+ and {cond} {scond} {match_cond}
order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
idx desc,
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 5f759b43bc6..80ccc6d75b2 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -99,9 +99,10 @@ def validate_returned_items(doc):
frappe.throw(_("Row # {0}: Serial No {1} does not match with {2} {3}")
.format(d.idx, s, doc.doctype, doc.return_against))
- if warehouse_mandatory and frappe.db.get_value("Item", d.item_code, "is_stock_item") \
- and not d.get("warehouse"):
- frappe.throw(_("Warehouse is mandatory"))
+ if (warehouse_mandatory and not d.get("warehouse") and
+ frappe.db.get_value("Item", d.item_code, "is_stock_item")
+ ):
+ frappe.throw(_("Warehouse is mandatory"))
items_returned = True
@@ -462,4 +463,4 @@ def get_returned_serial_nos(child_doc, parent_doc):
for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters):
serial_nos.extend(get_serial_nos(row.serial_no))
- return serial_nos
\ No newline at end of file
+ return serial_nos
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 7f28289760c..da2765deded 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -330,9 +330,15 @@ class SellingController(StockController):
# For internal transfers use incoming rate as the valuation rate
if self.is_internal_transfer():
- rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
- if d.rate != rate:
- d.rate = rate
+ if d.doctype == "Packed Item":
+ incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision('incoming_rate'))
+ if d.incoming_rate != incoming_rate:
+ d.incoming_rate = incoming_rate
+ else:
+ rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
+ if d.rate != rate:
+ d.rate = rate
+
d.discount_percentage = 0
d.discount_amount = 0
frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer")
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 71bde7fad73..17bd7354f93 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -1,17 +1,21 @@
# 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, erpnext
-from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate
-from frappe import _
-import frappe.defaults
+import json
from collections import defaultdict
-from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced
+
+import frappe
+import frappe.defaults
+from frappe import _
+from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
+
+import erpnext
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
+from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
-from erpnext.stock.stock_ledger import get_valuation_rate
from erpnext.stock import get_warehouse_account_map
+from erpnext.stock.stock_ledger import get_valuation_rate
+
class QualityInspectionRequiredError(frappe.ValidationError): pass
class QualityInspectionRejectedError(frappe.ValidationError): pass
@@ -49,12 +53,17 @@ class StockController(AccountsController):
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")
@@ -189,7 +198,6 @@ class StockController(AccountsController):
if hasattr(self, "items"):
item_doclist = self.get("items")
elif self.doctype == "Stock Reconciliation":
- import json
item_doclist = []
data = json.loads(self.reconciliation_json)
for row in data[data.index(self.head_row)+1:]:
@@ -319,7 +327,7 @@ class StockController(AccountsController):
return serialized_items
def validate_warehouse(self):
- from erpnext.stock.utils import validate_warehouse_company, validate_disabled_warehouse
+ from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
warehouses = list(set(d.warehouse for d in
self.get("items") if getattr(d, "warehouse", None)))
@@ -353,42 +361,68 @@ class StockController(AccountsController):
}, 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]))
@@ -494,24 +528,107 @@ class StockController(AccountsController):
})
if future_sle_exists(args):
create_repost_item_valuation_entry(args)
- elif not is_reposting_pending():
- check_if_stock_and_account_balance_synced(self.posting_date,
- self.company, self.doctype, self.name)
+
+@frappe.whitelist()
+def make_quality_inspections(doctype, docname, items):
+ if isinstance(items, str):
+ items = json.loads(items)
+
+ inspections = []
+ for item in items:
+ if flt(item.get("sample_size")) > flt(item.get("qty")):
+ frappe.throw(_("{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity})").format(
+ item_name=item.get("item_name"),
+ sample_size=item.get("sample_size"),
+ accepted_quantity=item.get("qty")
+ ))
+
+ quality_inspection = frappe.get_doc({
+ "doctype": "Quality Inspection",
+ "inspection_type": "Incoming",
+ "inspected_by": frappe.session.user,
+ "reference_type": doctype,
+ "reference_name": docname,
+ "item_code": item.get("item_code"),
+ "description": item.get("description"),
+ "sample_size": flt(item.get("sample_size")),
+ "item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None,
+ "batch_no": item.get("batch_no")
+ }).insert()
+ quality_inspection.save()
+ inspections.append(quality_inspection.name)
+
+ return inspections
def is_reposting_pending():
return frappe.db.exists("Repost Item Valuation",
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
+def future_sle_exists(args, sl_entries=None):
+ key = (args.voucher_type, args.voucher_no)
-def future_sle_exists(args):
- sl_entries = frappe.get_all("Stock Ledger Entry",
+ if validate_future_sle_not_exists(args, key, sl_entries):
+ return False
+ elif get_cached_data(args, key):
+ return True
+
+ if not sl_entries:
+ sl_entries = get_sle_entries_against_voucher(args)
+ if not sl_entries:
+ return
+
+ or_conditions = get_conditions_to_validate_future_sle(sl_entries)
+
+ data = frappe.db.sql("""
+ select item_code, warehouse, count(name) as total_row
+ from `tabStock Ledger Entry`
+ where
+ ({})
+ and timestamp(posting_date, posting_time)
+ >= timestamp(%(posting_date)s, %(posting_time)s)
+ and voucher_no != %(voucher_no)s
+ and is_cancelled = 0
+ GROUP BY
+ item_code, warehouse
+ """.format(" or ".join(or_conditions)), args, as_dict=1)
+
+ for d in data:
+ frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row
+
+ return len(data)
+
+def validate_future_sle_not_exists(args, key, sl_entries=None):
+ item_key = ''
+ if args.get('item_code'):
+ item_key = (args.get('item_code'), args.get('warehouse'))
+
+ if not sl_entries and hasattr(frappe.local, 'future_sle'):
+ if (not frappe.local.future_sle.get(key) or
+ (item_key and item_key not in frappe.local.future_sle.get(key))):
+ return True
+
+def get_cached_data(args, key):
+ if not hasattr(frappe.local, 'future_sle'):
+ frappe.local.future_sle = {}
+
+ if key not in frappe.local.future_sle:
+ frappe.local.future_sle[key] = frappe._dict({})
+
+ if args.get('item_code'):
+ item_key = (args.get('item_code'), args.get('warehouse'))
+ count = frappe.local.future_sle[key].get(item_key)
+
+ return True if (count or count == 0) else False
+ else:
+ return frappe.local.future_sle[key]
+
+def get_sle_entries_against_voucher(args):
+ return frappe.get_all("Stock Ledger Entry",
filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no},
fields=["item_code", "warehouse"],
order_by="creation asc")
- if not sl_entries:
- return
-
+def get_conditions_to_validate_future_sle(sl_entries):
warehouse_items_map = {}
for entry in sl_entries:
if entry.warehouse not in warehouse_items_map:
@@ -522,23 +639,10 @@ def future_sle_exists(args):
or_conditions = []
for warehouse, items in warehouse_items_map.items():
or_conditions.append(
- "warehouse = '{}' and item_code in ({})".format(
- warehouse,
- ", ".join(frappe.db.escape(item) for item in items)
- )
- )
+ f"""warehouse = {frappe.db.escape(warehouse)}
+ and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""")
- return frappe.db.sql("""
- select name
- from `tabStock Ledger Entry`
- where
- ({})
- and timestamp(posting_date, posting_time)
- >= timestamp(%(posting_date)s, %(posting_time)s)
- and voucher_no != %(voucher_no)s
- and is_cancelled = 0
- limit 1
- """.format(" or ".join(or_conditions)), args)
+ return or_conditions
def create_repost_item_valuation_entry(args):
args = frappe._dict(args)
diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py
new file mode 100644
index 00000000000..36ae1102164
--- /dev/null
+++ b/erpnext/controllers/subcontracting.py
@@ -0,0 +1,393 @@
+import frappe
+import copy
+from frappe import _
+from frappe.utils import flt, cint, get_link_to_form
+from collections import defaultdict
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+class Subcontracting():
+ def set_materials_for_subcontracted_items(self, raw_material_table):
+ if self.doctype == 'Purchase Invoice' and not self.update_stock:
+ return
+
+ self.raw_material_table = raw_material_table
+ self.__identify_change_in_item_table()
+ self.__prepare_supplied_items()
+ self.__validate_supplied_items()
+
+ def __prepare_supplied_items(self):
+ self.initialized_fields()
+ self.__get_purchase_orders()
+ self.__get_pending_qty_to_receive()
+ self.get_available_materials()
+ self.__remove_changed_rows()
+ self.__set_supplied_items()
+
+ def initialized_fields(self):
+ self.available_materials = frappe._dict()
+ self.__transferred_items = frappe._dict()
+ self.alternative_item_details = frappe._dict()
+ self.__get_backflush_based_on()
+
+ def __get_backflush_based_on(self):
+ self.backflush_based_on = frappe.db.get_single_value("Buying Settings",
+ "backflush_raw_materials_of_subcontract_based_on")
+
+ def __get_purchase_orders(self):
+ self.purchase_orders = []
+
+ if self.doctype == 'Purchase Order':
+ return
+
+ self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order]
+
+ def __identify_change_in_item_table(self):
+ self.__changed_name = []
+ self.__reference_name = []
+
+ if self.doctype == 'Purchase Order' or self.is_new():
+ self.set(self.raw_material_table, [])
+ return
+
+ item_dict = self.__get_data_before_save()
+ if not item_dict:
+ return True
+
+ for n_row in self.items:
+ self.__reference_name.append(n_row.name)
+ if (n_row.name not in item_dict) or (n_row.item_code, n_row.qty) != item_dict[n_row.name]:
+ self.__changed_name.append(n_row.name)
+
+ if item_dict.get(n_row.name):
+ del item_dict[n_row.name]
+
+ self.__changed_name.extend(item_dict.keys())
+
+ def __get_data_before_save(self):
+ item_dict = {}
+ if self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self._doc_before_save:
+ for row in self._doc_before_save.get('items'):
+ item_dict[row.name] = (row.item_code, row.qty)
+
+ return item_dict
+
+ def get_available_materials(self):
+ ''' Get the available raw materials which has been transferred to the supplier.
+ available_materials = {
+ (item_code, subcontracted_item, purchase_order): {
+ 'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details
+ }
+ }
+ '''
+ if not self.purchase_orders:
+ return
+
+ for row in self.__get_transferred_items():
+ key = (row.rm_item_code, row.main_item_code, row.purchase_order)
+
+ if key not in self.available_materials:
+ self.available_materials.setdefault(key, frappe._dict({'qty': 0, 'serial_no': [],
+ 'batch_no': defaultdict(float), 'item_details': row, 'po_details': []})
+ )
+
+ details = self.available_materials[key]
+ details.qty += row.qty
+ details.po_details.append(row.po_detail)
+
+ if row.serial_no:
+ details.serial_no.extend(get_serial_nos(row.serial_no))
+
+ if row.batch_no:
+ details.batch_no[row.batch_no] += row.qty
+
+ self.__set_alternative_item_details(row)
+
+ self.__transferred_items = copy.deepcopy(self.available_materials)
+ for doctype in ['Purchase Receipt', 'Purchase Invoice']:
+ self.__update_consumed_materials(doctype)
+
+ def __update_consumed_materials(self, doctype, return_consumed_items=False):
+ '''Deduct the consumed materials from the available materials.'''
+
+ pr_items = self.__get_received_items(doctype)
+ if not pr_items:
+ return ([], {}) if return_consumed_items else None
+
+ pr_items = {d.name: d.get(self.get('po_field') or 'purchase_order') for d in pr_items}
+ consumed_materials = self.__get_consumed_items(doctype, pr_items.keys())
+
+ if return_consumed_items:
+ return (consumed_materials, pr_items)
+
+ for row in consumed_materials:
+ key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
+ if not self.available_materials.get(key):
+ continue
+
+ self.available_materials[key]['qty'] -= row.consumed_qty
+ if row.serial_no:
+ self.available_materials[key]['serial_no'] = list(
+ set(self.available_materials[key]['serial_no']) - set(get_serial_nos(row.serial_no))
+ )
+
+ if row.batch_no:
+ self.available_materials[key]['batch_no'][row.batch_no] -= row.consumed_qty
+
+ def __get_transferred_items(self):
+ fields = ['`tabStock Entry`.`purchase_order`']
+ alias_dict = {'item_code': 'rm_item_code', 'subcontracted_item': 'main_item_code', 'basic_rate': 'rate'}
+
+ child_table_fields = ['item_code', 'item_name', 'description', 'qty', 'basic_rate', 'amount',
+ 'serial_no', 'uom', 'subcontracted_item', 'stock_uom', 'batch_no', 'conversion_factor',
+ 's_warehouse', 't_warehouse', 'item_group', 'po_detail']
+
+ if self.backflush_based_on == 'BOM':
+ child_table_fields.append('original_item')
+
+ for field in child_table_fields:
+ fields.append(f'`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}')
+
+ filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purpose', '=', 'Send to Subcontractor'],
+ ['Stock Entry', 'purchase_order', 'in', self.purchase_orders]]
+
+ return frappe.get_all('Stock Entry', fields = fields, filters=filters)
+
+ def __get_received_items(self, doctype):
+ fields = []
+ self.po_field = 'purchase_order'
+
+ for field in ['name', self.po_field, 'parent']:
+ fields.append(f'`tab{doctype} Item`.`{field}`')
+
+ filters = [[doctype, 'docstatus', '=', 1], [f'{doctype} Item', self.po_field, 'in', self.purchase_orders]]
+ if doctype == 'Purchase Invoice':
+ filters.append(['Purchase Invoice', 'update_stock', "=", 1])
+
+ return frappe.get_all(f'{doctype}', fields = fields, filters = filters)
+
+ def __get_consumed_items(self, doctype, pr_items):
+ return frappe.get_all('Purchase Receipt Item Supplied',
+ fields = ['serial_no', 'rm_item_code', 'reference_name', 'batch_no', 'consumed_qty', 'main_item_code'],
+ filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items)), 'parenttype': doctype})
+
+ def __set_alternative_item_details(self, row):
+ if row.get('original_item'):
+ self.alternative_item_details[row.get('original_item')] = row
+
+ def __get_pending_qty_to_receive(self):
+ '''Get qty to be received against the purchase order.'''
+
+ self.qty_to_be_received = defaultdict(float)
+
+ if self.doctype != 'Purchase Order' and self.backflush_based_on != 'BOM' and self.purchase_orders:
+ for row in frappe.get_all('Purchase Order Item',
+ fields = ['item_code', '(qty - received_qty) as qty', 'parent', 'name'],
+ filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}):
+
+ self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
+
+ def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
+ doctype = 'BOM Item' if not exploded_item else 'BOM Explosion Item'
+ fields = [f'`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit']
+
+ alias_dict = {'item_code': 'rm_item_code', 'name': 'bom_detail_no', 'source_warehouse': 'reserve_warehouse'}
+ for field in ['item_code', 'name', 'rate', 'stock_uom',
+ 'source_warehouse', 'description', 'item_name', 'stock_uom']:
+ fields.append(f'`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}')
+
+ filters = [[doctype, 'parent', '=', bom_no], [doctype, 'docstatus', '=', 1],
+ ['BOM', 'item', '=', item_code], [doctype, 'sourced_by_supplier', '=', 0]]
+
+ return frappe.get_all('BOM', fields = fields, filters=filters, order_by = f'`tab{doctype}`.`idx`') or []
+
+ def __remove_changed_rows(self):
+ if not self.__changed_name:
+ return
+
+ i=1
+ self.set(self.raw_material_table, [])
+ for d in self._doc_before_save.supplied_items:
+ if d.reference_name in self.__changed_name:
+ continue
+
+ if (d.reference_name not in self.__reference_name):
+ continue
+
+ d.idx = i
+ self.append('supplied_items', d)
+
+ i += 1
+
+ def __set_supplied_items(self):
+ self.bom_items = {}
+
+ has_supplied_items = True if self.get(self.raw_material_table) else False
+ for row in self.items:
+ if (self.doctype != 'Purchase Order' and ((self.__changed_name and row.name not in self.__changed_name)
+ or (has_supplied_items and not self.__changed_name))):
+ continue
+
+ if self.doctype == 'Purchase Order' or self.backflush_based_on == 'BOM':
+ for bom_item in self.__get_materials_from_bom(row.item_code, row.bom, row.get('include_exploded_items')):
+ qty = (flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor)
+ bom_item.main_item_code = row.item_code
+ self.__update_reserve_warehouse(bom_item, row)
+ self.__set_alternative_item(bom_item)
+ self.__add_supplied_item(row, bom_item, qty)
+
+ elif self.backflush_based_on != 'BOM':
+ for key, transfer_item in self.available_materials.items():
+ if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0:
+ qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0
+ transfer_item.qty -= qty
+ self.__add_supplied_item(row, transfer_item.get('item_details'), qty)
+
+ if self.qty_to_be_received:
+ self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty
+
+ def __update_reserve_warehouse(self, row, item):
+ if self.doctype == 'Purchase Order':
+ row.reserve_warehouse = (self.set_reserve_warehouse or item.warehouse)
+
+ def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
+ key = (item_row.item_code, item_row.purchase_order)
+
+ if self.qty_to_be_received == item_row.qty:
+ return transfer_item.qty
+
+ if self.qty_to_be_received:
+ qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
+ transfer_item.item_details.required_qty = transfer_item.qty
+
+ if (transfer_item.serial_no or frappe.get_cached_value('UOM',
+ transfer_item.item_details.stock_uom, 'must_be_whole_number')):
+ return frappe.utils.ceil(qty)
+
+ return qty
+
+ def __set_alternative_item(self, bom_item):
+ if self.alternative_item_details.get(bom_item.rm_item_code):
+ bom_item.update(self.alternative_item_details[bom_item.rm_item_code])
+
+ def __add_supplied_item(self, item_row, bom_item, qty):
+ bom_item.conversion_factor = item_row.conversion_factor
+ rm_obj = self.append(self.raw_material_table, bom_item)
+ rm_obj.reference_name = item_row.name
+
+ if self.doctype == 'Purchase Order':
+ rm_obj.required_qty = qty
+ else:
+ rm_obj.consumed_qty = 0
+ rm_obj.purchase_order = item_row.purchase_order
+ self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
+
+ def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
+ key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
+
+ if (self.available_materials.get(key) and self.available_materials[key]['batch_no']):
+ new_rm_obj = None
+ for batch_no, batch_qty in self.available_materials[key]['batch_no'].items():
+ if batch_qty >= qty:
+ self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
+ self.available_materials[key]['batch_no'][batch_no] -= qty
+ return
+
+ elif qty > 0 and batch_qty > 0:
+ qty -= batch_qty
+ new_rm_obj = self.append(self.raw_material_table, bom_item)
+ new_rm_obj.reference_name = item_row.name
+ self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
+ self.available_materials[key]['batch_no'][batch_no] = 0
+
+ if abs(qty) > 0 and not new_rm_obj:
+ self.__set_consumed_qty(rm_obj, qty)
+ else:
+ self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
+ self.__set_serial_nos(item_row, rm_obj)
+
+ def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
+ rm_obj.required_qty = required_qty
+ rm_obj.consumed_qty = consumed_qty
+
+ def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty):
+ rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no,
+ 'required_qty': qty, 'purchase_order': item_row.purchase_order})
+
+ self.__set_serial_nos(item_row, rm_obj)
+
+ def __set_serial_nos(self, item_row, rm_obj):
+ key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
+ if (self.available_materials.get(key) and self.available_materials[key]['serial_no']):
+ used_serial_nos = self.available_materials[key]['serial_no'][0: cint(rm_obj.consumed_qty)]
+ rm_obj.serial_no = '\n'.join(used_serial_nos)
+
+ # Removed the used serial nos from the list
+ for sn in used_serial_nos:
+ self.available_materials[key]['serial_no'].remove(sn)
+
+ def set_consumed_qty_in_po(self):
+ # Update consumed qty back in the purchase order
+ if self.is_subcontracted != 'Yes':
+ return
+
+ self.__get_purchase_orders()
+ itemwise_consumed_qty = defaultdict(float)
+ for doctype in ['Purchase Receipt', 'Purchase Invoice']:
+ consumed_items, pr_items = self.__update_consumed_materials(doctype, return_consumed_items=True)
+
+ for row in consumed_items:
+ key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
+ itemwise_consumed_qty[key] += row.consumed_qty
+
+ self.__update_consumed_qty_in_po(itemwise_consumed_qty)
+
+ def __update_consumed_qty_in_po(self, itemwise_consumed_qty):
+ fields = ['main_item_code', 'rm_item_code', 'parent', 'supplied_qty', 'name']
+ filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}
+
+ for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters, order_by='idx'):
+ key = (row.rm_item_code, row.main_item_code, row.parent)
+ consumed_qty = itemwise_consumed_qty.get(key, 0)
+
+ if row.supplied_qty < consumed_qty:
+ consumed_qty = row.supplied_qty
+
+ itemwise_consumed_qty[key] -= consumed_qty
+ frappe.db.set_value('Purchase Order Item Supplied', row.name, 'consumed_qty', consumed_qty)
+
+ def __validate_supplied_items(self):
+ if self.doctype not in ['Purchase Invoice', 'Purchase Receipt']:
+ return
+
+ for row in self.get(self.raw_material_table):
+ self.__validate_consumed_qty(row)
+
+ key = (row.rm_item_code, row.main_item_code, row.purchase_order)
+ if not self.__transferred_items or not self.__transferred_items.get(key):
+ return
+
+ self.__validate_batch_no(row, key)
+ self.__validate_serial_no(row, key)
+
+ def __validate_consumed_qty(self, row):
+ if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0:
+ msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}'
+
+ frappe.throw(_(msg),title=_('Consumed Items Qty Check'))
+
+ def __validate_batch_no(self, row, key):
+ if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'):
+ link = get_link_to_form('Purchase Order', row.purchase_order)
+ msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Purchase Order {link}'
+ frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
+
+ def __validate_serial_no(self, row, key):
+ if row.get('serial_no'):
+ serial_nos = get_serial_nos(row.get('serial_no'))
+ incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get('serial_no'))
+
+ if incorrect_sn:
+ incorrect_sn = "\n".join(incorrect_sn)
+ link = get_link_to_form('Purchase Order', row.purchase_order)
+ msg = f'The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}'
+ frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))
\ No newline at end of file
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index dda642a5323..56da5b71da0 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -54,6 +54,7 @@ class calculate_taxes_and_totals(object):
if item.item_code and item.get('item_tax_template'):
item_doc = frappe.get_cached_doc("Item", item.item_code)
args = {
+ 'net_rate': item.net_rate or item.rate,
'tax_category': self.doc.get('tax_category'),
'posting_date': self.doc.get('posting_date'),
'bill_date': self.doc.get('bill_date'),
@@ -77,10 +78,12 @@ class calculate_taxes_and_totals(object):
taxes = _get_item_tax_template(args, item_taxes + item_group_taxes, for_validate=True)
- if item.item_tax_template not in taxes:
- frappe.throw(_("Row {0}: Invalid Item Tax Template for item {1}").format(
- item.idx, frappe.bold(item.item_code)
- ))
+ if taxes:
+ if item.item_tax_template not in taxes:
+ item.item_tax_template = taxes[0]
+ frappe.msgprint(_("Row {0}: Item Tax template updated as per validity and rate applied").format(
+ item.idx, frappe.bold(item.item_code)
+ ))
def validate_conversion_rate(self):
# validate conversion rate
@@ -655,7 +658,13 @@ class calculate_taxes_and_totals(object):
item.margin_type = None
item.margin_rate_or_amount = 0.0
- if item.margin_type and item.margin_rate_or_amount:
+ if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate):
+ item.margin_type = "Amount"
+ item.margin_rate_or_amount = flt(item.rate - item.price_list_rate,
+ item.precision("margin_rate_or_amount"))
+ item.rate_with_margin = item.rate
+
+ elif item.margin_type and item.margin_rate_or_amount:
margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100
rate_with_margin = flt(item.price_list_rate) + flt(margin_value)
base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate)
@@ -683,7 +692,6 @@ class calculate_taxes_and_totals(object):
self.calculate_paid_amount()
-
def get_itemised_tax_breakup_html(doc):
if not doc.taxes:
return
diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json
index 8517ddec321..fe7b4e17f0b 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/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py
index 2009ebf7cba..df73f09c493 100644
--- a/erpnext/crm/doctype/appointment/appointment.py
+++ b/erpnext/crm/doctype/appointment/appointment.py
@@ -38,7 +38,7 @@ class Appointment(Document):
number_of_agents = frappe.db.get_single_value('Appointment Booking Settings', 'number_of_agents')
if not number_of_agents == 0:
if (number_of_appointments_in_same_slot >= number_of_agents):
- frappe.throw('Time slot is not available')
+ frappe.throw(_('Time slot is not available'))
# Link lead
if not self.party:
lead = self.find_lead_by_email()
@@ -75,10 +75,10 @@ class Appointment(Document):
subject=_('Appointment Confirmation'))
if frappe.session.user == "Guest":
frappe.msgprint(
- 'Please check your email to confirm the appointment')
+ _('Please check your email to confirm the appointment'))
else :
frappe.msgprint(
- 'Appointment was created. But no lead was found. Please check the email to confirm')
+ _('Appointment was created. But no lead was found. Please check the email to confirm'))
def on_change(self):
# Sync Calendar
@@ -91,7 +91,7 @@ class Appointment(Document):
def set_verified(self, email):
if not email == self.customer_email:
- frappe.throw('Email verification failed.')
+ frappe.throw(_('Email verification failed.'))
# Create new lead
self.create_lead_and_link()
# Remove unverified status
@@ -184,7 +184,7 @@ class Appointment(Document):
appointment_event.insert(ignore_permissions=True)
self.calendar_event = appointment_event.name
self.save(ignore_permissions=True)
-
+
def _get_verify_url(self):
verify_route = '/book_appointment/verify'
params = {
diff --git a/erpnext/crm/doctype/campaign/__init__.py b/erpnext/crm/doctype/campaign/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/crm/doctype/campaign/campaign.js b/erpnext/crm/doctype/campaign/campaign.js
new file mode 100644
index 00000000000..11bfa74b29c
--- /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 986ac1306cd..f833f4c9d11 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 10945428aee..e32799f34eb 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 00000000000..7124b8c7d60
--- /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 b18129b14fc..ce36069052e 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -78,8 +78,25 @@ class Lead(SellingController):
def on_trash(self):
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
+ self.unlink_dynamic_links()
self.delete_events()
+ def unlink_dynamic_links(self):
+ links = frappe.get_all('Dynamic Link', filters={'link_doctype': self.doctype, 'link_name': self.name}, fields=['parent', 'parenttype'])
+
+ for link in links:
+ linked_doc = frappe.get_doc(link['parenttype'], link['parent'])
+
+ if len(linked_doc.get('links')) == 1:
+ linked_doc.delete()
+ else:
+ to_remove = None
+ for d in linked_doc.get('links'):
+ if d.link_doctype == self.doctype and d.link_name == self.name:
+ to_remove = d
+ if to_remove:
+ link_doctype.remove(to_remove)
+
def has_customer(self):
return frappe.db.get_value("Customer", {"lead_name": self.name})
diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js
index 3f5c95ab0aa..fe5707af296 100644
--- a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js
+++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js
@@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Opportunity"] = {
get_chart_data: function (_columns, result) {
return {
data: {
- labels: result.map(d => d[0]),
+ labels: result.map(d => d.creation_date),
datasets: [{
name: "First Response Time",
- values: result.map(d => d[1])
+ values: result.map(d => d.first_response_time)
}]
},
type: "line",
@@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Opportunity"] = {
hide_days: 0,
hide_seconds: 0
};
- value = frappe.utils.get_formatted_duration(d, duration_options);
- return value;
+ return frappe.utils.get_formatted_duration(d, duration_options);
}
}
}
diff --git a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
index 6a0dcf460a7..0f2ea96a583 100644
--- a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
+++ b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
@@ -75,7 +75,7 @@ class CourseSchedulingTool(Document):
"""Validates if Course Start Date is greater than Course End Date"""
if self.course_start_date > self.course_end_date:
frappe.throw(
- "Course Start Date cannot be greater than Course End Date.")
+ _("Course Start Date cannot be greater than Course End Date."))
def delete_course_schedule(self, rescheduled, reschedule_errors):
"""Delete all course schedule within the Date range and specified filters"""
diff --git a/erpnext/education/utils.py b/erpnext/education/utils.py
index 819106bde78..3070e6a3e8a 100644
--- a/erpnext/education/utils.py
+++ b/erpnext/education/utils.py
@@ -236,7 +236,7 @@ def get_quiz(quiz_name, course):
'questions': questions,
'activity': None,
'is_time_bound': quiz.is_time_bound,
- 'duration':quiz.duration
+ 'duration': quiz.duration
}
student = get_current_student()
@@ -355,11 +355,11 @@ def get_or_create_course_enrollment(course, program):
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 3c2e59ab821..b0e662d3f32 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 frappe
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 @@ class TestMpesaSettings(unittest.TestCase):
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 @@ class TestMpesaSettings(unittest.TestCase):
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 @@ class TestMpesaSettings(unittest.TestCase):
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 @@ def create_mpesa_settings(payment_gateway_name="Express"):
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/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
index 5f990cdd034..42d4b9b2b43 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
@@ -99,5 +99,7 @@ class PlaidConnector():
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions))
transactions.extend(response["transactions"])
return transactions
+ except ItemError as e:
+ raise e
except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
index bbc2ca8846c..37bf2824505 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
@@ -16,6 +16,10 @@ frappe.ui.form.on('Plaid Settings', {
new erpnext.integrations.plaidLink(frm);
});
+ frm.add_custom_button(__('Reset Plaid Link'), () => {
+ new erpnext.integrations.plaidLink(frm);
+ });
+
frm.add_custom_button(__("Sync Now"), () => {
frappe.call({
method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization",
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index ce15e47c5ef..3ef069b5e20 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -12,6 +12,7 @@ from frappe.desk.doctype.tag.tag import add_tag
from frappe.model.document import Document
from frappe.utils import add_months, formatdate, getdate, today
+from plaid.errors import ItemError
class PlaidSettings(Document):
@staticmethod
@@ -51,7 +52,7 @@ def add_institution(token, response):
})
bank.insert()
except Exception:
- frappe.throw(frappe.get_traceback())
+ frappe.log_error(frappe.get_traceback(), title=_('Plaid Link Error'))
else:
bank = frappe.get_doc("Bank", response["institution"]["name"])
bank.plaid_access_token = access_token
@@ -83,7 +84,12 @@ def add_bank_accounts(response, bank, company):
if not acc_subtype:
add_account_subtype(account["subtype"])
- if not frappe.db.exists("Bank Account", dict(integration_id=account["id"])):
+ existing_bank_account = frappe.db.exists("Bank Account", {
+ 'account_name': account["name"],
+ 'bank': bank["bank_name"]
+ })
+
+ if not existing_bank_account:
try:
new_account = frappe.get_doc({
"doctype": "Bank Account",
@@ -103,10 +109,27 @@ def add_bank_accounts(response, bank, company):
except frappe.UniqueValidationError:
frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"]))
except Exception:
- frappe.throw(frappe.get_traceback())
+ frappe.log_error(frappe.get_traceback(), title=_("Plaid Link Error"))
+ frappe.throw(_("There was an error creating Bank Account while linking with Plaid."),
+ title=_("Plaid Link Failed"))
else:
- result.append(frappe.db.get_value("Bank Account", dict(integration_id=account["id"]), "name"))
+ try:
+ existing_account = frappe.get_doc('Bank Account', existing_bank_account)
+ existing_account.update({
+ "bank": bank["bank_name"],
+ "account_name": account["name"],
+ "account_type": account.get("type", ""),
+ "account_subtype": account.get("subtype", ""),
+ "mask": account.get("mask", ""),
+ "integration_id": account["id"]
+ })
+ existing_account.save()
+ result.append(existing_bank_account)
+ except Exception:
+ frappe.log_error(frappe.get_traceback(), title=_("Plaid Link Error"))
+ frappe.throw(_("There was an error updating Bank Account {} while linking with Plaid.").format(
+ existing_bank_account), title=_("Plaid Link Failed"))
return result
@@ -172,9 +195,16 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
account_id = None
plaid = PlaidConnector(access_token)
- transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
- return transactions
+ try:
+ transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
+ except ItemError as e:
+ if e.code == "ITEM_LOGIN_REQUIRED":
+ msg = _("There was an error syncing transactions.") + " "
+ msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " "
+ frappe.log_error(msg, title=_("Plaid Link Refresh Required"))
+
+ return transactions or []
def new_bank_transaction(transaction):
diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py
index 3840e781b4c..a5e162f8b5d 100644
--- a/erpnext/erpnext_integrations/utils.py
+++ b/erpnext/erpnext_integrations/utils.py
@@ -52,7 +52,8 @@ def create_mode_of_payment(gateway, payment_type="General"):
"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 @@ def create_mode_of_payment(gateway, payment_type="General"):
})
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/healthcare/doctype/patient_history_settings/test_patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py
index c93b788aed7..33119d8185f 100644
--- a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py
+++ b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
import unittest
import json
-from frappe.utils import getdate
+from frappe.utils import getdate, strip_html
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient
class TestPatientHistorySettings(unittest.TestCase):
@@ -44,9 +44,9 @@ class TestPatientHistorySettings(unittest.TestCase):
self.assertTrue(medical_rec)
medical_rec = frappe.get_doc("Patient Medical Record", medical_rec)
- expected_subject = "Date: {0}
Rating: 3
Feedback: Test Patient History Settings
".format(
+ expected_subject = "Date: {0}Rating: 3Feedback: Test Patient History Settings".format(
frappe.utils.format_date(getdate()))
- self.assertEqual(medical_rec.subject, expected_subject)
+ self.assertEqual(strip_html(medical_rec.subject), expected_subject)
self.assertEqual(medical_rec.patient, patient)
self.assertEqual(medical_rec.communication_date, getdate())
@@ -101,4 +101,4 @@ def create_doc(patient):
}).insert()
doc.submit()
- return doc
\ No newline at end of file
+ return doc
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 3da606b68b8..59b011d1a97 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -25,7 +25,8 @@ doctype_js = {
"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 = {
@@ -158,6 +159,7 @@ website_route_rules = [
"parents": [{"label": _("Material Request"), "route": "material-requests"}]
}
},
+ {"from_route": "/project", "to_route": "Project"}
]
standard_portal_menu_items = [
@@ -244,7 +246,10 @@ doc_events = {
"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/attendance/attendance.js b/erpnext/hr/doctype/attendance/attendance.js
index c3c3cb82f94..7964078c7f0 100644
--- a/erpnext/hr/doctype/attendance/attendance.js
+++ b/erpnext/hr/doctype/attendance/attendance.js
@@ -11,5 +11,5 @@ cur_frm.cscript.onload = function(doc, cdt, cdn) {
cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) {
return{
query: "erpnext.controllers.queries.employee_query"
- }
+ }
}
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index f3b8a799b3c..3412675d811 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -15,6 +15,7 @@ class Attendance(Document):
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
self.validate_attendance_date()
self.validate_duplicate_record()
+ self.validate_employee_status()
self.check_leave_record()
def validate_attendance_date(self):
@@ -38,6 +39,10 @@ class Attendance(Document):
frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format(
frappe.bold(self.employee), frappe.bold(self.attendance_date)))
+ def validate_employee_status(self):
+ if frappe.db.get_value("Employee", self.employee, "status") == "Inactive":
+ frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee))
+
def check_leave_record(self):
leave_record = frappe.db.sql("""
select leave_type, half_day, half_day_date
diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js
index 0c7eafe9c61..9a3bac0eb23 100644
--- a/erpnext/hr/doctype/attendance/attendance_list.js
+++ b/erpnext/hr/doctype/attendance/attendance_list.js
@@ -21,6 +21,9 @@ frappe.listview_settings['Attendance'] = {
label: __('For Employee'),
fieldtype: 'Link',
options: 'Employee',
+ get_query: () => {
+ return {query: "erpnext.controllers.queries.employee_query"}
+ },
reqd: 1,
onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1);
diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json
index 5442ed56c31..d592a9c79e2 100644
--- a/erpnext/hr/doctype/employee/employee.json
+++ b/erpnext/hr/doctype/employee/employee.json
@@ -207,7 +207,7 @@
"label": "Status",
"oldfieldname": "status",
"oldfieldtype": "Select",
- "options": "Active\nInactive\nLeft",
+ "options": "Active\nInactive\nSuspended\nLeft",
"reqd": 1,
"search_index": 1
},
@@ -813,7 +813,7 @@
"idx": 24,
"image_field": "image",
"links": [],
- "modified": "2021-06-12 11:31:37.730760",
+ "modified": "2021-06-17 11:31:37.730760",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index bc5694226ad..fa017d9d4c2 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
-from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr
+from frappe.utils import getdate, validate_email_address, today, add_years, cstr
from frappe.model.naming import set_name_by_naming_series
from frappe import throw, _, scrub
from frappe.permissions import add_user_permission, remove_user_permission, \
@@ -12,7 +12,6 @@ from frappe.permissions import add_user_permission, remove_user_permission, \
from frappe.model.document import Document
from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet
-from erpnext.hr.doctype.job_offer.job_offer import get_staffing_plan_detail
class EmployeeUserDisabledError(frappe.ValidationError): pass
class EmployeeLeftValidationError(frappe.ValidationError): pass
@@ -37,7 +36,7 @@ class Employee(NestedSet):
def validate(self):
from erpnext.controllers.status_updater import validate_status
- validate_status(self.status, ["Active", "Inactive", "Left"])
+ validate_status(self.status, ["Active", "Inactive", "Suspended", "Left"])
self.employee = self.name
self.set_employee_name()
diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py
index 285374d9f69..e853bee69f2 100644
--- a/erpnext/hr/doctype/employee/employee_dashboard.py
+++ b/erpnext/hr/doctype/employee/employee_dashboard.py
@@ -7,7 +7,8 @@ def get_data():
'heatmap_message': _('This is based on the attendance of this Employee'),
'fieldname': 'employee',
'non_standard_fieldnames': {
- 'Bank Account': 'party'
+ 'Bank Account': 'party',
+ 'Employee Grievance': 'raised_by'
},
'transactions': [
{
@@ -20,7 +21,7 @@ def get_data():
},
{
'label': _('Lifecycle'),
- 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation']
+ 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
},
{
'label': _('Shift'),
diff --git a/erpnext/hr/doctype/employee/employee_list.js b/erpnext/hr/doctype/employee/employee_list.js
index 6679e318c24..d37e1496ca3 100644
--- a/erpnext/hr/doctype/employee/employee_list.js
+++ b/erpnext/hr/doctype/employee/employee_list.js
@@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = {
filters: [["status","=", "Active"]],
get_indicator: function(doc) {
var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status];
- indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray"}[doc.status];
+ indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray", "Suspended": "orange"}[doc.status];
return indicator;
}
};
diff --git a/erpnext/hr/doctype/employee_grievance/__init__.py b/erpnext/hr/doctype/employee_grievance/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.js b/erpnext/hr/doctype/employee_grievance/employee_grievance.js
new file mode 100644
index 00000000000..25c5badbc77
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.js
@@ -0,0 +1,39 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Employee Grievance', {
+ setup: function(frm) {
+ frm.set_query('grievance_against_party', function() {
+ return {
+ filters: {
+ name: ['in', [
+ 'Company', 'Department', 'Employee Group', 'Employee Grade', 'Employee']
+ ]
+ }
+ };
+ });
+ frm.set_query('associated_document_type', function() {
+ let ignore_modules = ["Setup", "Core", "Integrations", "Automation", "Website",
+ "Utilities", "Event Streaming", "Social", "Chat", "Data Migration", "Printing", "Desk", "Custom"];
+ return {
+ filters: {
+ istable: 0,
+ issingle: 0,
+ module: ["Not In", ignore_modules]
+ }
+ };
+ });
+ },
+
+ grievance_against_party: function(frm) {
+ let filters = {};
+ if (frm.doc.grievance_against_party == 'Employee' && frm.doc.raised_by) {
+ filters.name = ["!=", frm.doc.raised_by];
+ }
+ frm.set_query('grievance_against', function() {
+ return {
+ filters: filters
+ };
+ });
+ },
+});
diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.json b/erpnext/hr/doctype/employee_grievance/employee_grievance.json
new file mode 100644
index 00000000000..5a918562afb
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.json
@@ -0,0 +1,261 @@
+{
+ "actions": [],
+ "autoname": "HR-GRIEV-.YYYY.-.#####",
+ "creation": "2021-05-11 13:41:51.485295",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "subject",
+ "raised_by",
+ "employee_name",
+ "designation",
+ "column_break_3",
+ "date",
+ "status",
+ "reports_to",
+ "grievance_details_section",
+ "grievance_against_party",
+ "grievance_against",
+ "grievance_type",
+ "column_break_11",
+ "associated_document_type",
+ "associated_document",
+ "section_break_14",
+ "description",
+ "investigation_details_section",
+ "cause_of_grievance",
+ "resolution_details_section",
+ "resolved_by",
+ "resolution_date",
+ "employee_responsible",
+ "column_break_16",
+ "resolution_detail",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "grievance_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Grievance Type",
+ "options": "Grievance Type",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Date ",
+ "reqd": 1
+ },
+ {
+ "default": "Open",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Status",
+ "options": "Open\nInvestigated\nResolved\nInvalid",
+ "reqd": 1
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Description",
+ "reqd": 1
+ },
+ {
+ "fieldname": "cause_of_grievance",
+ "fieldtype": "Text",
+ "label": "Cause of Grievance",
+ "mandatory_depends_on": "eval: doc.status == \"Investigated\" || doc.status == \"Resolved\""
+ },
+ {
+ "fieldname": "resolution_details_section",
+ "fieldtype": "Section Break",
+ "label": "Resolution Details"
+ },
+ {
+ "fieldname": "resolved_by",
+ "fieldtype": "Link",
+ "label": "Resolved By",
+ "mandatory_depends_on": "eval: doc.status == \"Resolved\"",
+ "options": "User"
+ },
+ {
+ "fieldname": "employee_responsible",
+ "fieldtype": "Link",
+ "label": "Employee Responsible ",
+ "options": "Employee"
+ },
+ {
+ "fieldname": "resolution_detail",
+ "fieldtype": "Small Text",
+ "label": "Resolution Details",
+ "mandatory_depends_on": "eval: doc.status == \"Resolved\""
+ },
+ {
+ "fieldname": "column_break_16",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "resolution_date",
+ "fieldtype": "Date",
+ "label": "Resolution Date",
+ "mandatory_depends_on": "eval: doc.status == \"Resolved\""
+ },
+ {
+ "fieldname": "grievance_against",
+ "fieldtype": "Dynamic Link",
+ "label": "Grievance Against",
+ "options": "grievance_against_party",
+ "reqd": 1
+ },
+ {
+ "fieldname": "raised_by",
+ "fieldtype": "Link",
+ "label": "Raised By",
+ "options": "Employee",
+ "reqd": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Employee Grievance",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fetch_from": "raised_by.designation",
+ "fieldname": "designation",
+ "fieldtype": "Link",
+ "label": "Designation",
+ "options": "Designation",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "raised_by.reports_to",
+ "fieldname": "reports_to",
+ "fieldtype": "Link",
+ "label": "Reports To",
+ "options": "Employee",
+ "read_only": 1
+ },
+ {
+ "fieldname": "grievance_details_section",
+ "fieldtype": "Section Break",
+ "label": "Grievance Details"
+ },
+ {
+ "fieldname": "column_break_11",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_14",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "grievance_against_party",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Grievance Against Party",
+ "options": "DocType",
+ "reqd": 1
+ },
+ {
+ "fieldname": "associated_document_type",
+ "fieldtype": "Link",
+ "label": "Associated Document Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "associated_document",
+ "fieldtype": "Dynamic Link",
+ "label": "Associated Document",
+ "options": "associated_document_type"
+ },
+ {
+ "fieldname": "investigation_details_section",
+ "fieldtype": "Section Break",
+ "label": "Investigation Details"
+ },
+ {
+ "fetch_from": "raised_by.employee_name",
+ "fieldname": "employee_name",
+ "fieldtype": "Data",
+ "label": "Employee Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "subject",
+ "fieldtype": "Data",
+ "label": "Subject",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2021-06-21 12:51:01.499486",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Employee Grievance",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "select": 1,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "select": 1,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "search_fields": "subject,raised_by,grievance_against_party",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "subject",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.py b/erpnext/hr/doctype/employee_grievance/employee_grievance.py
new file mode 100644
index 00000000000..503b5ea4449
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.py
@@ -0,0 +1,15 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _, bold
+from frappe.model.document import Document
+
+class EmployeeGrievance(Document):
+ def on_submit(self):
+ if self.status not in ["Invalid", "Resolved"]:
+ frappe.throw(_("Only Employee Grievance with status {0} or {1} can be submitted").format(
+ bold("Invalid"),
+ bold("Resolved"))
+ )
+
diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js b/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js
new file mode 100644
index 00000000000..fc08e216099
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js
@@ -0,0 +1,12 @@
+frappe.listview_settings["Employee Grievance"] = {
+ has_indicator_for_draft: 1,
+ get_indicator: function(doc) {
+ var colors = {
+ "Open": "red",
+ "Investigated": "orange",
+ "Resolved": "green",
+ "Invalid": "grey"
+ };
+ return [__(doc.status), colors[doc.status], "status,=," + doc.status];
+ }
+};
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py
new file mode 100644
index 00000000000..a615b20a5a2
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py
@@ -0,0 +1,51 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+import unittest
+from frappe.utils import today
+from erpnext.hr.doctype.employee.test_employee import make_employee
+class TestEmployeeGrievance(unittest.TestCase):
+ def test_create_employee_grievance(self):
+ create_employee_grievance()
+
+def create_employee_grievance():
+ grievance_type = create_grievance_type()
+ emp_1 = make_employee("test_emp_grievance_@example.com", company="_Test Company")
+ emp_2 = make_employee("testculprit@example.com", company="_Test Company")
+
+ grievance = frappe.new_doc("Employee Grievance")
+ grievance.subject = "Test Employee Grievance"
+ grievance.raised_by = emp_1
+ grievance.date = today()
+ grievance.grievance_type = grievance_type
+ grievance.grievance_against_party = "Employee"
+ grievance.grievance_against = emp_2
+ grievance.description = "test descrip"
+
+ #set cause
+ grievance.cause_of_grievance = "test cause"
+
+ #resolution details
+ grievance.resolution_date = today()
+ grievance.resolution_detail = "test resolution detail"
+ grievance.resolved_by = "test_emp_grievance_@example.com"
+ grievance.employee_responsible = emp_2
+ grievance.status = "Resolved"
+
+ grievance.save()
+ grievance.submit()
+
+ return grievance
+
+
+def create_grievance_type():
+ if frappe.db.exists("Grievance Type", "Employee Abuse"):
+ return frappe.get_doc("Grievance Type", "Employee Abuse")
+ grievance_type = frappe.new_doc("Grievance Type")
+ grievance_type.name = "Employee Abuse"
+ grievance_type.description = "Test"
+ grievance_type.save()
+
+ return grievance_type.name
+
diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
index 578eccf787d..96ea686706c 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 @@ class TestExpenseClaim(unittest.TestCase):
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 @@ class TestExpenseClaim(unittest.TestCase):
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 @@ def generate_taxes():
parent_account = frappe.db.get_value('Account',
{'company': company_name, 'is_group':1, 'account_type': 'Tax'},
'name')
- account = create_account(company=company_name, account_name="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/grievance_type/__init__.py b/erpnext/hr/doctype/grievance_type/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.js b/erpnext/hr/doctype/grievance_type/grievance_type.js
new file mode 100644
index 00000000000..425f2fd5b5e
--- /dev/null
+++ b/erpnext/hr/doctype/grievance_type/grievance_type.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Grievance Type', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.json b/erpnext/hr/doctype/grievance_type/grievance_type.json
new file mode 100644
index 00000000000..1dce00a0e22
--- /dev/null
+++ b/erpnext/hr/doctype/grievance_type/grievance_type.json
@@ -0,0 +1,70 @@
+{
+ "actions": [],
+ "autoname": "Prompt",
+ "creation": "2021-05-11 12:41:50.256071",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "section_break_5",
+ "description"
+ ],
+ "fields": [
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Description"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-06-21 12:54:37.764712",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Grievance Type",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.py b/erpnext/hr/doctype/grievance_type/grievance_type.py
new file mode 100644
index 00000000000..618cf0a0318
--- /dev/null
+++ b/erpnext/hr/doctype/grievance_type/grievance_type.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 GrievanceType(Document):
+ pass
diff --git a/erpnext/hr/doctype/grievance_type/test_grievance_type.py b/erpnext/hr/doctype/grievance_type/test_grievance_type.py
new file mode 100644
index 00000000000..a02a34d41fa
--- /dev/null
+++ b/erpnext/hr/doctype/grievance_type/test_grievance_type.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestGrievanceType(unittest.TestCase):
+ pass
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_list.js b/erpnext/hr/doctype/job_applicant/job_applicant_list.js
index 3b9141ba79c..2ad0d591d8c 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant_list.js
+++ b/erpnext/hr/doctype/job_applicant/job_applicant_list.js
@@ -2,7 +2,7 @@
// MIT License. See license.txt
frappe.listview_settings['Job Applicant'] = {
- add_fields: ["company", "designation", "job_applicant", "status"],
+ add_fields: ["status"],
get_indicator: function (doc) {
if (doc.status == "Accepted") {
return [__(doc.status), "green", "status,=," + doc.status];
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
index ae02c512c23..3a6539ece9e 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
@@ -110,6 +110,7 @@
"label": "Allocation"
},
{
+ "allow_on_submit": 1,
"bold": 1,
"fieldname": "new_leaves_allocated",
"fieldtype": "Float",
@@ -235,7 +236,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-14 15:28:26.335104",
+ "modified": "2021-06-03 15:28:26.335104",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",
@@ -277,4 +278,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"timeline_field": "employee"
-}
\ No newline at end of file
+}
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
index 11302cad758..4757cd3b192 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
@@ -8,6 +8,7 @@ from frappe import _
from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name, get_leave_period
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry
+from erpnext.hr.doctype.leave_application.leave_application import get_approved_leaves_for_period
class OverlapError(frappe.ValidationError): pass
class BackDatedAllocationError(frappe.ValidationError): pass
@@ -55,6 +56,43 @@ class LeaveAllocation(Document):
if self.carry_forward:
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
+ def on_update_after_submit(self):
+ if self.has_value_changed("new_leaves_allocated"):
+ self.validate_against_leave_applications()
+ leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count()
+ args = {
+ "leaves": leaves_to_be_added,
+ "from_date": self.from_date,
+ "to_date": self.to_date,
+ "is_carry_forward": 0
+ }
+ create_leave_ledger_entry(self, args, True)
+
+ def get_existing_leave_count(self):
+ ledger_entries = frappe.get_all("Leave Ledger Entry",
+ filters={
+ "transaction_type": "Leave Allocation",
+ "transaction_name": self.name,
+ "employee": self.employee,
+ "company": self.company,
+ "leave_type": self.leave_type
+ },
+ pluck="leaves")
+ total_existing_leaves = 0
+ for entry in ledger_entries:
+ total_existing_leaves += entry
+
+ return total_existing_leaves
+
+ def validate_against_leave_applications(self):
+ leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type,
+ self.from_date, self.to_date)
+ if flt(leaves_taken) > flt(self.total_leaves_allocated):
+ if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
+ frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken))
+ else:
+ frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError)
+
def update_leave_policy_assignments_when_no_allocations_left(self):
allocations = frappe.db.get_list("Leave Allocation", filters = {
"docstatus": 1,
@@ -225,4 +263,4 @@ def get_unused_leaves(employee, leave_type, from_date, to_date):
def validate_carry_forward(leave_type):
if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"):
- frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type))
\ No newline at end of file
+ frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type))
diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
index 6e7ae87d08c..fdcd533660b 100644
--- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
@@ -1,10 +1,10 @@
from __future__ import unicode_literals
import frappe
+import erpnext
import unittest
from frappe.utils import nowdate, add_months, getdate, add_days
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation, expire_allocation
-
class TestLeaveAllocation(unittest.TestCase):
@classmethod
def setUpClass(cls):
@@ -164,6 +164,51 @@ class TestLeaveAllocation(unittest.TestCase):
leave_allocation.cancel()
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
+ def test_leave_addition_after_submit(self):
+ frappe.db.sql("delete from `tabLeave Allocation`")
+ frappe.db.sql("delete from `tabLeave Ledger Entry`")
+
+ leave_allocation = create_leave_allocation()
+ leave_allocation.submit()
+ self.assertTrue(leave_allocation.total_leaves_allocated, 15)
+ leave_allocation.new_leaves_allocated = 40
+ leave_allocation.submit()
+ self.assertTrue(leave_allocation.total_leaves_allocated, 40)
+
+ def test_leave_subtraction_after_submit(self):
+ frappe.db.sql("delete from `tabLeave Allocation`")
+ frappe.db.sql("delete from `tabLeave Ledger Entry`")
+ leave_allocation = create_leave_allocation()
+ leave_allocation.submit()
+ self.assertTrue(leave_allocation.total_leaves_allocated, 15)
+ leave_allocation.new_leaves_allocated = 10
+ leave_allocation.submit()
+ self.assertTrue(leave_allocation.total_leaves_allocated, 10)
+
+ def test_against_leave_application_validation_after_submit(self):
+ frappe.db.sql("delete from `tabLeave Allocation`")
+ frappe.db.sql("delete from `tabLeave Ledger Entry`")
+
+ leave_allocation = create_leave_allocation()
+ leave_allocation.submit()
+ self.assertTrue(leave_allocation.total_leaves_allocated, 15)
+ employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
+ leave_application = frappe.get_doc({
+ "doctype": 'Leave Application',
+ "employee": employee.name,
+ "leave_type": "_Test Leave Type",
+ "from_date": add_months(nowdate(), 2),
+ "to_date": add_months(add_days(nowdate(), 10), 2),
+ "company": erpnext.get_default_company() or "_Test Company",
+ "docstatus": 1,
+ "status": "Approved",
+ "leave_approver": 'test@example.com'
+ })
+ leave_application.submit()
+ leave_allocation.new_leaves_allocated = 8
+ leave_allocation.total_leaves_allocated = 8
+ self.assertRaises(frappe.ValidationError, leave_allocation.submit)
+
def create_leave_allocation(**args):
args = frappe._dict(args)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 0bf551e178c..cee6f374fdc 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -4,8 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, \
- comma_or, get_fullname, add_days, nowdate, get_datetime_str
+from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, get_fullname, add_days, nowdate
from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
@@ -85,7 +84,7 @@ class LeaveApplication(Document):
def validate_dates(self):
if frappe.db.get_single_value("HR Settings", "restrict_backdated_leave_application"):
- if self.from_date and self.from_date < frappe.utils.today():
+ if self.from_date and getdate(self.from_date) < getdate():
allowed_role = frappe.db.get_single_value("HR Settings", "role_allowed_to_create_backdated_leave_application")
if allowed_role not in frappe.get_roles():
frappe.throw(_("Only users with the {0} role can create backdated leave applications").format(allowed_role))
@@ -248,9 +247,9 @@ class LeaveApplication(Document):
self.throw_overlap_error(d)
def throw_overlap_error(self, d):
- msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(self.employee,
- d['leave_type'], formatdate(d['from_date']), formatdate(d['to_date'])) \
- + """ {0}""".format(d["name"])
+ form_link = get_link_to_form("Leave Application", d.name)
+ msg = _("Employee {0} has already applied for {1} between {2} and {3} : {4}").format(self.employee,
+ d['leave_type'], formatdate(d['from_date']), formatdate(d['to_date']), form_link)
frappe.throw(msg, OverlapError)
def get_total_leaves_on_half_day(self):
@@ -356,7 +355,7 @@ class LeaveApplication(Document):
sender = dict()
sender['email'] = frappe.get_doc('User', frappe.session.user).email
- sender['full_name'] = frappe.utils.get_fullname(sender['email'])
+ sender['full_name'] = get_fullname(sender['email'])
try:
frappe.sendmail(
@@ -823,4 +822,4 @@ def get_leave_approver(employee):
leave_approver = frappe.db.get_value('Department Approver', {'parent': department,
'parentfield': 'leave_approvers', 'idx': 1}, 'approver')
- return leave_approver
\ No newline at end of file
+ return leave_approver
diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.js b/erpnext/hr/doctype/staffing_plan/staffing_plan.js
index 04af2323c72..228391ba001 100644
--- a/erpnext/hr/doctype/staffing_plan/staffing_plan.js
+++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.js
@@ -103,4 +103,4 @@ var set_total_estimated_budget = function(frm) {
})
frm.set_value('total_estimated_budget', estimated_budget);
}
-}
\ No newline at end of file
+};
diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
index 533149a8235..e6c783aca22 100644
--- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py
+++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
@@ -41,7 +41,7 @@ class StaffingPlan(Document):
detail.total_estimated_cost = 0
if detail.number_of_positions > 0:
- if detail.vacancies > 0 and detail.estimated_cost_per_position:
+ if detail.vacancies and detail.estimated_cost_per_position:
detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position)
self.total_estimated_budget += detail.total_estimated_cost
@@ -76,12 +76,12 @@ class StaffingPlan(Document):
if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \
flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost):
frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \
- for {2} as per staffing plan {3} for parent company {4}."
- .format(cint(parent_plan_details[0].vacancies),
+ for {2} as per staffing plan {3} for parent company {4}.").format(
+ cint(parent_plan_details[0].vacancies),
parent_plan_details[0].total_estimated_cost,
frappe.bold(staffing_plan_detail.designation),
parent_plan_details[0].name,
- parent_company)), ParentCompanyError)
+ parent_company), ParentCompanyError)
#Get vacanices already planned for all companies down the hierarchy of Parent Company
lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"])
@@ -98,14 +98,14 @@ class StaffingPlan(Document):
(flt(parent_plan_details[0].total_estimated_cost) < \
(flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))):
frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \
- You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}."
- .format(cint(all_sibling_details.vacancies),
+ You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format(
+ cint(all_sibling_details.vacancies),
all_sibling_details.total_estimated_cost,
frappe.bold(staffing_plan_detail.designation),
parent_company,
cint(parent_plan_details[0].vacancies),
parent_plan_details[0].total_estimated_cost,
- parent_plan_details[0].name)))
+ parent_plan_details[0].name))
def validate_with_subsidiary_plans(self, staffing_plan_detail):
#Valdate this plan with all child company plan
@@ -121,11 +121,11 @@ class StaffingPlan(Document):
cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \
flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost):
frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \
- Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies"
- .format(self.company,
+ Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format(
+ self.company,
cint(children_details.vacancies),
children_details.total_estimated_cost,
- frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError)
+ frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError)
@frappe.whitelist()
def get_designation_counts(designation, company):
@@ -170,4 +170,4 @@ def get_active_staffing_plan_details(company, designation, from_date=getdate(now
designation, from_date, to_date)
# Only a single staffing plan can be active for a designation on given date
- return staffing_plan if staffing_plan else None
\ No newline at end of file
+ return staffing_plan if staffing_plan else None
diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js
index b7d34b178a0..d5f6e5f573e 100644
--- a/erpnext/hr/doctype/training_event/training_event.js
+++ b/erpnext/hr/doctype/training_event/training_event.js
@@ -20,11 +20,10 @@ frappe.ui.form.on('Training Event', {
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 @@ frappe.ui.form.on("Training Event Employee", {
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 2d313e9faca..bcb7d5e5bc0 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/doctype/vehicle_log/test_vehicle_log.py b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
index cf0048c1a76..ed52c4e1222 100644
--- a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
+++ b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
import unittest
-from frappe.utils import nowdate,flt, cstr,random_string
+from frappe.utils import nowdate, flt, cstr, random_string
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.vehicle_log.vehicle_log import make_expense_claim
@@ -18,23 +18,13 @@ class TestVehicleLog(unittest.TestCase):
self.employee_id = make_employee("testdriver@example.com", company="_Test Company")
self.license_plate = get_vehicle(self.employee_id)
-
+
def tearDown(self):
frappe.delete_doc("Vehicle", self.license_plate, force=1)
frappe.delete_doc("Employee", self.employee_id, force=1)
def test_make_vehicle_log_and_syncing_of_odometer_value(self):
- vehicle_log = frappe.get_doc({
- "doctype": "Vehicle Log",
- "license_plate": cstr(self.license_plate),
- "employee": self.employee_id,
- "date":frappe.utils.nowdate(),
- "odometer":5010,
- "fuel_qty":frappe.utils.flt(50),
- "price": frappe.utils.flt(500)
- })
- vehicle_log.save()
- vehicle_log.submit()
+ vehicle_log = make_vehicle_log(self.license_plate, self.employee_id)
#checking value of vehicle odometer value on submit.
vehicle = frappe.get_doc("Vehicle", self.license_plate)
@@ -51,19 +41,9 @@ class TestVehicleLog(unittest.TestCase):
self.assertEqual(vehicle.last_odometer, current_odometer - distance_travelled)
vehicle_log.delete()
-
+
def test_vehicle_log_fuel_expense(self):
- vehicle_log = frappe.get_doc({
- "doctype": "Vehicle Log",
- "license_plate": cstr(self.license_plate),
- "employee": self.employee_id,
- "date": frappe.utils.nowdate(),
- "odometer":5010,
- "fuel_qty":frappe.utils.flt(50),
- "price": frappe.utils.flt(500)
- })
- vehicle_log.save()
- vehicle_log.submit()
+ vehicle_log = make_vehicle_log(self.license_plate, self.employee_id)
expense_claim = make_expense_claim(vehicle_log.name)
fuel_expense = expense_claim.expenses[0].amount
@@ -73,6 +53,18 @@ class TestVehicleLog(unittest.TestCase):
frappe.delete_doc("Expense Claim", expense_claim.name)
frappe.delete_doc("Vehicle Log", vehicle_log.name)
+ def test_vehicle_log_with_service_expenses(self):
+ vehicle_log = make_vehicle_log(self.license_plate, self.employee_id, with_services=True)
+
+ expense_claim = make_expense_claim(vehicle_log.name)
+ expenses = expense_claim.expenses[0].amount
+ self.assertEqual(expenses, 27000)
+
+ vehicle_log.cancel()
+ frappe.delete_doc("Expense Claim", expense_claim.name)
+ frappe.delete_doc("Vehicle Log", vehicle_log.name)
+
+
def get_vehicle(employee_id):
license_plate=random_string(10).upper()
vehicle = frappe.get_doc({
@@ -81,15 +73,46 @@ def get_vehicle(employee_id):
"make": "Maruti",
"model": "PCM",
"employee": employee_id,
- "last_odometer":5000,
- "acquisition_date":frappe.utils.nowdate(),
+ "last_odometer": 5000,
+ "acquisition_date": nowdate(),
"location": "Mumbai",
"chassis_no": "1234ABCD",
"uom": "Litre",
- "vehicle_value":frappe.utils.flt(500000)
+ "vehicle_value": flt(500000)
})
try:
vehicle.insert()
except frappe.DuplicateEntryError:
pass
- return license_plate
\ No newline at end of file
+ return license_plate
+
+
+def make_vehicle_log(license_plate, employee_id, with_services=False):
+ vehicle_log = frappe.get_doc({
+ "doctype": "Vehicle Log",
+ "license_plate": cstr(license_plate),
+ "employee": employee_id,
+ "date": nowdate(),
+ "odometer": 5010,
+ "fuel_qty": flt(50),
+ "price": flt(500)
+ })
+
+ if with_services:
+ vehicle_log.append("service_detail", {
+ "service_item": "Oil Change",
+ "type": "Inspection",
+ "frequency": "Mileage",
+ "expense_amount": flt(500)
+ })
+ vehicle_log.append("service_detail", {
+ "service_item": "Wheels",
+ "type": "Change",
+ "frequency": "Half Yearly",
+ "expense_amount": flt(1500)
+ })
+
+ vehicle_log.save()
+ vehicle_log.submit()
+
+ return vehicle_log
\ No newline at end of file
diff --git a/erpnext/hr/doctype/vehicle_log/vehicle_log.json b/erpnext/hr/doctype/vehicle_log/vehicle_log.json
index 619e295ebe8..4ea904542d8 100644
--- a/erpnext/hr/doctype/vehicle_log/vehicle_log.json
+++ b/erpnext/hr/doctype/vehicle_log/vehicle_log.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "naming_series:",
"creation": "2016-09-03 14:14:51.788550",
"doctype": "DocType",
@@ -10,7 +11,6 @@
"naming_series",
"license_plate",
"employee",
- "column_break_4",
"column_break_7",
"model",
"make",
@@ -65,10 +65,6 @@
"options": "Employee",
"reqd": 1
},
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
- },
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
@@ -142,7 +138,6 @@
{
"fieldname": "service_detail",
"fieldtype": "Table",
- "label": "Service Detail",
"options": "Vehicle Service"
},
{
@@ -158,7 +153,7 @@
"fetch_from": "license_plate.last_odometer",
"fieldname": "last_odometer",
"fieldtype": "Int",
- "label": "last Odometer Value ",
+ "label": "Last Odometer Value ",
"read_only": 1,
"reqd": 1
},
@@ -168,7 +163,8 @@
}
],
"is_submittable": 1,
- "modified": "2020-03-18 16:45:45.060761",
+ "links": [],
+ "modified": "2021-05-17 00:10:21.188352",
"modified_by": "Administrator",
"module": "HR",
"name": "Vehicle Log",
diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.json b/erpnext/hr/notification/training_scheduled/training_scheduled.json
index e49541e3214..f3650038fd6 100644
--- a/erpnext/hr/notification/training_scheduled/training_scheduled.json
+++ b/erpnext/hr/notification/training_scheduled/training_scheduled.json
@@ -11,8 +11,8 @@
"event": "Submit",
"idx": 0,
"is_standard": 1,
- "message": "
| \n | \n \n {{_(\"Training Event:\")}} {{ doc.event_name }}\n \n | \n \n |
| \n | \n \n {{ doc.introduction }}\n \n
| \n \n |
| \n | \n \n {{_(\"Training Event:\")}} {{ doc.event_name }}\n \n | \n \n |
| \n | \n \n {{ doc.introduction }}\n \n
| \n \n |
+ {% if data.value %} + + {{ __("Open BOM {0}", [data.value.bold()]) }} + {% endif %} + {% if data.item_code %} + + {{ __("Open Item {0}", [data.item_code.bold()]) }} + {% endif %} +
+diff --git a/erpnext/manufacturing/doctype/bom/bom_tree.js b/erpnext/manufacturing/doctype/bom/bom_tree.js index 185b9ed4bcf..60fb377f476 100644 --- a/erpnext/manufacturing/doctype/bom/bom_tree.js +++ b/erpnext/manufacturing/doctype/bom/bom_tree.js @@ -64,7 +64,7 @@ frappe.treeview_settings["BOM"] = { 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 e1cca9e3ef4..c89f7d66fdf 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -2,16 +2,17 @@ # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals +from collections import deque import unittest import frappe 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 six import string_types from erpnext.stock.doctype.item.test_item import make_item from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.tests.test_subcontracting import set_backflush_based_on test_records = frappe.get_test_records('BOM') @@ -122,7 +123,7 @@ class TestBOM(unittest.TestCase): bom.items[0].conversion_factor = 5 bom.insert() - bom.update_cost() + bom.update_cost(update_hour_rate = False) # test amounts in selected currency self.assertEqual(bom.items[0].rate, 300) @@ -160,6 +161,7 @@ class TestBOM(unittest.TestCase): def test_subcontractor_sourced_item(self): item_code = "_Test Subcontracted FG Item 1" + set_backflush_based_on('Material Transferred for Subcontract') if not frappe.db.exists('Item', item_code): make_item(item_code, { @@ -225,11 +227,119 @@ class TestBOM(unittest.TestCase): supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) self.assertEqual(bom_items, supplied_items) + def test_bom_tree_representation(self): + bom_tree = { + "Assembly": { + "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, + "SubAssembly2": {"ChildPart3": {}}, + "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}}, + "ChildPart5": {}, + "ChildPart6": {}, + "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + created_tree = parent_bom.get_tree_representation() + + reqd_order = level_order_traversal(bom_tree)[1:] # skip first item + created_order = created_tree.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, 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}) + + + +def level_order_traversal(node): + traversal = [] + q = deque() + q.append(node) + + while q: + node = q.popleft() + + for node_name, subtree in node.items(): + traversal.append(node_name) + q.append(subtree) + + return traversal + +def create_nested_bom(tree, prefix="_Test bom "): + """ Helper function to create a simple nested bom from tree describing item names. (along with required items) + """ + + def create_items(bom_tree): + for item_code, subtree in bom_tree.items(): + bom_item_code = prefix + item_code + if not frappe.db.exists("Item", bom_item_code): + frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert() + create_items(subtree) + create_items(tree) + + def dfs(tree, node): + """naive implementation for searching right subtree""" + for node_name, subtree in tree.items(): + if node_name == node: + return subtree + else: + result = dfs(subtree, node) + if result is not None: + return result + + order_of_creating_bom = reversed(level_order_traversal(tree)) + + for item in order_of_creating_bom: + child_items = dfs(tree, item) + if child_items: + bom_item_code = prefix + item + bom = frappe.get_doc(doctype="BOM", item=bom_item_code) + for child_item in child_items.keys(): + bom.append("items", {"item_code": prefix + child_item}) + bom.insert() + bom.submit() + + return bom # parent bom is last bom + + def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=None): - if warehouse_list and isinstance(warehouse_list, string_types): + if warehouse_list and isinstance(warehouse_list, str): warehouse_list = [warehouse_list] if not warehouse_list: diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 07464e3e766..4458e6db234 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -13,10 +13,10 @@ "col_break1", "hour_rate", "time_in_mins", - "batch_size", "operating_cost", "base_hour_rate", "base_operating_cost", + "batch_size", "image" ], "fields": [ @@ -61,6 +61,8 @@ }, { "description": "In minutes", + "fetch_from": "operation.total_operation_time", + "fetch_if_empty": 1, "fieldname": "time_in_mins", "fieldtype": "Float", "in_list_view": 1, @@ -104,7 +106,8 @@ "label": "Image" }, { - "default": "1", + "fetch_from": "operation.batch_size", + "fetch_if_empty": 1, "fieldname": "batch_size", "fieldtype": "Int", "label": "Batch Size" @@ -120,7 +123,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-13 18:14:10.018774", + "modified": "2021-01-12 14:48:09.596843", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 4e8dd41022b..81860c9fbcf 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -11,6 +11,16 @@ frappe.ui.form.on('Job Card', { } }; }); + + frm.set_indicator_formatter('sub_operation', + function(doc) { + if (doc.status == "Pending") { + return "red"; + } else { + return doc.status === "Complete" ? "green" : "orange"; + } + } + ); }, refresh: function(frm) { @@ -31,6 +41,10 @@ frappe.ui.form.on('Job Card', { } } + if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) { + frm.trigger('setup_corrective_job_card'); + } + frm.set_query("quality_inspection", function() { return { query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query", @@ -43,12 +57,62 @@ frappe.ui.form.on('Job Card', { frm.trigger("toggle_operation_number"); - if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) + if (frm.doc.docstatus == 0 && !frm.is_new() && + (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) && (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { frm.trigger("prepare_timer_buttons"); } }, + setup_corrective_job_card: function(frm) { + frm.add_custom_button(__('Corrective Job Card'), () => { + let operations = frm.doc.sub_operations.map(d => d.sub_operation).concat(frm.doc.operation); + + let fields = [ + { + fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation', + fieldname: 'operation', get_query() { + return { + filters: { + "is_corrective_operation": 1 + } + }; + } + }, { + fieldtype: 'Link', label: __('For Operation'), options: 'Operation', + fieldname: 'for_operation', get_query() { + return { + filters: { + "name": ["in", operations] + } + }; + } + } + ]; + + frappe.prompt(fields, d => { + frm.events.make_corrective_job_card(frm, d.operation, d.for_operation); + }, __("Select Corrective Operation")); + }, __('Make')); + }, + + make_corrective_job_card: function(frm, operation, for_operation) { + frappe.call({ + method: 'erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card', + args: { + source_name: frm.doc.name, + operation: operation, + for_operation: for_operation + }, + callback: function(r) { + if (r.message) { + frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + } + }); + }, + operation: function(frm) { frm.trigger("toggle_operation_number"); @@ -97,101 +161,105 @@ frappe.ui.form.on('Job Card', { prepare_timer_buttons: function(frm) { frm.trigger("make_dashboard"); - if (!frm.doc.job_started) { - frm.add_custom_button(__("Start"), () => { - if (!frm.doc.employee) { - frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee", - fieldname: 'employee'}, d => { - if (d.employee) { - frm.set_value("employee", d.employee); - } else { - frm.events.start_job(frm); - } - }, __("Enter Value"), __("Start")); + + if (!frm.doc.started_time && !frm.doc.current_time) { + frm.add_custom_button(__("Start Job"), () => { + if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) { + frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'), + options: "Job Card Time Log", fieldname: 'employees'}, d => { + frm.events.start_job(frm, "Work In Progress", d.employees); + }, __("Assign Job to Employee")); } else { - frm.events.start_job(frm); + frm.events.start_job(frm, "Work In Progress", frm.doc.employee); } }).addClass("btn-primary"); } else if (frm.doc.status == "On Hold") { - frm.add_custom_button(__("Resume"), () => { - frappe.flags.resume_job = 1; - frm.events.start_job(frm); + frm.add_custom_button(__("Resume Job"), () => { + frm.events.start_job(frm, "Resume Job", frm.doc.employee); }).addClass("btn-primary"); } else { - frm.add_custom_button(__("Pause"), () => { - frappe.flags.pause_job = 1; - frm.set_value("status", "On Hold"); - frm.events.complete_job(frm); + frm.add_custom_button(__("Pause Job"), () => { + frm.events.complete_job(frm, "On Hold"); }); - frm.add_custom_button(__("Complete"), () => { - let completed_time = frappe.datetime.now_datetime(); - frm.trigger("hide_timer"); + frm.add_custom_button(__("Complete Job"), () => { + var sub_operations = frm.doc.sub_operations; - if (frm.doc.for_quantity) { + let set_qty = true; + if (sub_operations && sub_operations.length > 1) { + set_qty = false; + let last_op_row = sub_operations[sub_operations.length - 2]; + + if (last_op_row.status == 'Complete') { + set_qty = true; + } + } + + if (set_qty) { frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'), - fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => { - frm.events.complete_job(frm, completed_time, data.qty); - }, __("Enter Value"), __("Complete")); + fieldname: 'qty', default: frm.doc.for_quantity}, data => { + frm.events.complete_job(frm, "Complete", data.qty); + }, __("Enter Value")); } else { - frm.events.complete_job(frm, completed_time, 0); + frm.events.complete_job(frm, "Complete", 0.0); } }).addClass("btn-primary"); } }, - start_job: function(frm) { - let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs'); - row.from_time = frappe.datetime.now_datetime(); - frm.set_value('job_started', 1); - frm.set_value('started_time' , row.from_time); - frm.set_value("status", "Work In Progress"); - - if (!frappe.flags.resume_job) { - frm.set_value('current_time' , 0); - } - - frm.save(); + start_job: function(frm, status, employee) { + const args = { + job_card_id: frm.doc.name, + start_time: frappe.datetime.now_datetime(), + employees: employee, + status: status + }; + frm.events.make_time_log(frm, args); }, - complete_job: function(frm, completed_time, completed_qty) { - frm.doc.time_logs.forEach(d => { - if (d.from_time && !d.to_time) { - d.to_time = completed_time || frappe.datetime.now_datetime(); - d.completed_qty = completed_qty || 0; + complete_job: function(frm, status, completed_qty) { + const args = { + job_card_id: frm.doc.name, + complete_time: frappe.datetime.now_datetime(), + status: status, + completed_qty: completed_qty + }; + frm.events.make_time_log(frm, args); + }, - if(frappe.flags.pause_job) { - let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0; - frm.set_value('current_time' , currentIncrement + (frm.doc.current_time || 0)); - } else { - frm.set_value('started_time' , ''); - frm.set_value('job_started', 0); - frm.set_value('current_time' , 0); - } + make_time_log: function(frm, args) { + frm.events.update_sub_operation(frm, args); - frm.save(); + frappe.call({ + method: "erpnext.manufacturing.doctype.job_card.job_card.make_time_log", + args: { + args: args + }, + freeze: true, + callback: function () { + frm.reload_doc(); + frm.trigger("make_dashboard"); } }); }, + update_sub_operation: function(frm, args) { + if (frm.doc.sub_operations && frm.doc.sub_operations.length) { + let sub_operations = frm.doc.sub_operations.filter(d => d.status != 'Complete'); + if (sub_operations && sub_operations.length) { + args["sub_operation"] = sub_operations[0].sub_operation; + } + } + }, + validate: function(frm) { if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) { frm.trigger("reset_timer"); } }, - employee: function(frm) { - if (frm.doc.job_started && !frm.doc.current_time) { - frm.trigger("reset_timer"); - } else { - frm.events.start_job(frm); - } - }, - reset_timer: function(frm) { frm.set_value('started_time' , ''); - frm.set_value('job_started', 0); - frm.set_value('current_time' , 0); }, make_dashboard: function(frm) { @@ -297,7 +365,6 @@ frappe.ui.form.on('Job Card Time Log', { }, to_time: function(frm) { - frm.set_value('job_started', 0); frm.set_value('started_time', ''); } }) \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 5713f697e99..046e2fd1825 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -9,38 +9,49 @@ "naming_series", "work_order", "bom_no", - "workstation", - "operation", - "operation_row_number", "column_break_4", "posting_date", "company", - "remarks", "production_section", "production_item", "item_name", "for_quantity", - "quality_inspection", - "wip_warehouse", + "serial_no", "column_break_12", - "employee", - "employee_name", - "status", + "wip_warehouse", + "quality_inspection", "project", + "batch_no", + "operation_section_section", + "operation", + "operation_row_number", + "column_break_18", + "workstation", + "employee", + "section_break_21", + "sub_operations", "timing_detail", "time_logs", "section_break_13", "total_completed_qty", - "total_time_in_mins", "column_break_15", + "total_time_in_mins", "section_break_8", "items", + "corrective_operation_section", + "for_job_card", + "is_corrective_job_card", + "column_break_33", + "hour_rate", + "for_operation", "more_information", "operation_id", "sequence_id", "transferred_qty", "requested_qty", + "status", "column_break_20", + "remarks", "barcode", "job_started", "started_time", @@ -117,13 +128,6 @@ "fieldtype": "Section Break", "label": "Timing Detail" }, - { - "fieldname": "employee", - "fieldtype": "Link", - "in_standard_filter": 1, - "label": "Employee", - "options": "Employee" - }, { "allow_bulk_edit": 1, "fieldname": "time_logs", @@ -133,9 +137,11 @@ }, { "fieldname": "section_break_13", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_border": 1 }, { + "default": "0", "fieldname": "total_completed_qty", "fieldtype": "Float", "label": "Total Completed Qty", @@ -160,8 +166,7 @@ "fieldname": "items", "fieldtype": "Table", "label": "Items", - "options": "Job Card Item", - "read_only": 1 + "options": "Job Card Item" }, { "collapsible": 1, @@ -251,12 +256,7 @@ "reqd": 1 }, { - "fetch_from": "employee.employee_name", - "fieldname": "employee_name", - "fieldtype": "Read Only", - "label": "Employee Name" - }, - { + "collapsible": 1, "fieldname": "production_section", "fieldtype": "Section Break", "label": "Production" @@ -314,11 +314,89 @@ "label": "Quality Inspection", "no_copy": 1, "options": "Quality Inspection" + }, + { + "allow_bulk_edit": 1, + "fieldname": "sub_operations", + "fieldtype": "Table", + "label": "Sub Operations", + "options": "Job Card Operation", + "read_only": 1 + }, + { + "fieldname": "operation_section_section", + "fieldtype": "Section Break", + "label": "Operation Section" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_21", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "depends_on": "is_corrective_job_card", + "fieldname": "hour_rate", + "fieldtype": "Currency", + "label": "Hour Rate" + }, + { + "collapsible": 1, + "depends_on": "is_corrective_job_card", + "fieldname": "corrective_operation_section", + "fieldtype": "Section Break", + "label": "Corrective Operation" + }, + { + "default": "0", + "fieldname": "is_corrective_job_card", + "fieldtype": "Check", + "label": "Is Corrective Job Card", + "read_only": 1 + }, + { + "fieldname": "column_break_33", + "fieldtype": "Column Break" + }, + { + "fieldname": "for_job_card", + "fieldtype": "Link", + "label": "For Job Card", + "options": "Job Card", + "read_only": 1 + }, + { + "fetch_from": "for_job_card.operation", + "fetch_if_empty": 1, + "fieldname": "for_operation", + "fieldtype": "Link", + "label": "For Operation", + "options": "Operation" + }, + { + "fieldname": "employee", + "fieldtype": "Table MultiSelect", + "label": "Employee", + "options": "Job Card Time Log" + }, + { + "fieldname": "serial_no", + "fieldtype": "Small Text", + "label": "Serial No" + }, + { + "fieldname": "batch_no", + "fieldtype": "Link", + "label": "Batch No", + "options": "Batch" } ], "is_submittable": 1, "links": [], - "modified": "2020-11-19 18:26:50.531664", + "modified": "2021-03-16 15:59:32.766484", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index cdc45188942..69c7f5c614b 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -5,11 +5,12 @@ from __future__ import unicode_literals import frappe import datetime +import json from frappe import _, bold from frappe.model.mapper import get_mapped_doc from frappe.model.document import Document from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate, - get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form) + get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form, time_diff_in_seconds) from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations @@ -25,10 +26,21 @@ class JobCard(Document): self.set_status() self.validate_operation_id() self.validate_sequence_id() + self.get_sub_operations() + self.update_sub_operation_status() + + def get_sub_operations(self): + if self.operation: + self.sub_operations = [] + for row in frappe.get_all("Sub Operation", + filters = {"parent": self.operation}, fields=["operation", "idx"]): + row.status = "Pending" + row.sub_operation = row.operation + self.append("sub_operations", row) def validate_time_logs(self): - self.total_completed_qty = 0.0 self.total_time_in_mins = 0.0 + self.total_completed_qty = 0.0 if self.get('time_logs'): for d in self.get('time_logs'): @@ -44,11 +56,14 @@ class JobCard(Document): d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60 self.total_time_in_mins += d.time_in_mins - if d.completed_qty: + if d.completed_qty and not self.sub_operations: self.total_completed_qty += d.completed_qty self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty")) + for row in self.sub_operations: + self.total_completed_qty += row.completed_qty + def get_overlap_for(self, args, check_next_available_slot=False): production_capacity = 1 @@ -57,7 +72,7 @@ class JobCard(Document): self.workstation, 'production_capacity') or 1 validate_overlap_for = " and jc.workstation = %(workstation)s " - if self.employee: + if args.get("employee"): # override capacity for employee production_capacity = 1 validate_overlap_for = " and jc.employee = %(employee)s " @@ -80,7 +95,7 @@ class JobCard(Document): "to_time": args.to_time, "name": args.name or "No Name", "parent": args.parent or "No Name", - "employee": self.employee, + "employee": args.get("employee"), "workstation": self.workstation }, as_dict=True) @@ -158,6 +173,108 @@ class JobCard(Document): row.planned_start_time = datetime.datetime.combine(start_date, get_time(workstation_doc.working_hours[0].start_time)) + def add_time_log(self, args): + last_row = [] + employees = args.employees + if isinstance(employees, str): + employees = json.loads(employees) + + if self.time_logs and len(self.time_logs) > 0: + last_row = self.time_logs[-1] + + self.reset_timer_value(args) + if last_row and args.get("complete_time"): + for row in self.time_logs: + if not row.to_time: + row.update({ + "to_time": get_datetime(args.get("complete_time")), + "operation": args.get("sub_operation"), + "completed_qty": args.get("completed_qty") or 0.0 + }) + elif args.get("start_time"): + new_args = frappe._dict({ + "from_time": get_datetime(args.get("start_time")), + "operation": args.get("sub_operation"), + "completed_qty": 0.0 + }) + + 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": + self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time) + + 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', { + 'employee': name.get('employee'), + 'completed_qty': 0.0 + }) + + def reset_timer_value(self, args): + self.started_time = None + + if args.get("status") in ["Work In Progress", "Complete"]: + self.current_time = 0.0 + + if args.get("status") == "Work In Progress": + self.started_time = get_datetime(args.get("start_time")) + + if args.get("status") == "Resume Job": + args["status"] = "Work In Progress" + + if args.get("status"): + self.status = args.get("status") + + def update_sub_operation_status(self): + if not (self.sub_operations and self.time_logs): + return + + operation_wise_completed_time = {} + for time_log in self.time_logs: + if time_log.operation not in operation_wise_completed_time: + operation_wise_completed_time.setdefault(time_log.operation, + frappe._dict({"status": "Pending", "completed_qty":0.0, "completed_time": 0.0, "employee": []})) + + op_row = operation_wise_completed_time[time_log.operation] + op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete" + if self.status == 'On Hold': + op_row.status = 'Pause' + + op_row.employee.append(time_log.employee) + if time_log.time_in_mins: + op_row.completed_time += time_log.time_in_mins + op_row.completed_qty += time_log.completed_qty + + for row in self.sub_operations: + operation_deatils = operation_wise_completed_time.get(row.sub_operation) + if operation_deatils: + if row.status != 'Complete': + row.status = operation_deatils.status + + row.completed_time = operation_deatils.completed_time + if operation_deatils.employee: + row.completed_time = row.completed_time / len(set(operation_deatils.employee)) + + if operation_deatils.completed_qty: + row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee)) + else: + row.status = 'Pending' + row.completed_time = 0.0 + row.completed_qty = 0.0 + def update_time_logs(self, row): self.append("time_logs", { "from_time": row.planned_start_time, @@ -182,15 +299,18 @@ class JobCard(Document): if self.get('operation') == d.operation: self.append('items', { - 'item_code': d.item_code, - 'source_warehouse': d.source_warehouse, - 'uom': frappe.db.get_value("Item", d.item_code, 'stock_uom'), - 'item_name': d.item_name, - 'description': d.description, - 'required_qty': (d.required_qty * flt(self.for_quantity)) / doc.qty + "item_code": d.item_code, + "source_warehouse": d.source_warehouse, + "uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'), + "item_name": d.item_name, + "description": d.description, + "required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty, + "rate": d.rate, + "amount": d.amount }) def on_submit(self): + self.validate_transfer_qty() self.validate_job_card() self.update_work_order() self.set_transferred_qty() @@ -199,7 +319,16 @@ class JobCard(Document): self.update_work_order() self.set_transferred_qty() + def validate_transfer_qty(self): + if self.items and self.transferred_qty < self.for_quantity: + frappe.throw(_('Materials needs to be transferred to the work in progress warehouse for the job card {0}') + .format(self.name)) + def validate_job_card(self): + if self.work_order and frappe.get_cached_value('Work Order', self.work_order, 'status') == 'Stopped': + frappe.throw(_("Transaction not allowed against stopped Work Order {0}") + .format(get_link_to_form('Work Order', self.work_order))) + if not self.time_logs: frappe.throw(_("Time logs are required for {0} {1}") .format(bold("Job Card"), get_link_to_form("Job Card", self.name))) @@ -215,6 +344,10 @@ class JobCard(Document): if not self.work_order: return + if self.is_corrective_job_card and not cint(frappe.db.get_single_value('Manufacturing Settings', + 'add_corrective_operation_cost_in_finished_good_valuation')): + return + for_quantity, time_in_mins = 0, 0 from_time_list, to_time_list = [], [] @@ -225,10 +358,24 @@ class JobCard(Document): time_in_mins = flt(data[0].time_in_mins) wo = frappe.get_doc('Work Order', self.work_order) - if self.operation_id: + + if self.is_corrective_job_card: + self.update_corrective_in_work_order(wo) + + elif self.operation_id: self.validate_produced_quantity(for_quantity, wo) self.update_work_order_data(for_quantity, time_in_mins, wo) + def update_corrective_in_work_order(self, wo): + wo.corrective_operation_cost = 0.0 + for row in frappe.get_all('Job Card', fields = ['total_time_in_mins', 'hour_rate'], + filters = {'is_corrective_job_card': 1, 'docstatus': 1, 'work_order': self.work_order}): + wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate) + + wo.calculate_operating_cost() + wo.flags.ignore_validate_update_after_submit = True + wo.save() + def validate_produced_quantity(self, for_quantity, wo): if self.docstatus < 2: return @@ -248,8 +395,8 @@ class JobCard(Document): min(from_time) as start_time, max(to_time) as end_time FROM `tabJob Card` jc, `tabJob Card Time Log` jctl WHERE - jctl.parent = jc.name and jc.work_order = %s - and jc.operation_id = %s and jc.docstatus = 1 + jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s + and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0 """, (self.work_order, self.operation_id), as_dict=1) for data in wo.operations: @@ -271,7 +418,8 @@ class JobCard(Document): def get_current_operation_data(self): return frappe.get_all('Job Card', fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], - filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id}) + filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id, + "is_corrective_job_card": 0}) def set_transferred_qty_in_job_card(self, ste_doc): for row in ste_doc.items: @@ -354,7 +502,11 @@ class JobCard(Document): .format(bold(self.operation), work_order), OperationMismatchError) def validate_sequence_id(self): - if not (self.work_order and self.sequence_id): return + if self.is_corrective_job_card: + return + + if not (self.work_order and self.sequence_id): + return current_operation_qty = 0.0 data = self.get_current_operation_data() @@ -376,6 +528,17 @@ class JobCard(Document): frappe.throw(_("{0}, complete the operation {1} before the operation {2}.") .format(message, bold(row.operation), bold(self.operation)), OperationSequenceError) + +@frappe.whitelist() +def make_time_log(args): + if isinstance(args, str): + args = json.loads(args) + + args = frappe._dict(args) + doc = frappe.get_doc("Job Card", args.job_card_id) + doc.validate_sequence_id() + doc.add_time_log(args) + @frappe.whitelist() def get_operation_details(work_order, operation): if work_order and operation: @@ -511,3 +674,28 @@ def get_job_details(start, end, filters=None): events.append(job_card_data) return events + +@frappe.whitelist() +def make_corrective_job_card(source_name, operation=None, for_operation=None, target_doc=None): + def set_missing_values(source, target): + target.is_corrective_job_card = 1 + target.operation = operation + target.for_operation = for_operation + + target.set('time_logs', []) + target.set('employee', []) + target.set('items', []) + target.get_sub_operations() + target.get_required_items() + target.validate_time_logs() + + doclist = get_mapped_doc("Job Card", source_name, { + "Job Card": { + "doctype": "Job Card", + "field_map": { + "name": "for_job_card", + }, + } + }, target_doc, set_missing_values) + + return doclist \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json index 100ef4ca3a3..d91530dd3b5 100644 --- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json +++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json @@ -25,8 +25,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Item Code", - "options": "Item", - "read_only": 1 + "options": "Item" }, { "fieldname": "source_warehouse", @@ -67,8 +66,7 @@ "fieldname": "required_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Required Qty", - "read_only": 1 + "label": "Required Qty" }, { "fieldname": "column_break_9", @@ -107,7 +105,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-02-11 13:50:13.804108", + "modified": "2021-04-22 18:50:00.003444", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card Item", diff --git a/erpnext/manufacturing/doctype/job_card_operation/__init__.py b/erpnext/manufacturing/doctype/job_card_operation/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json new file mode 100644 index 00000000000..9a8692b84d9 --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json @@ -0,0 +1,59 @@ +{ + "actions": [], + "creation": "2020-12-07 16:58:38.449041", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "sub_operation", + "completed_time", + "status", + "completed_qty" + ], + "fields": [ + { + "default": "Pending", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Complete\nPause\nPending\nWork In Progress", + "read_only": 1 + }, + { + "description": "In mins", + "fieldname": "completed_time", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Completed Time", + "read_only": 1 + }, + { + "fieldname": "sub_operation", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Operation", + "options": "Operation", + "read_only": 1 + }, + { + "fieldname": "completed_qty", + "fieldtype": "Float", + "label": "Completed Qty", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-03-16 18:24:35.399593", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Job Card Operation", + "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/job_card_operation/job_card_operation.py b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py new file mode 100644 index 00000000000..85d72982ed3 --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.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 JobCardOperation(Document): + pass diff --git a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json index 9dd54dd6182..a7102d7d237 100644 --- a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json +++ b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json @@ -1,14 +1,17 @@ { + "actions": [], "creation": "2019-03-08 23:56:43.187569", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "employee", "from_time", "to_time", "column_break_2", "time_in_mins", - "completed_qty" + "completed_qty", + "operation" ], "fields": [ { @@ -41,10 +44,27 @@ "in_list_view": 1, "label": "Completed Qty", "reqd": 1 + }, + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee" + }, + { + "fieldname": "operation", + "fieldtype": "Link", + "label": "Operation", + "no_copy": 1, + "options": "Operation", + "read_only": 1 } ], + "index_web_pages_for_search": 1, "istable": 1, - "modified": "2019-12-03 12:56:02.285448", + "links": [], + "modified": "2020-12-23 14:30:00.970916", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card Time Log", diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index b7634da87c2..024f7847259 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -26,7 +26,10 @@ "column_break_16", "overproduction_percentage_for_work_order", "other_settings_section", - "update_bom_costs_automatically" + "update_bom_costs_automatically", + "add_corrective_operation_cost_in_finished_good_valuation", + "column_break_23", + "make_serial_no_batch_from_work_order" ], "fields": [ { @@ -155,13 +158,30 @@ { "fieldname": "column_break_5", "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_23", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "System will automatically create the serial numbers / batch for the Finished Good on submission of work order", + "fieldname": "make_serial_no_batch_from_work_order", + "fieldtype": "Check", + "label": "Make Serial No / Batch from Work Order" + }, + { + "default": "0", + "fieldname": "add_corrective_operation_cost_in_finished_good_valuation", + "fieldtype": "Check", + "label": "Add Corrective Operation Cost in Finished Good Valuation" } ], "icon": "icon-wrench", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-10-13 10:55:43.996581", + "modified": "2021-03-16 15:54:38.967341", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", @@ -178,4 +198,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/operation/operation.js b/erpnext/manufacturing/doctype/operation/operation.js index 5c2aba6f095..102b6780e5f 100644 --- a/erpnext/manufacturing/doctype/operation/operation.js +++ b/erpnext/manufacturing/doctype/operation/operation.js @@ -2,7 +2,13 @@ // For license information, please see license.txt frappe.ui.form.on('Operation', { - refresh: function(frm) { - + setup: function(frm) { + frm.set_query('operation', 'sub_operations', function() { + return { + filters: { + 'name': ['not in', [frm.doc.name]] + } + }; + }); } -}); +}); \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/operation/operation.json b/erpnext/manufacturing/doctype/operation/operation.json index c231fba2faa..10a97eda763 100644 --- a/erpnext/manufacturing/doctype/operation/operation.json +++ b/erpnext/manufacturing/doctype/operation/operation.json @@ -1,167 +1,132 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "Prompt", - "beta": 0, - "creation": "2014-11-07 16:20:30.683186", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2014-11-07 16:20:30.683186", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "workstation", + "data_2", + "is_corrective_operation", + "job_card_section", + "create_job_card_based_on_batch_size", + "column_break_6", + "batch_size", + "sub_operations_section", + "sub_operations", + "total_operation_time", + "section_break_4", + "description" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "workstation", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Default Workstation", - "length": 0, - "no_copy": 0, - "options": "Workstation", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "workstation", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Default Workstation", + "options": "Workstation" + }, { - "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_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "collapsible": 1, + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "label": "Operation Description" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" + }, + { + "collapsible": 1, + "fieldname": "sub_operations_section", + "fieldtype": "Section Break", + "label": "Sub Operations" + }, + { + "fieldname": "sub_operations", + "fieldtype": "Table", + "options": "Sub Operation" + }, + { + "description": "Time in mins.", + "fieldname": "total_operation_time", + "fieldtype": "Float", + "label": "Total Operation Time", + "read_only": 1 + }, + { + "fieldname": "data_2", + "fieldtype": "Column Break" + }, + { + "default": "1", + "depends_on": "create_job_card_based_on_batch_size", + "fieldname": "batch_size", + "fieldtype": "Int", + "label": "Batch Size", + "mandatory_depends_on": "create_job_card_based_on_batch_size" + }, + { + "default": "0", + "fieldname": "create_job_card_based_on_batch_size", + "fieldtype": "Check", + "label": "Create Job Card based on Batch Size" + }, + { + "collapsible": 1, + "fieldname": "job_card_section", + "fieldtype": "Section Break", + "label": "Job Card" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "is_corrective_operation", + "fieldtype": "Check", + "label": "Is Corrective Operation" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-wrench", - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-11-07 05:28:27.462413", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Operation", - "name_case": "", - "owner": "Administrator", + ], + "icon": "fa fa-wrench", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-01-12 15:09:23.593338", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Operation", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 0, - "export": 1, - "if_owner": 0, - "import": 1, - "is_custom": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Manufacturing User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "export": 1, + "import": 1, + "read": 1, + "role": "Manufacturing User", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 0, - "export": 1, - "if_owner": 0, - "import": 1, - "is_custom": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 1, - "role": "Manufacturing Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "export": 1, + "import": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/operation/operation.py b/erpnext/manufacturing/doctype/operation/operation.py index 69e83292ff8..374f32019bd 100644 --- a/erpnext/manufacturing/doctype/operation/operation.py +++ b/erpnext/manufacturing/doctype/operation/operation.py @@ -2,9 +2,34 @@ # For license information, please see license.txt from __future__ import unicode_literals + +import frappe +from frappe import _ from frappe.model.document import Document class Operation(Document): def validate(self): if not self.description: self.description = self.name + + self.duplicate_sub_operation() + self.set_total_time() + + def duplicate_sub_operation(self): + operation_list = [] + for row in self.sub_operations: + if row.operation in operation_list: + frappe.throw(_("The operation {0} can not add multiple times") + .format(frappe.bold(row.operation))) + + if self.name == row.operation: + frappe.throw(_("The operation {0} can not be the sub operation") + .format(frappe.bold(row.operation))) + + operation_list.append(row.operation) + + def set_total_time(self): + self.total_operation_time = 0.0 + + for row in self.sub_operations: + self.total_operation_time += row.time_in_mins diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 288c1d0cd66..d198a6962a5 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 @@ frappe.ui.form.on('Production Plan', { 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 @@ frappe.ui.form.on('Production Plan', { 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')); + } } } @@ -211,16 +219,38 @@ frappe.ui.form.on('Production Plan', { }); }, - get_items: function(frm) { + get_items: function (frm) { + frm.clear_table('prod_plan_references'); + frappe.call({ method: "get_items", freeze: true, doc: frm.doc, - callback: function() { + callback: function () { refresh_field('po_items'); } }); }, + combine_items: function (frm) { + frm.clear_table('prod_plan_references'); + + frappe.call({ + method: "get_items", + freeze: true, + doc: frm.doc, + }); + }, + + 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) { @@ -295,8 +325,25 @@ frappe.ui.form.on('Production Plan', { }, download_materials_required: function(frm) { - let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; - open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc }); + const fields = [{ + fieldname: 'warehouses', + fieldtype: 'Table MultiSelect', + label: __('Warehouses'), + default: frm.doc.from_warehouse, + options: "Production Plan Material Request Warehouse", + get_query: function () { + return { + filters: { + company: frm.doc.company + } + }; + }, + }]; + + frappe.prompt(fields, (row) => { + let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; + open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc, warehouses: row.warehouses }); + }, __('Select Warehouses to get Stock for Materials Planning'), __('Get Stock')); }, show_progress: function(frm) { diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index f11470086af..84378956c61 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -28,7 +28,13 @@ "material_requests", "select_items_to_manufacture_section", "get_items", + "combine_items", "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", @@ -184,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", @@ -196,7 +202,7 @@ { "fieldname": "material_request_planning", "fieldtype": "Section Break", - "label": "Material Request Planning" + "label": "Material Requirement Planning" }, { "default": "1", @@ -234,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" }, @@ -316,13 +323,48 @@ "fieldname": "include_safety_stock", "fieldtype": "Check", "label": "Include Safety Stock in Required Qty Calculation" + }, + { + "default": "0", + "depends_on": "eval:doc.get_items_from == 'Sales Order'", + "fieldname": "combine_items", + "fieldtype": "Check", + "label": "Consolidate Items" + }, + { + "fieldname": "section_break_25", + "fieldtype": "Section Break" + }, + { + "fieldname": "prod_plan_references", + "fieldtype": "Table", + "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-03-08 11:17:25.470147", + "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 a3e23a68972..6a024f275aa 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 @@ -96,8 +97,10 @@ class ProductionPlan(Document): @frappe.whitelist() def get_items(self): + self.set('po_items', []) if self.get_items_from == "Sales Order": self.get_so_items() + elif self.get_items_from == "Material Request": self.get_mr_items() @@ -165,9 +168,31 @@ class ProductionPlan(Document): self.calculate_total_planned_qty() def add_items(self, items): - self.set('po_items', []) + refs = {} for data in items: item_details = get_item_details(data.item_code) + if self.combine_items: + if item_details.bom_no in refs: + refs[item_details.bom_no]['so_details'].append({ + 'sales_order': data.parent, + 'sales_order_item': data.name, + 'qty': data.pending_qty + }) + refs[item_details.bom_no]['qty'] += data.pending_qty + continue + + else: + refs[item_details.bom_no] = { + 'qty': data.pending_qty, + 'po_item_ref': data.name, + 'so_details': [] + } + refs[item_details.bom_no]['so_details'].append({ + 'sales_order': data.parent, + 'sales_order_item': data.name, + 'qty': data.pending_qty + }) + pi = self.append('po_items', { 'include_exploded_items': 1, 'warehouse': data.warehouse, @@ -191,6 +216,23 @@ class ProductionPlan(Document): pi.material_request_item = data.name pi.description = data.description + if refs: + for po_item in self.po_items: + po_item.planned_qty = refs[po_item.bom_no]['qty'] + po_item.pending_qty = refs[po_item.bom_no]['qty'] + po_item.sales_order = '' + self.add_pp_ref(refs) + + def add_pp_ref(self, refs): + for bom_no in refs: + for so_detail in refs[bom_no]['so_details']: + self.append('prod_plan_references', { + 'item_reference': refs[bom_no]['po_item_ref'], + 'sales_order': so_detail['sales_order'], + 'sales_order_item': so_detail['sales_order_item'], + 'qty': so_detail['qty'] + }) + def calculate_total_planned_qty(self): self.total_planned_qty = 0 for d in self.po_items: @@ -308,49 +350,88 @@ class ProductionPlan(Document): @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 = ["""%s""" % \ - (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 @@ -435,19 +516,43 @@ class ProductionPlan(Document): 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): - if isinstance(doc, string_types): +def download_raw_materials(doc, warehouses=None): + if isinstance(doc, str): doc = frappe._dict(json.loads(doc)) item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', - 'Projected Qty', 'Actual Qty', 'Ordered Qty', 'Reserved Qty for Production', - 'Safety Stock', 'Required Qty']] + 'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned Qty', + 'Reserved Qty for Production', 'Safety Stock', 'Required Qty']] - for d in get_items_for_material_requests(doc): + doc.warehouse = None + for d in get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True): item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'), d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'), - d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) + d.get('planned_qty'), d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) if not doc.get('for_warehouse'): row = {'item_code': d.get('item_code')} @@ -466,7 +571,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name, bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse, item.default_material_request_type, item.min_order_qty, item_default.default_warehouse, - item.purchase_uom, item_uom.conversion_factor + item.purchase_uom, item_uom.conversion_factor, item.safety_stock from `tabBOM Explosion Item` bei JOIN `tabBOM` bom ON bom.name = bei.parent @@ -618,7 +723,7 @@ def get_sales_orders(self): @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) @@ -636,36 +741,38 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty, ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty, - ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse from `tabBin` - where item_code = %(item_code)s {conditions} + ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse, + ifnull(sum(planned_qty),0) as planned_qty + from `tabBin` where item_code = %(item_code)s {conditions} group by item_code, warehouse """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) +def get_warehouse_list(warehouses): + warehouse_list = [] + + if isinstance(warehouses, str): + warehouses = json.loads(warehouses) + + for row in warehouses: + child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse")) + if child_warehouses: + warehouse_list.extend(child_warehouses) + else: + warehouse_list.append(row.get("warehouse")) + + return warehouse_list + @frappe.whitelist() -def get_items_for_material_requests(doc, warehouses=None): - if isinstance(doc, string_types): +def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None): + if isinstance(doc, str): doc = frappe._dict(json.loads(doc)) - warehouse_list = [] if warehouses: - if isinstance(warehouses, string_types): - warehouses = json.loads(warehouses) + warehouses = list(set(get_warehouse_list(warehouses))) - for row in warehouses: - child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse")) - if child_warehouses: - warehouse_list.extend(child_warehouses) - else: - warehouse_list.append(row.get("warehouse")) - - if warehouse_list: - warehouses = list(set(warehouse_list)) - - if doc.get("for_warehouse") and doc.get("for_warehouse") in warehouses: + if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses: warehouses.remove(doc.get("for_warehouse")) - warehouse_list = None - doc['mr_items'] = [] po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items') @@ -680,6 +787,9 @@ def get_items_for_material_requests(doc, warehouses=None): 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') @@ -754,7 +864,7 @@ def get_items_for_material_requests(doc, warehouses=None): if items: mr_items.append(items) - if not ignore_existing_ordered_qty and warehouses: + if (not ignore_existing_ordered_qty or get_parent_warehouse_data) and warehouses: new_mr_items = [] for item in mr_items: get_materials_from_other_locations(item, warehouses, new_mr_items, company) @@ -811,23 +921,28 @@ def get_item_data(item_code): # "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 09ec24a67a2..ca597f63278 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 @@ def get_data(): '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 27335aa204b..93e6d7a97f4 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.stock.doctype.item.test_item import create_item 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): @@ -100,7 +100,7 @@ class TestProductionPlan(unittest.TestCase): def test_production_plan_sales_orders(self): item = 'Test Production Item 1' - so = make_sales_order(item_code=item, qty=5) + so = make_sales_order(item_code=item, qty=1) sales_order = so.name sales_order_item = so.items[0].name @@ -124,8 +124,8 @@ class TestProductionPlan(unittest.TestCase): wo_doc = frappe.get_doc('Work Order', work_order) wo_doc.update({ - 'wip_warehouse': '_Test Warehouse 1 - _TC', - 'fg_warehouse': '_Test Warehouse - _TC' + 'wip_warehouse': 'Work In Progress - _TC', + 'fg_warehouse': 'Finished Goods - _TC' }) wo_doc.submit() @@ -145,6 +145,58 @@ class TestProductionPlan(unittest.TestCase): self.assertEqual(sales_orders, []) + def test_production_plan_combine_items(self): + item = 'Test Production Item 1' + so = make_sales_order(item_code=item, qty=1) + + pln = frappe.new_doc('Production Plan') + pln.company = so.company + pln.get_items_from = 'Sales Order' + pln.append('sales_orders', { + 'sales_order': so.name, + 'sales_order_date': so.transaction_date, + 'customer': so.customer, + 'grand_total': so.grand_total + }) + so = make_sales_order(item_code=item, qty=2) + pln.append('sales_orders', { + 'sales_order': so.name, + 'sales_order_date': so.transaction_date, + 'customer': so.customer, + 'grand_total': so.grand_total + }) + pln.combine_items = 1 + pln.get_items() + pln.submit() + + self.assertTrue(pln.po_items[0].planned_qty, 3) + + pln.make_work_order() + work_order = frappe.db.get_value('Work Order', { + 'production_plan_item': pln.po_items[0].name, + 'production_plan': pln.name + }, 'name') + + wo_doc = frappe.get_doc('Work Order', work_order) + wo_doc.update({ + 'wip_warehouse': 'Work In Progress - _TC', + }) + + wo_doc.submit() + so_items = [] + for plan_reference in pln.prod_plan_references: + so_items.append(plan_reference.sales_order_item) + so_wo_qty = frappe.db.get_value('Sales Order Item', plan_reference.sales_order_item, 'work_order_qty') + self.assertEqual(so_wo_qty, plan_reference.qty) + + wo_doc.cancel() + 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) @@ -184,10 +236,10 @@ class TestProductionPlan(unittest.TestCase): 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() @@ -199,6 +251,27 @@ class TestProductionPlan(unittest.TestCase): 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 d0dce53437b..f829d57475a 100644 --- a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json +++ b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json @@ -1,792 +1,220 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "hash", - "beta": 0, - "creation": "2013-02-22 01:27:49", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 1, + "actions": [], + "autoname": "hash", + "creation": "2013-02-22 01:27:49", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "include_exploded_items", + "item_code", + "bom_no", + "column_break_6", + "planned_qty", + "warehouse", + "planned_start_date", + "section_break_9", + "pending_qty", + "ordered_qty", + "column_break_17", + "description", + "stock_uom", + "produced_qty", + "reference_section", + "sales_order", + "sales_order_item", + "column_break_19", + "material_request", + "material_request_item", + "product_bundle_item", + "item_reference" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fetch_if_empty": 0, - "fieldname": "include_exploded_items", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Include Exploded Items", - "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 - }, + "columns": 1, + "default": "1", + "fieldname": "include_exploded_items", + "fieldtype": "Check", + "label": "Include Exploded Items" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fetch_if_empty": 0, - "fieldname": "item_code", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Code", - "length": 0, - "no_copy": 0, - "oldfieldname": "item_code", - "oldfieldtype": "Link", - "options": "Item", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "150px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, + "columns": 2, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "oldfieldname": "item_code", + "oldfieldtype": "Link", + "options": "Item", + "print_width": "150px", + "reqd": 1, "width": "150px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fetch_if_empty": 0, - "fieldname": "bom_no", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "BOM No", - "length": 0, - "no_copy": 0, - "oldfieldname": "bom_no", - "oldfieldtype": "Link", - "options": "BOM", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, + "columns": 2, + "fieldname": "bom_no", + "fieldtype": "Link", + "in_list_view": 1, + "label": "BOM No", + "oldfieldname": "bom_no", + "oldfieldtype": "Link", + "options": "BOM", + "print_width": "100px", + "reqd": 1, "width": "100px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "planned_qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Planned Qty", - "length": 0, - "no_copy": 0, - "oldfieldname": "planned_qty", - "oldfieldtype": "Currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, + "fieldname": "planned_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Planned Qty", + "oldfieldname": "planned_qty", + "oldfieldtype": "Currency", + "print_width": "100px", + "reqd": 1, "width": "100px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_6", - "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_6", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "description": "If enabled, system will create the work order for the exploded items against which BOM is available.", - "fetch_if_empty": 0, - "fieldname": "make_work_order_for_sub_assembly_items", - "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": "Make Work Order for Sub Assembly Items", - "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": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "For Warehouse", + "options": "Warehouse" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fetch_if_empty": 0, - "fieldname": "warehouse", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "For Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "permlevel": 0, - "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": "Today", + "fieldname": "planned_start_date", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Planned Start Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fetch_if_empty": 0, - "fieldname": "planned_start_date", - "fieldtype": "Datetime", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Planned Start 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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Quantity and Description" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_9", - "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": "Quantity and Description", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "pending_qty", - "fieldtype": "Float", - "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": "Pending Qty", - "length": 0, - "no_copy": 0, - "oldfieldname": "prevdoc_reqd_qty", - "oldfieldtype": "Currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, + "default": "0", + "fieldname": "pending_qty", + "fieldtype": "Float", + "label": "Pending Qty", + "oldfieldname": "prevdoc_reqd_qty", + "oldfieldtype": "Currency", + "print_width": "100px", + "read_only": 1, "width": "100px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "ordered_qty", - "fieldtype": "Float", - "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": "Ordered Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "ordered_qty", + "fieldtype": "Float", + "label": "Ordered Qty", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "produced_qty", - "fieldtype": "Float", - "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": "Produced Qty", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "produced_qty", + "fieldtype": "Float", + "label": "Produced Qty", + "no_copy": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_17", - "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_17", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "description", - "fieldtype": "Text Editor", - "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": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "200px", - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, + "fieldname": "description", + "fieldtype": "Text Editor", + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text", + "print_width": "200px", + "read_only": 1, "width": "200px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "stock_uom", - "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": "UOM", - "length": 0, - "no_copy": 0, - "oldfieldname": "stock_uom", - "oldfieldtype": "Data", - "options": "UOM", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "80px", - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "UOM", + "oldfieldname": "stock_uom", + "oldfieldtype": "Data", + "options": "UOM", + "print_width": "80px", + "read_only": 1, + "reqd": 1, "width": "80px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "reference_section", - "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": "Reference", - "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": "reference_section", + "fieldtype": "Section Break", + "label": "Reference" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "sales_order", - "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 Order", - "length": 0, - "no_copy": 0, - "oldfieldname": "source_docname", - "oldfieldtype": "Data", - "options": "Sales Order", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "sales_order", + "fieldtype": "Link", + "label": "Sales Order", + "oldfieldname": "source_docname", + "oldfieldtype": "Data", + "options": "Sales Order", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "sales_order_item", - "fieldtype": "Data", - "hidden": 1, - "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 Order Item", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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": "sales_order_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Sales Order Item", + "no_copy": 1, + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_19", - "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_19", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "material_request", - "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": "Material Request", - "length": 0, - "no_copy": 0, - "options": "Material Request", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "material_request", + "fieldtype": "Link", + "label": "Material Request", + "options": "Material Request", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "material_request_item", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "material_request_item", - "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": "material_request_item", + "fieldtype": "Data", + "hidden": 1, + "label": "material_request_item" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "product_bundle_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": "Product Bundle Item", - "length": 0, - "no_copy": 1, - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "product_bundle_item", + "fieldtype": "Link", + "label": "Product Bundle Item", + "no_copy": 1, + "options": "Item", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "item_reference", + "fieldtype": "Data", + "hidden": 1, + "label": "Item Reference" } - ], - "has_web_view": 0, - "hide_toolbar": 0, - "idx": 1, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2019-04-08 23:09:57.199423", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Production Plan Item", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2021-06-28 18:31:06.822168", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Production Plan Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "ASC" } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan_item_reference/__init__.py b/erpnext/manufacturing/doctype/production_plan_item_reference/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json new file mode 100644 index 00000000000..84dee4ad284 --- /dev/null +++ b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json @@ -0,0 +1,52 @@ +{ + "actions": [], + "creation": "2021-04-22 10:32:58.896330", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_reference", + "sales_order", + "sales_order_item", + "qty" + ], + "fields": [ + { + "fieldname": "sales_order", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Sales Order Reference", + "options": "Sales Order" + }, + { + "fieldname": "sales_order_item", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Sales Order Item" + }, + { + "fieldname": "qty", + "fieldtype": "Data", + "in_list_view": 1, + "label": "qty" + }, + { + "fieldname": "item_reference", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Item Reference" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-07 17:03:49.707487", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Production Plan Item Reference", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.py b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.py new file mode 100644 index 00000000000..51fbc3633b1 --- /dev/null +++ b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ProductionPlanItemReference(Document): + pass diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/__init__.py b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d 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 00000000000..657ee35a852 --- /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 00000000000..6850a2eb4ed --- /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/routing/routing.py b/erpnext/manufacturing/doctype/routing/routing.py index 8312d7436c2..ece0db717a2 100644 --- a/erpnext/manufacturing/doctype/routing/routing.py +++ b/erpnext/manufacturing/doctype/routing/routing.py @@ -4,14 +4,24 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cint +from frappe.utils import cint, flt from frappe import _ from frappe.model.document import Document class Routing(Document): def validate(self): + self.calculate_operating_cost() self.set_routing_id() + def on_update(self): + self.calculate_operating_cost() + + def calculate_operating_cost(self): + for operation in self.operations: + if not operation.hour_rate: + operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate') + operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2) + def set_routing_id(self): sequence_id = 0 for row in self.operations: @@ -21,4 +31,4 @@ class Routing(Document): frappe.throw(_("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}") .format(row.idx, row.sequence_id, sequence_id)) - sequence_id = row.sequence_id \ No newline at end of file + sequence_id = row.sequence_id diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py index 6a38dcfa030..92f26946ab7 100644 --- a/erpnext/manufacturing/doctype/routing/test_routing.py +++ b/erpnext/manufacturing/doctype/routing/test_routing.py @@ -7,9 +7,7 @@ import unittest import frappe from frappe.test_runner import make_test_records from erpnext.stock.doctype.item.test_item import make_item -from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError -from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record class TestRouting(unittest.TestCase): @@ -48,7 +46,53 @@ class TestRouting(unittest.TestCase): wo_doc.cancel() wo_doc.delete() + def test_update_bom_operation_time(self): + operations = [ + { + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "hour_rate_rent": 300, + "hour_rate_labour": 750 , + "time_in_mins": 30 + }, + { + "operation": "Test Operation B", + "workstation": "_Test Workstation B", + "hour_rate_labour": 200, + "hour_rate_rent": 1000, + "time_in_mins": 20 + } + ] + + test_routing_operations = [ + { + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "time_in_mins": 30 + }, + { + "operation": "Test Operation B", + "workstation": "_Test Workstation A", + "time_in_mins": 20 + } + ] + setup_operations(operations) + routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations) + bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency = 'INR') + self.assertEqual(routing_doc.operations[0].time_in_mins, 30) + self.assertEqual(routing_doc.operations[1].time_in_mins, 20) + routing_doc.operations[0].time_in_mins = 90 + routing_doc.operations[1].time_in_mins = 42.2 + routing_doc.save() + bom_doc.update_cost() + bom_doc.reload() + self.assertEqual(bom_doc.operations[0].time_in_mins, 90) + self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2) + + def setup_operations(rows): + from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation + from erpnext.manufacturing.doctype.operation.test_operation import make_operation for row in rows: make_workstation(row) make_operation(row) @@ -61,12 +105,14 @@ def create_routing(**args): if not args.do_not_save: try: - for operation in args.operations: - doc.append("operations", operation) - doc.insert() except frappe.DuplicateEntryError: doc = frappe.get_doc("Routing", args.routing_name) + doc.delete_key('operations') + for operation in args.operations: + doc.append("operations", operation) + + doc.save() return doc @@ -91,7 +137,7 @@ def setup_bom(**args): name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name') if not name: bom_doc = make_bom(item = args.item_code, raw_materials = args.get("raw_materials"), - routing = args.routing, with_operations=1) + routing = args.routing, with_operations=1, currency = args.currency) else: bom_doc = frappe.get_doc("BOM", name) diff --git a/erpnext/manufacturing/doctype/sub_operation/__init__.py b/erpnext/manufacturing/doctype/sub_operation/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.js b/erpnext/manufacturing/doctype/sub_operation/sub_operation.js new file mode 100644 index 00000000000..be9db6a4089 --- /dev/null +++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Sub Operation', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json new file mode 100644 index 00000000000..10cee32398a --- /dev/null +++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json @@ -0,0 +1,52 @@ +{ + "actions": [], + "creation": "2020-12-07 15:39:47.488519", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "operation", + "time_in_mins", + "column_break_5", + "description" + ], + "fields": [ + { + "fieldname": "operation", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Operation", + "options": "Operation" + }, + { + "default": "0", + "description": "Time in mins", + "fieldname": "time_in_mins", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Operation Time" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-07-15 16:39:41.635362", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Sub Operation", + "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/sub_operation/sub_operation.py b/erpnext/manufacturing/doctype/sub_operation/sub_operation.py new file mode 100644 index 00000000000..f4b27758e9d --- /dev/null +++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.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 SubOperation(Document): + pass diff --git a/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py b/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py new file mode 100644 index 00000000000..d3410ca3120 --- /dev/null +++ b/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestSubOperation(unittest.TestCase): + pass diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index cb1ee92196f..bf1ccb71594 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -389,17 +389,12 @@ class TestWorkOrder(unittest.TestCase): ste.submit() stock_entries.append(ste) - job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) + job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, order_by='creation asc') self.assertEqual(len(job_cards), len(bom.operations)) for i, job_card in enumerate(job_cards): doc = frappe.get_doc("Job Card", job_card) - doc.append("time_logs", { - "from_time": add_to_date(None, i), - "hours": 1, - "to_time": add_to_date(None, i + 1), - "completed_qty": doc.for_quantity - }) + doc.time_logs[0].completed_qty = 1 doc.submit() ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) @@ -518,6 +513,60 @@ class TestWorkOrder(unittest.TestCase): 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.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 3e5a72db9a7..512048512ed 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -141,8 +141,7 @@ frappe.ui.form.on("Work Order", { } if (frm.doc.docstatus === 1 - && frm.doc.operations && frm.doc.operations.length - && frm.doc.qty != frm.doc.material_transferred_for_manufacturing) { + && frm.doc.operations && frm.doc.operations.length) { const not_completed = frm.doc.operations.filter(d => { if(d.status != 'Completed') { @@ -190,35 +189,41 @@ frappe.ui.form.on("Work Order", { const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'), fields: [ { - fieldtype:'Link', - fieldname:'operation', + fieldtype: 'Link', + fieldname: 'operation', label: __('Operation'), - read_only:1, - in_list_view:1 + read_only: 1, + in_list_view: 1 }, { - fieldtype:'Link', - fieldname:'workstation', + fieldtype: 'Link', + fieldname: 'workstation', label: __('Workstation'), - read_only:1, - in_list_view:1 + read_only: 1, + in_list_view: 1 }, { - fieldtype:'Data', - fieldname:'name', + fieldtype: 'Data', + fieldname: 'name', label: __('Operation Id') }, { - fieldtype:'Float', - fieldname:'pending_qty', + fieldtype: 'Float', + fieldname: 'pending_qty', label: __('Pending Qty'), }, { - fieldtype:'Float', - fieldname:'qty', + fieldtype: 'Float', + fieldname: 'qty', label: __('Quantity to Manufacture'), - read_only:0, - in_list_view:1, + read_only: 0, + in_list_view: 1, + }, + { + fieldtype: 'Float', + fieldname: 'batch_size', + label: __('Batch Size'), + read_only: 1 }, ], data: operations_data, @@ -229,9 +234,13 @@ frappe.ui.form.on("Work Order", { }, function(data) { frappe.call({ method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card", + freeze: true, args: { work_order: frm.doc.name, operations: data.operations, + }, + callback: function() { + frm.reload_doc(); } }); }, __("Job Card"), __("Create")); @@ -243,13 +252,16 @@ frappe.ui.form.on("Work Order", { if(data.completed_qty != frm.doc.qty) { pending_qty = frm.doc.qty - flt(data.completed_qty); - dialog.fields_dict.operations.df.data.push({ - 'name': data.name, - 'operation': data.operation, - 'workstation': data.workstation, - 'qty': pending_qty, - 'pending_qty': pending_qty, - }); + if (pending_qty) { + dialog.fields_dict.operations.df.data.push({ + 'name': data.name, + 'operation': data.operation, + 'workstation': data.workstation, + 'batch_size': data.batch_size, + 'qty': pending_qty, + 'pending_qty': pending_qty + }); + } } }); dialog.fields_dict.operations.grid.refresh(); @@ -704,6 +716,8 @@ erpnext.work_order = { stop_work_order: function(frm, status) { frappe.call({ method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop", + freeze: true, + freeze_message: __("Updating Work Order status"), args: { work_order: frm.doc.name, status: status diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index cd9edeeea83..3b56854aaf3 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -21,6 +21,12 @@ "produced_qty", "sales_order", "project", + "serial_no_and_batch_for_finished_good_section", + "has_serial_no", + "has_batch_no", + "column_break_17", + "serial_no", + "batch_size", "settings_section", "allow_alternative_item", "use_multi_level_bom", @@ -52,16 +58,22 @@ "actual_operating_cost", "additional_operating_cost", "column_break_24", + "corrective_operation_cost", "total_operating_cost", "more_info", "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" ], @@ -488,17 +500,77 @@ "fieldtype": "Float", "label": "Lead Time", "read_only": 1 - } + }, + { + "collapsible": 1, + "depends_on": "eval:!doc.__islocal", + "fieldname": "serial_no_and_batch_for_finished_good_section", + "fieldtype": "Section Break", + "label": "Serial No and Batch for Finished Good" + }, + { + "fieldname": "column_break_17", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fetch_from": "production_item.has_serial_no", + "fieldname": "has_serial_no", + "fieldtype": "Check", + "label": "Has Serial No", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "production_item.has_batch_no", + "fieldname": "has_batch_no", + "fieldtype": "Check", + "label": "Has Batch No", + "read_only": 1 + }, + { + "depends_on": "has_serial_no", + "fieldname": "serial_no", + "fieldtype": "Small Text", + "label": "Serial Nos", + "no_copy": 1 + }, + { + "default": "0", + "depends_on": "has_batch_no", + "fieldname": "batch_size", + "fieldtype": "Float", + "label": "Batch Size" + }, + { + "allow_on_submit": 1, + "description": "From Corrective Job Card", + "fieldname": "corrective_operation_cost", + "fieldtype": "Currency", + "label": "Corrective Operation Cost", + "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-03-16 13:27:51.116484", + "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 8507f5eb34c..282b5d0afe4 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1,7 +1,6 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals import frappe import json import math @@ -19,18 +18,17 @@ from frappe.utils.csvutils import getlink from erpnext.stock.utils import get_bin, validate_warehouse_company, get_latest_stock_qty from erpnext.utilities.transaction_base import validate_uom_is_integer from frappe.model.mapper import get_mapped_doc +from erpnext.stock.doctype.batch.batch import make_batch +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_auto_serial_nos, auto_make_serial_nos class OverProductionError(frappe.ValidationError): pass class CapacityError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass class OperationTooLongError(frappe.ValidationError): pass class ItemHasVariantError(frappe.ValidationError): pass +class SerialNoQtyError(frappe.ValidationError): + pass -from six import string_types - -form_grid_templates = { - "operations": "templates/form_grid/work_order_grid.html" -} class WorkOrder(Document): def onload(self): @@ -127,7 +125,9 @@ class WorkOrder(Document): variable_cost = self.actual_operating_cost if self.actual_operating_cost \ else self.planned_operating_cost - self.total_operating_cost = flt(self.additional_operating_cost) + flt(variable_cost) + + self.total_operating_cost = (flt(self.additional_operating_cost) + + flt(variable_cost) + flt(self.corrective_operation_cost)) def validate_work_order_against_so(self): # already ordered qty @@ -235,13 +235,20 @@ class WorkOrder(Document): production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item) + def before_submit(self): + 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")) - self.update_work_order_qty_in_so() + if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}): + self.update_work_order_qty_in_combined_so() + else: + self.update_work_order_qty_in_so() + self.update_reserved_qty_for_production() self.update_completed_qty_in_material_request() self.update_planned_qty() @@ -250,14 +257,82 @@ class WorkOrder(Document): def on_cancel(self): self.validate_cancel() - frappe.db.set(self,'status', 'Cancelled') - self.update_work_order_qty_in_so() + + if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}): + self.update_work_order_qty_in_combined_so() + else: + self.update_work_order_qty_in_so() + self.delete_job_card() self.update_completed_qty_in_material_request() self.update_planned_qty() self.update_ordered_qty() self.update_reserved_qty_for_production() + self.delete_auto_created_batch_and_serial_no() + + def create_serial_no_batch_no(self): + if not (self.has_serial_no or self.has_batch_no): + return + + if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")): + return + + if self.has_batch_no: + self.create_batch_for_finished_good() + + args = { + "item_code": self.production_item, + "work_order": self.name + } + + if self.has_serial_no: + self.make_serial_nos(args) + + def create_batch_for_finished_good(self): + total_qty = self.qty + if not self.batch_size: + self.batch_size = total_qty + + while total_qty > 0: + qty = self.batch_size + if self.batch_size >= total_qty: + qty = total_qty + + if total_qty > self.batch_size: + total_qty -= self.batch_size + else: + qty = total_qty + total_qty = 0 + + make_batch(frappe._dict({ + "item": self.production_item, + "qty_to_produce": qty, + "reference_doctype": self.doctype, + "reference_name": self.name + })) + + def delete_auto_created_batch_and_serial_no(self): + for row in frappe.get_all("Serial No", filters = {"work_order": self.name}): + frappe.delete_doc("Serial No", row.name) + self.db_set("serial_no", "") + + for row in frappe.get_all("Batch", filters = {"reference_name": self.name}): + frappe.delete_doc("Batch", row.name) + + def make_serial_nos(self, args): + serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series") + if serial_no_series: + self.serial_no = get_auto_serial_nos(serial_no_series, self.qty) + + if self.serial_no: + args.update({"serial_no": self.serial_no, "actual_qty": self.qty}) + auto_make_serial_nos(args) + + serial_nos_length = len(get_serial_nos(self.serial_no)) + if serial_nos_length != self.qty: + frappe.throw(_("{0} Serial Numbers required for Item {1}. You have provided {2}.") + .format(self.qty, self.production_item, serial_nos_length), SerialNoQtyError) def create_job_card(self): manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings") @@ -265,32 +340,40 @@ class WorkOrder(Document): enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning) plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 - for i, row in enumerate(self.operations): - self.set_operation_start_end_time(i, row) - - if not row.workstation: - frappe.throw(_("Row {0}: select the workstation against the operation {1}") - .format(row.idx, row.operation)) - - original_start_time = row.planned_start_time - job_card_doc = create_job_card(self, row, - enable_capacity_planning=enable_capacity_planning, auto_create=True) - - if enable_capacity_planning and job_card_doc: - row.planned_start_time = job_card_doc.time_logs[-1].from_time - row.planned_end_time = job_card_doc.time_logs[-1].to_time - - if date_diff(row.planned_start_time, original_start_time) > plan_days: - frappe.message_log.pop() - frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.") - .format(plan_days, row.operation), CapacityError) - - row.db_update() + for index, row in enumerate(self.operations): + qty = self.qty + while qty > 0: + qty = split_qty_based_on_batch_size(self, row, qty) + if row.job_card_qty > 0: + self.prepare_data_for_job_card(row, index, + plan_days, enable_capacity_planning) planned_end_date = self.operations and self.operations[-1].planned_end_time if planned_end_date: self.db_set("planned_end_date", planned_end_date) + def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning): + self.set_operation_start_end_time(index, row) + + if not row.workstation: + frappe.throw(_("Row {0}: select the workstation against the operation {1}") + .format(row.idx, row.operation)) + + original_start_time = row.planned_start_time + job_card_doc = create_job_card(self, row, auto_create=True, + enable_capacity_planning=enable_capacity_planning) + + if enable_capacity_planning and job_card_doc: + row.planned_start_time = job_card_doc.time_logs[-1].from_time + row.planned_end_time = job_card_doc.time_logs[-1].to_time + + if date_diff(row.planned_start_time, original_start_time) > plan_days: + frappe.message_log.pop() + frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.") + .format(plan_days, row.operation), CapacityError) + + row.db_update() + def set_operation_start_end_time(self, idx, row): """Set start and end time for given operation. If first operation, set start as `planned_start_date`, else add time diff to end time of earlier operation.""" @@ -358,52 +441,73 @@ class WorkOrder(Document): frappe.db.set_value('Sales Order Item', self.sales_order_item, 'work_order_qty', flt(work_order_qty/total_bundle_qty, 2)) + def update_work_order_qty_in_combined_so(self): + total_bundle_qty = 1 + if self.product_bundle_item: + total_bundle_qty = frappe.db.sql(""" select sum(qty) from + `tabProduct Bundle Item` where parent = %s""", (frappe.db.escape(self.product_bundle_item)))[0][0] + + if not total_bundle_qty: + # product bundle is 0 (product bundle allows 0 qty for items) + total_bundle_qty = 1 + + prod_plan = frappe.get_doc('Production Plan', self.production_plan) + item_reference = frappe.get_value('Production Plan Item', self.production_plan_item, 'sales_order_item') + + for plan_reference in prod_plan.prod_plan_references: + work_order_qty = 0.0 + if plan_reference.item_reference == item_reference: + if self.docstatus == 1: + work_order_qty = flt(plan_reference.qty) / total_bundle_qty + frappe.db.set_value('Sales Order Item', + plan_reference.sales_order_item, 'work_order_qty', work_order_qty) + def update_completed_qty_in_material_request(self): if self.material_request: frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item]) def set_work_order_operations(self): """Fetch operations from BOM and set in 'Work Order'""" - self.set('operations', []) - if not self.bom_no: + def _get_operations(bom_no, qty=1): + return frappe.db.sql( + f"""select + operation, description, workstation, idx, + base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins, + "Pending" as status, parent as bom, batch_size, sequence_id + from + `tabBOM Operation` + where + parent = %s order by idx + """, bom_no, as_dict=1) + + + self.set('operations', []) + if not self.bom_no or not frappe.get_cached_value('BOM', self.bom_no, 'with_operations'): return - if self.use_multi_level_bom: - bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree() - else: - bom_list = [self.bom_no] + operations = [] - operations = frappe.db.sql(""" - select - operation, description, workstation, idx, - base_hour_rate as hour_rate, time_in_mins, - "Pending" as status, parent as bom, batch_size, sequence_id - from - `tabBOM Operation` - where - parent in (%s) order by idx - """ % ", ".join(["%s"]*len(bom_list)), tuple(bom_list), as_dict=1) + if self.use_multi_level_bom: + bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation() + bom_traversal = reversed(bom_tree.level_order_traversal()) + + for node in bom_traversal: + if node.is_bom: + operations.extend(_get_operations(node.name, qty=node.exploded_qty)) + + 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) - - if self.use_multi_level_bom and self.get('operations') and self.get('items'): - raw_material_operations = [d.operation for d in self.get('items')] - operations = [d.operation for d in self.get('operations')] - - for operation in raw_material_operations: - if operation not in operations: - self.append('operations', { - 'operation': operation - }) - self.calculate_time() def calculate_time(self): - bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") - for d in self.get("operations"): - d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size)) + d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size)) self.calculate_operating_cost() @@ -485,6 +589,7 @@ class WorkOrder(Document): 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): @@ -550,7 +655,7 @@ class WorkOrder(Document): 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, @@ -640,6 +745,17 @@ class WorkOrder(Document): bom.set_bom_material_details() return bom + def update_batch_produced_qty(self, stock_entry_doc): + if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")): + return + + for row in stock_entry_doc.items: + if row.batch_no and (row.is_finished_item or row.is_scrap_item): + qty = frappe.get_all("Stock Entry Detail", filters = {"batch_no": row.batch_no, "docstatus": 1}, + or_filters= {"is_finished_item": 1, "is_scrap_item": 1}, fields = ["sum(qty)"], as_list=1)[0][0] + + frappe.db.set_value("Batch", row.batch_no, "produced_qty", flt(qty)) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): @@ -717,7 +833,7 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None): return wo_doc def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"): - if isinstance(variant_items, string_types): + if isinstance(variant_items, str): variant_items = json.loads(variant_items) for item in variant_items: @@ -797,6 +913,7 @@ def make_stock_entry(work_order_id, purpose, qty=None): stock_entry.set_stock_entry_type() stock_entry.get_items() + stock_entry.set_serial_no_batch_for_finished_good() return stock_entry.as_dict() @frappe.whitelist() @@ -838,13 +955,47 @@ def query_sales_order(production_item): @frappe.whitelist() def make_job_card(work_order, operations): - if isinstance(operations, string_types): + if isinstance(operations, str): operations = json.loads(operations) work_order = frappe.get_doc('Work Order', work_order) for row in operations: + row = frappe._dict(row) validate_operation_data(row) - create_job_card(work_order, row, row.get("qty"), auto_create=True) + qty = row.get("qty") + while qty > 0: + qty = split_qty_based_on_batch_size(work_order, row, qty) + if row.job_card_qty > 0: + create_job_card(work_order, row, auto_create=True) + +def split_qty_based_on_batch_size(wo_doc, row, qty): + if not cint(frappe.db.get_value("Operation", + row.operation, "create_job_card_based_on_batch_size")): + row.batch_size = row.get("qty") or wo_doc.qty + + row.job_card_qty = row.batch_size + if row.batch_size and qty >= row.batch_size: + qty -= row.batch_size + elif qty > 0: + row.job_card_qty = qty + qty = 0 + + get_serial_nos_for_job_card(row, wo_doc) + + return qty + +def get_serial_nos_for_job_card(row, wo_doc): + if not wo_doc.serial_no: + return + + serial_nos = get_serial_nos(wo_doc.serial_no) + used_serial_nos = [] + for d in frappe.get_all('Job Card', fields=['serial_no'], + filters={'docstatus': ('<', 2), 'work_order': wo_doc.name, 'operation_id': row.name}): + used_serial_nos.extend(get_serial_nos(d.serial_no)) + + serial_nos = sorted(list(set(serial_nos) - set(used_serial_nos))) + row.serial_no = '\n'.join(serial_nos[0:row.job_card_qty]) def validate_operation_data(row): if row.get("qty") <= 0: @@ -863,20 +1014,22 @@ def validate_operation_data(row): ) ) -def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False): +def create_job_card(work_order, row, enable_capacity_planning=False, auto_create=False): doc = frappe.new_doc("Job Card") doc.update({ 'work_order': work_order.name, 'operation': row.get("operation"), 'workstation': row.get("workstation"), 'posting_date': nowdate(), - 'for_quantity': qty or work_order.get('qty', 0), + 'for_quantity': row.job_card_qty or work_order.get('qty', 0), 'operation_id': row.get("name"), 'bom_no': work_order.bom_no, 'project': work_order.project, 'company': work_order.company, 'sequence_id': row.get("sequence_id"), - 'wip_warehouse': work_order.wip_warehouse + 'wip_warehouse': work_order.wip_warehouse, + 'hour_rate': row.get("hour_rate"), + 'serial_no': row.get("serial_no") }) if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer: diff --git a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py index 87c090f99c3..9aa0715e7ff 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py +++ b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py @@ -4,10 +4,17 @@ from frappe import _ def get_data(): return { 'fieldname': 'work_order', + 'non_standard_fieldnames': { + 'Batch': 'reference_name' + }, 'transactions': [ { 'label': _('Transactions'), 'items': ['Stock Entry', 'Job Card', 'Pick List'] + }, + { + 'label': _('Reference'), + 'items': ['Serial No', 'Batch'] } ] } \ No newline at end of file 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 00000000000..a4bf93edef1 --- /dev/null +++ b/erpnext/manufacturing/doctype/work_order/work_order_preview.html @@ -0,0 +1,33 @@ +
+ {% if data.value %} + + {{ __("Open Work Order {0}", [data.value.bold()]) }} + {% endif %} + {% if data.item_code %} + + {{ __("Open Item {0}", [data.item_code.bold()]) }} + {% endif %} +
+- -
+-
{{ _("No tasks") }}
-{% endif %} + {{ progress_bar(doc.percent_complete) }} + {% if doc.tasks %} +{{ _("No Tasks") }}
+ {% endif %} - + {% if doc.timesheets %} +{{ _("No Timesheets") }}
+ {% endif %} -- {% endif %} -{% else %} -
{{ _("No time sheets") }}
-{% endif %} - -{% if doc.attachments %} - - -