Merge branch 'develop' into subcontracting

This commit is contained in:
Sagar Sharma 2022-05-20 11:56:25 +05:30
commit 213113bc00
892 changed files with 157085 additions and 36805 deletions

View File

@ -5,7 +5,7 @@
"es6": true "es6": true
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": 9, "ecmaVersion": 11,
"sourceType": "module" "sourceType": "module"
}, },
"extends": "eslint:recommended", "extends": "eslint:recommended",

View File

@ -26,3 +26,6 @@ b147b85e6ac19a9220cd1e2958a6ebd99373283a
# bulk format python code with black # bulk format python code with black
494bd9ef78313436f0424b918f200dab8fc7c20b 494bd9ef78313436f0424b918f200dab8fc7c20b
# bulk format python code with black
baec607ff5905b1c67531096a9cf50ec7ff00a5d

View File

@ -12,10 +12,18 @@ Welcome to ERPNext issue tracker! Before creating an issue, please heed the foll
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext 1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com - For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
- For documentation issues, refer to https://github.com/frappe/erpnext_com
2. Use the search function before creating a new issue. Duplicates will be closed and directed to 2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion. the original discussion.
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen. 3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
Please keep in mind that we get many many requests and we can't possibly work on all of them, we prioritize development based on the goals of the product and organization. Feature requests are still welcome as it helps us in research when we do decide to work on the requested feature.
If you're in urgent need to a feature, please try the following channels to get paid developments done quickly:
1. Certified ERPNext partners: https://erpnext.com/partners
2. Developer community on ERPNext forums: https://discuss.erpnext.com/c/developers/5
3. Telegram group for ERPNext/Frappe development work: https://t.me/erpnext_opps
--> -->
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**

View File

@ -2,6 +2,13 @@
set -e set -e
# Check for merge conflicts before proceeding
python -m compileall -f "${GITHUB_WORKSPACE}"
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
then echo "Found merge conflicts"
exit 1
fi
cd ~ || exit cd ~ || exit
sudo apt-get install redis-server libcups2-dev sudo apt-get install redis-server libcups2-dev

2
.github/stale.yml vendored
View File

@ -25,7 +25,7 @@ pulls:
ready. Thank you for contributing. ready. Thank you for contributing.
issues: issues:
daysUntilStale: 60 daysUntilStale: 90
daysUntilClose: 7 daysUntilClose: 7
exemptLabels: exemptLabels:
- valid - valid

View File

@ -1,4 +1,4 @@
<svg width="201" height="60" viewBox="0 0 201 60" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="4 2 193 52">
<g filter="url(#filter0_dd)"> <g filter="url(#filter0_dd)">
<rect x="4" y="2" width="193" height="52" rx="6" fill="#2490EF"/> <rect x="4" y="2" width="193" height="52" rx="6" fill="#2490EF"/>
<path d="M28 22.2891H32.8786V35.5H36.2088V22.2891H41.0874V19.5H28V22.2891Z" fill="white"/> <path d="M28 22.2891H32.8786V35.5H36.2088V22.2891H41.0874V19.5H28V22.2891Z" fill="white"/>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

31
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,31 @@
name: Generate Semantic Release
on:
push:
branches:
- version-13
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout Entire Repository
uses: actions/checkout@v2
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js v14
uses: actions/setup-node@v2
with:
node-version: 14
- name: Setup dependencies
run: |
npm install @semantic-release/git @semantic-release/exec --no-save
- name: Create Release
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GIT_AUTHOR_NAME: "Frappe PR Bot"
GIT_AUTHOR_EMAIL: "developers@frappe.io"
GIT_COMMITTER_NAME: "Frappe PR Bot"
GIT_COMMITTER_EMAIL: "developers@frappe.io"
run: npx semantic-release

View File

@ -118,10 +118,26 @@ jobs:
CI_BUILD_ID: ${{ github.run_id }} CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
- name: Upload coverage data
uses: actions/upload-artifact@v3
with:
name: coverage-${{ matrix.container }}
path: /home/runner/frappe-bench/sites/coverage.xml
coverage:
name: Coverage Wrap Up
needs: test
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v2
- name: Download artifacts
uses: actions/download-artifact@v3
- name: Upload coverage data - name: Upload coverage data
uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v2
with: with:
name: MariaDB name: MariaDB
fail_ci_if_error: true fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml
verbose: true verbose: true

View File

@ -88,3 +88,37 @@ pull_request_rules:
- version-12-pre-release - version-12-pre-release
assignees: assignees:
- "{{ author }}" - "{{ author }}"
- name: Automatic merge on CI success and review
conditions:
- status-success=linters
- status-success=Sider
- status-success=Semantic Pull Request
- status-success=Patch Test
- status-success=Python Unit Tests (1)
- status-success=Python Unit Tests (2)
- status-success=Python Unit Tests (3)
- label!=dont-merge
- label!=squash
- "#approved-reviews-by>=1"
actions:
merge:
method: merge
- name: Automatic squash on CI success and review
conditions:
- status-success=linters
- status-success=Sider
- status-success=Patch Test
- status-success=Python Unit Tests (1)
- status-success=Python Unit Tests (2)
- status-success=Python Unit Tests (3)
- label!=dont-merge
- label=squash
- "#approved-reviews-by>=1"
actions:
merge:
method: squash
commit_message_template: |
{{ title }} (#{{ number }})
{{ body }}

24
.releaserc Normal file
View File

@ -0,0 +1,24 @@
{
"branches": ["version-13"],
"plugins": [
"@semantic-release/commit-analyzer", {
"preset": "angular",
"releaseRules": [
{"breaking": true, "release": false}
]
},
"@semantic-release/release-notes-generator",
[
"@semantic-release/exec", {
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" erpnext/__init__.py'
}
],
[
"@semantic-release/git", {
"assets": ["erpnext/__init__.py"],
"message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}"
}
],
"@semantic-release/github"
]
}

View File

@ -1,11 +1,14 @@
<div align="center"> <div align="center">
<a href="https://erpnext.com">
<img src="https://raw.githubusercontent.com/frappe/erpnext/develop/erpnext/public/images/erpnext-logo.png" height="128"> <img src="https://raw.githubusercontent.com/frappe/erpnext/develop/erpnext/public/images/erpnext-logo.png" height="128">
</a>
<h2>ERPNext</h2> <h2>ERPNext</h2>
<p align="center"> <p align="center">
<p>ERP made simple</p> <p>ERP made simple</p>
</p> </p>
[![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml/badge.svg?branch=develop)](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml) [![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml/badge.svg?branch=develop)](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml)
[![UI](https://github.com/erpnext/erpnext_ui_tests/actions/workflows/ui-tests.yml/badge.svg?branch=develop&event=schedule)](https://github.com/erpnext/erpnext_ui_tests/actions/workflows/ui-tests.yml)
[![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext) [![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext)
[![codecov](https://codecov.io/gh/frappe/erpnext/branch/develop/graph/badge.svg?token=0TwvyUg3I5)](https://codecov.io/gh/frappe/erpnext) [![codecov](https://codecov.io/gh/frappe/erpnext/branch/develop/graph/badge.svg?token=0TwvyUg3I5)](https://codecov.io/gh/frappe/erpnext)
[![docker pulls](https://img.shields.io/docker/pulls/frappe/erpnext-worker.svg)](https://hub.docker.com/r/frappe/erpnext-worker) [![docker pulls](https://img.shields.io/docker/pulls/frappe/erpnext-worker.svg)](https://hub.docker.com/r/frappe/erpnext-worker)
@ -31,40 +34,39 @@ ERPNext as a monolith includes the following areas for managing businesses:
1. [Customize ERPNext](https://erpnext.com/docs/user/manual/en/customize-erpnext) 1. [Customize ERPNext](https://erpnext.com/docs/user/manual/en/customize-erpnext)
1. [And More](https://erpnext.com/docs/user/manual/en/) 1. [And More](https://erpnext.com/docs/user/manual/en/)
ERPNext requires MariaDB.
ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a full-stack web app framework built with Python & JavaScript. ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a full-stack web app framework built with Python & JavaScript.
- [User Guide](https://erpnext.com/docs/user) ## Installation
- [Discussion Forum](https://discuss.erpnext.com/)
--- <div align="center" style="max-height: 40px;">
<a href="https://frappecloud.com/erpnext/signup">
<div align="center">
<a href="https://frappecloud.com/deploy?apps=frappe,erpnext&source=erpnext_readme">
<img src=".github/try-on-f-cloud-button.svg" height="40"> <img src=".github/try-on-f-cloud-button.svg" height="40">
</a> </a>
<a href="https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/frappe/frappe_docker/main/pwd.yml">
<img src="https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png" alt="Try in PWD" height="37"/>
</a>
</div> </div>
> Login for the PWD site: (username: Administrator, password: admin)
### Containerized Installation ### Containerized Installation
Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details. Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details.
### Full Install ### Manual Install
The Easy Way: our install script for bench will install all dependencies (e.g. MariaDB). See https://github.com/frappe/bench for more details. The Easy Way: our install script for bench will install all dependencies (e.g. MariaDB). See https://github.com/frappe/bench for more details.
New passwords will be created for the ERPNext "Administrator" user, the MariaDB root user, and the frappe user (the script displays the passwords and saves them to ~/frappe_passwords.txt). New passwords will be created for the ERPNext "Administrator" user, the MariaDB root user, and the frappe user (the script displays the passwords and saves them to ~/frappe_passwords.txt).
---
## License ## Learning and community
GNU/General Public License (see [license.txt](license.txt)) 1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
4. [Telegram Group](https://t.me/erpnexthelp) - Get instant help from huge community of users.
The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
---
## Contributing ## Contributing
@ -72,49 +74,14 @@ The ERPNext code is licensed as GNU General Public License (v3) and the Document
1. [Report Security Vulnerabilities](https://erpnext.com/security) 1. [Report Security Vulnerabilities](https://erpnext.com/security)
1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines) 1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
1. [Translations](https://translate.erpnext.com) 1. [Translations](https://translate.erpnext.com)
1. [Chart of Accounts](https://charts.erpnext.com)
---
## Learning ## License
1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community. GNU/General Public License (see [license.txt](license.txt))
--- The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
## Logo and Trademark ## Logo and Trademark Policy
The brand name ERPNext and the logo are trademarks of Frappe Technologies Pvt. Ltd. Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md).
### Introduction
Frappe Technologies Pvt. Ltd. (Frappe) owns and oversees the trademarks for the ERPNext name and logos. We have developed this trademark usage policy with the following goals in mind:
- Wed like to make it easy for anyone to use the ERPNext name or logo for community-oriented efforts that help spread and improve ERPNext.
- Wed like to make it clear how ERPNext-related businesses and projects can (and cannot) use the ERPNext name and logo.
- Wed like to make it hard for anyone to use the ERPNext name and logo to unfairly profit from, trick or confuse people who are looking for official ERPNext resources.
### Frappe Trademark Usage Policy
Permission from Frappe is required to use the ERPNext name or logo as part of any project, product, service, domain or company name.
We will grant permission to use the ERPNext name and logo for projects that meet the following criteria:
- The primary purpose of your project is to promote the spread and improvement of the ERPNext software.
- Your project is non-commercial in nature (it can make money to cover its costs or contribute to non-profit entities, but it cannot be run as a for-profit project or business).
Your project neither promotes nor is associated with entities that currently fail to comply with the GPL license under which ERPNext is distributed.
- If your project meets these criteria, you will be permitted to use the ERPNext name and logo to promote your project in any way you see fit with one exception: Please do not use ERPNext as part of a domain name.
Use of the ERPNext name and logo is additionally allowed in the following situations:
All other ERPNext-related businesses or projects can use the ERPNext name and logo to refer to and explain their services, but they cannot use them as part of a product, project, service, domain, or company name and they cannot use them in any way that suggests an affiliation with or endorsement by ERPNext or Frappe Technologies or the ERPNext open source project. For example, a consulting company can describe its business as “123 Web Services, offering ERPNext consulting for small businesses,” but cannot call its business “The ERPNext Consulting Company.”
Similarly, its OK to use the ERPNext logo as part of a page that describes your products or services, but it is not OK to use it as part of your company or product logo or branding itself. Under no circumstances is it permitted to use ERPNext as part of a top-level domain name.
We do not allow the use of the trademark in advertising, including AdSense/AdWords.
Please note that it is not the goal of this policy to limit commercial activity around ERPNext. We encourage ERPNext-based businesses, and we would love to see hundreds of them.
When in doubt about your use of the ERPNext name or logo, please contact Frappe Technologies for clarification.
(inspired by WordPress)

36
TRADEMARK_POLICY.md Normal file
View File

@ -0,0 +1,36 @@
## Logo and Trademark Policy
The brand name ERPNext and the logo are trademarks of Frappe Technologies Pvt. Ltd.
### Introduction
Frappe Technologies Pvt. Ltd. (Frappe) owns and oversees the trademarks for the ERPNext name and logos. We have developed this trademark usage policy with the following goals in mind:
- Wed like to make it easy for anyone to use the ERPNext name or logo for community-oriented efforts that help spread and improve ERPNext.
- Wed like to make it clear how ERPNext-related businesses and projects can (and cannot) use the ERPNext name and logo.
- Wed like to make it hard for anyone to use the ERPNext name and logo to unfairly profit from, trick or confuse people who are looking for official ERPNext resources.
### Frappe Trademark Usage Policy
Permission from Frappe is required to use the ERPNext name or logo as part of any project, product, service, domain or company name.
We will grant permission to use the ERPNext name and logo for projects that meet the following criteria:
- The primary purpose of your project is to promote the spread and improvement of the ERPNext software.
- Your project is non-commercial in nature (it can make money to cover its costs or contribute to non-profit entities, but it cannot be run as a for-profit project or business).
Your project neither promotes nor is associated with entities that currently fail to comply with the GPL license under which ERPNext is distributed.
- If your project meets these criteria, you will be permitted to use the ERPNext name and logo to promote your project in any way you see fit with one exception: Please do not use ERPNext as part of a domain name.
Use of the ERPNext name and logo is additionally allowed in the following situations:
All other ERPNext-related businesses or projects can use the ERPNext name and logo to refer to and explain their services, but they cannot use them as part of a product, project, service, domain, or company name and they cannot use them in any way that suggests an affiliation with or endorsement by ERPNext or Frappe Technologies or the ERPNext open source project. For example, a consulting company can describe its business as “123 Web Services, offering ERPNext consulting for small businesses,” but cannot call its business “The ERPNext Consulting Company.”
Similarly, its OK to use the ERPNext logo as part of a page that describes your products or services, but it is not OK to use it as part of your company or product logo or branding itself. Under no circumstances is it permitted to use ERPNext as part of a top-level domain name.
We do not allow the use of the trademark in advertising, including AdSense/AdWords.
Please note that it is not the goal of this policy to limit commercial activity around ERPNext. We encourage ERPNext-based businesses, and we would love to see hundreds of them.
When in doubt about your use of the ERPNext name or logo, please contact Frappe Technologies for clarification.
(inspired by WordPress)

View File

@ -21,7 +21,6 @@ coverage:
comment: comment:
layout: "diff, files" layout: "diff, files"
require_changes: true require_changes: true
after_n_builds: 3
ignore: ignore:
- "erpnext/demo" - "erpnext/demo"

View File

@ -386,7 +386,6 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
doc, doc,
credit_account, credit_account,
debit_account, debit_account,
against,
amount, amount,
base_amount, base_amount,
end_date, end_date,
@ -540,19 +539,11 @@ def make_gl_entries(
frappe.db.commit() frappe.db.commit()
except Exception as e: except Exception as e:
if frappe.flags.in_test: if frappe.flags.in_test:
traceback = frappe.get_traceback() doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")
frappe.log_error(
title=_("Error while processing deferred accounting for Invoice {0}").format(doc.name),
message=traceback,
)
raise e raise e
else: else:
frappe.db.rollback() frappe.db.rollback()
traceback = frappe.get_traceback() doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")
frappe.log_error(
title=_("Error while processing deferred accounting for Invoice {0}").format(doc.name),
message=traceback,
)
frappe.flags.deferred_accounting_error = True frappe.flags.deferred_accounting_error = True
@ -570,7 +561,6 @@ def book_revenue_via_journal_entry(
doc, doc,
credit_account, credit_account,
debit_account, debit_account,
against,
amount, amount,
base_amount, base_amount,
posting_date, posting_date,
@ -591,6 +581,7 @@ def book_revenue_via_journal_entry(
journal_entry.voucher_type = ( journal_entry.voucher_type = (
"Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense" "Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense"
) )
journal_entry.process_deferred_accounting = deferred_process
debit_entry = { debit_entry = {
"account": credit_account, "account": credit_account,
@ -633,12 +624,7 @@ def book_revenue_via_journal_entry(
frappe.db.commit() frappe.db.commit()
except Exception: except Exception:
frappe.db.rollback() frappe.db.rollback()
traceback = frappe.get_traceback() doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")
frappe.log_error(
title=_("Error while processing deferred accounting for Invoice {0}").format(doc.name),
message=traceback,
)
frappe.flags.deferred_accounting_error = True frappe.flags.deferred_accounting_error = True

View File

@ -160,7 +160,7 @@ frappe.treeview_settings["Account"] = {
let root_company = treeview.page.fields_dict.root_company.get_value(); let root_company = treeview.page.fields_dict.root_company.get_value();
if(root_company) { if(root_company) {
frappe.throw(__("Please add the account to root level Company - ") + root_company); frappe.throw(__("Please add the account to root level Company - {0}"), [root_company]);
} else { } else {
treeview.new_node(); treeview.new_node();
} }

View File

@ -99,7 +99,7 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
if doctype == "Budget": if doctype == "Budget":
add_dimension_to_budget_doctype(df.copy(), doc) add_dimension_to_budget_doctype(df.copy(), doc)
else: else:
create_custom_field(doctype, df) create_custom_field(doctype, df, ignore_validate=True)
count += 1 count += 1
@ -115,7 +115,7 @@ def add_dimension_to_budget_doctype(df, doc):
} }
) )
create_custom_field("Budget", df) create_custom_field("Budget", df, ignore_validate=True)
property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options") property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options")
@ -205,10 +205,16 @@ def get_doctypes_with_dimensions():
return frappe.get_hooks("accounting_dimension_doctypes") return frappe.get_hooks("accounting_dimension_doctypes")
def get_accounting_dimensions(as_list=True): def get_accounting_dimensions(as_list=True, filters=None):
if not filters:
filters = {"disabled": 0}
if frappe.flags.accounting_dimensions is None: if frappe.flags.accounting_dimensions is None:
frappe.flags.accounting_dimensions = frappe.get_all( frappe.flags.accounting_dimensions = frappe.get_all(
"Accounting Dimension", fields=["label", "fieldname", "disabled", "document_type"] "Accounting Dimension",
fields=["label", "fieldname", "disabled", "document_type"],
filters=filters,
) )
if as_list: if as_list:
@ -228,12 +234,14 @@ def get_checks_for_pl_and_bs_accounts():
return dimensions return dimensions
def get_dimension_with_children(doctype, dimension): def get_dimension_with_children(doctype, dimensions):
if isinstance(dimension, list): if isinstance(dimensions, str):
dimension = dimension[0] dimensions = [dimensions]
all_dimensions = [] all_dimensions = []
for dimension in dimensions:
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"]) lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
children = frappe.get_all( children = frappe.get_all(
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft" doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"

View File

@ -18,7 +18,6 @@
"automatically_fetch_payment_terms", "automatically_fetch_payment_terms",
"column_break_17", "column_break_17",
"enable_common_party_accounting", "enable_common_party_accounting",
"enable_discount_accounting",
"report_setting_section", "report_setting_section",
"use_custom_cash_flow", "use_custom_cash_flow",
"deferred_accounting_settings_section", "deferred_accounting_settings_section",
@ -272,13 +271,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Create Ledger Entries for Change Amount" "label": "Create Ledger Entries for Change Amount"
}, },
{
"default": "0",
"description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account",
"fieldname": "enable_discount_accounting",
"fieldtype": "Check",
"label": "Enable Discount Accounting"
},
{ {
"default": "0", "default": "0",
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>", "description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
@ -354,7 +346,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2022-02-04 12:32:36.805652", "modified": "2022-04-08 14:45:06.796418",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -28,7 +28,6 @@ class AccountsSettings(Document):
self.validate_stale_days() self.validate_stale_days()
self.enable_payment_schedule_in_print() self.enable_payment_schedule_in_print()
self.toggle_discount_accounting_fields()
self.validate_pending_reposts() self.validate_pending_reposts()
def validate_stale_days(self): def validate_stale_days(self):
@ -52,74 +51,6 @@ class AccountsSettings(Document):
validate_fields_for_doctype=False, validate_fields_for_doctype=False,
) )
def toggle_discount_accounting_fields(self):
enable_discount_accounting = cint(self.enable_discount_accounting)
for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]:
make_property_setter(
doctype,
"discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
if enable_discount_accounting:
make_property_setter(
doctype,
"discount_account",
"mandatory_depends_on",
"eval: doc.discount_amount",
"Code",
validate_fields_for_doctype=False,
)
else:
make_property_setter(
doctype,
"discount_account",
"mandatory_depends_on",
"",
"Code",
validate_fields_for_doctype=False,
)
for doctype in ["Sales Invoice", "Purchase Invoice"]:
make_property_setter(
doctype,
"additional_discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
if enable_discount_accounting:
make_property_setter(
doctype,
"additional_discount_account",
"mandatory_depends_on",
"eval: doc.discount_amount",
"Code",
validate_fields_for_doctype=False,
)
else:
make_property_setter(
doctype,
"additional_discount_account",
"mandatory_depends_on",
"",
"Code",
validate_fields_for_doctype=False,
)
make_property_setter(
"Item",
"default_discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
def validate_pending_reposts(self): def validate_pending_reposts(self):
if self.acc_frozen_upto: if self.acc_frozen_upto:
check_pending_reposting(self.acc_frozen_upto) check_pending_reposting(self.acc_frozen_upto)

View File

@ -27,7 +27,6 @@
"bank_account_no", "bank_account_no",
"address_and_contact", "address_and_contact",
"address_html", "address_html",
"website",
"column_break_13", "column_break_13",
"contact_html", "contact_html",
"integration_details_section", "integration_details_section",
@ -156,11 +155,6 @@
"fieldtype": "HTML", "fieldtype": "HTML",
"label": "Address HTML" "label": "Address HTML"
}, },
{
"fieldname": "website",
"fieldtype": "Data",
"label": "Website"
},
{ {
"fieldname": "column_break_13", "fieldname": "column_break_13",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@ -208,7 +202,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2020-10-23 16:48:06.303658", "modified": "2022-05-04 15:49:42.620630",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Account", "name": "Bank Account",
@ -243,5 +237,6 @@
"search_fields": "bank,account", "search_fields": "bank,account",
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -5,7 +5,10 @@
import frappe import frappe
from frappe import _, msgprint from frappe import _, msgprint
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, fmt_money, getdate, nowdate from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, getdate
import erpnext
form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"} form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"}
@ -76,6 +79,53 @@ class BankClearance(Document):
as_dict=1, as_dict=1,
) )
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
loan_disbursements = (
frappe.qb.from_(loan_disbursement)
.select(
ConstantColumn("Loan Disbursement").as_("payment_document"),
loan_disbursement.name.as_("payment_entry"),
loan_disbursement.disbursed_amount.as_("credit"),
ConstantColumn(0).as_("debit"),
loan_disbursement.reference_number.as_("cheque_number"),
loan_disbursement.reference_date.as_("cheque_date"),
loan_disbursement.disbursement_date.as_("posting_date"),
loan_disbursement.applicant.as_("against_account"),
)
.where(loan_disbursement.docstatus == 1)
.where(loan_disbursement.disbursement_date >= self.from_date)
.where(loan_disbursement.disbursement_date <= self.to_date)
.where(loan_disbursement.clearance_date.isnull())
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
.orderby(loan_disbursement.disbursement_date)
.orderby(loan_disbursement.name, frappe.qb.desc)
).run(as_dict=1)
loan_repayment = frappe.qb.DocType("Loan Repayment")
loan_repayments = (
frappe.qb.from_(loan_repayment)
.select(
ConstantColumn("Loan Repayment").as_("payment_document"),
loan_repayment.name.as_("payment_entry"),
loan_repayment.amount_paid.as_("debit"),
ConstantColumn(0).as_("credit"),
loan_repayment.reference_number.as_("cheque_number"),
loan_repayment.reference_date.as_("cheque_date"),
loan_repayment.applicant.as_("against_account"),
loan_repayment.posting_date,
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.repay_from_salary == 0)
.where(loan_repayment.posting_date >= self.from_date)
.where(loan_repayment.posting_date <= self.to_date)
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
.orderby(loan_repayment.posting_date)
.orderby(loan_repayment.name, frappe.qb.desc)
).run(as_dict=1)
pos_sales_invoices, pos_purchase_invoices = [], [] pos_sales_invoices, pos_purchase_invoices = [], []
if self.include_pos_transactions: if self.include_pos_transactions:
pos_sales_invoices = frappe.db.sql( pos_sales_invoices = frappe.db.sql(
@ -114,20 +164,29 @@ class BankClearance(Document):
entries = sorted( entries = sorted(
list(payment_entries) list(payment_entries)
+ list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)), + list(journal_entries)
key=lambda k: k["posting_date"] or getdate(nowdate()), + list(pos_sales_invoices)
+ list(pos_purchase_invoices)
+ list(loan_disbursements)
+ list(loan_repayments),
key=lambda k: getdate(k["posting_date"]),
) )
self.set("payment_entries", []) self.set("payment_entries", [])
self.total_amount = 0.0 self.total_amount = 0.0
default_currency = erpnext.get_default_currency()
for d in entries: for d in entries:
row = self.append("payment_entries", {}) row = self.append("payment_entries", {})
amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0)) amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0))
if not d.get("account_currency"):
d.account_currency = default_currency
formatted_amount = fmt_money(abs(amount), 2, d.account_currency) formatted_amount = fmt_money(abs(amount), 2, d.account_currency)
d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr")) d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr"))
d.posting_date = getdate(d.posting_date)
d.pop("credit") d.pop("credit")
d.pop("debit") d.pop("debit")

View File

@ -1,9 +1,96 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
# import frappe
import unittest import unittest
import frappe
from frappe.utils import add_months, getdate
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.loan_management.doctype.loan.test_loan import (
create_loan,
create_loan_accounts,
create_loan_type,
create_repayment_entry,
make_loan_disbursement_entry,
)
class TestBankClearance(unittest.TestCase): class TestBankClearance(unittest.TestCase):
pass @classmethod
def setUpClass(cls):
make_bank_account()
create_loan_accounts()
create_loan_masters()
add_transactions()
# Basic test case to test if bank clearance tool doesn't break
# Detailed test can be added later
def test_bank_clearance(self):
bank_clearance = frappe.get_doc("Bank Clearance")
bank_clearance.account = "_Test Bank Clearance - _TC"
bank_clearance.from_date = add_months(getdate(), -1)
bank_clearance.to_date = getdate()
bank_clearance.get_payment_entries()
self.assertEqual(len(bank_clearance.payment_entries), 3)
def make_bank_account():
if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"):
frappe.get_doc(
{
"doctype": "Account",
"account_type": "Bank",
"account_name": "_Test Bank Clearance",
"company": "_Test Company",
"parent_account": "Bank Accounts - _TC",
}
).insert()
def create_loan_masters():
create_loan_type(
"Clearance Loan",
2000000,
13.5,
25,
0,
5,
"Cash",
"_Test Bank Clearance - _TC",
"_Test Bank Clearance - _TC",
"Loan Account - _TC",
"Interest Income Account - _TC",
"Penalty Income Account - _TC",
)
def add_transactions():
make_payment_entry()
make_loan()
def make_loan():
loan = create_loan(
"_Test Customer",
"Clearance Loan",
280000,
"Repay Over Number of Periods",
20,
applicant_type="Customer",
)
loan.submit()
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate())
repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount)
repayment_entry.save()
repayment_entry.submit()
def make_payment_entry():
pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC")
pe.reference_no = "Conrad Oct 18"
pe.reference_date = "2018-10-24"
pe.insert()
pe.submit()

View File

@ -467,6 +467,7 @@ def get_lr_matching_query(bank_account, amount_condition, filters):
loan_repayment.posting_date, loan_repayment.posting_date,
) )
.where(loan_repayment.docstatus == 1) .where(loan_repayment.docstatus == 1)
.where(loan_repayment.repay_from_salary == 0)
.where(loan_repayment.clearance_date.isnull()) .where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.payment_account == bank_account) .where(loan_repayment.payment_account == bank_account)
) )

View File

@ -200,7 +200,7 @@ frappe.ui.form.on("Bank Statement Import", {
}) })
.then((result) => { .then((result) => {
if (result.length > 0) { if (result.length > 0) {
frm.add_custom_button("Report Error", () => { frm.add_custom_button(__("Report Error"), () => {
let fake_xhr = { let fake_xhr = {
responseText: JSON.stringify({ responseText: JSON.stringify({
exc: result[0].error, exc: result[0].error,

View File

@ -136,7 +136,7 @@ def start_import(
except Exception: except Exception:
frappe.db.rollback() frappe.db.rollback()
data_import.db_set("status", "Error") data_import.db_set("status", "Error")
frappe.log_error(title=data_import.name) data_import.log_error("Bank Statement Import failed")
finally: finally:
frappe.flags.in_import = False frappe.flags.in_import = False

View File

@ -56,7 +56,7 @@ def create_bank_entries(columns, data, bank_account):
bank_transaction.submit() bank_transaction.submit()
success += 1 success += 1
except Exception: except Exception:
frappe.log_error(frappe.get_traceback()) bank_transaction.log_error("Bank entry creation failed")
errors += 1 errors += 1
return {"success": success, "errors": errors} return {"success": success, "errors": errors}

View File

@ -3,6 +3,7 @@
import frappe import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
@ -16,6 +17,6 @@ class CashFlowMapping(Document):
] ]
if len(checked_fields) > 1: if len(checked_fields) > 1:
frappe.throw( frappe.throw(
frappe._("You can only select a maximum of one option from the list of check boxes."), _("You can only select a maximum of one option from the list of check boxes."),
title="Error", title=_("Error"),
) )

View File

@ -11,6 +11,8 @@ from frappe.utils import nowdate
class CurrencyExchangeSettings(Document): class CurrencyExchangeSettings(Document):
def validate(self): def validate(self):
self.set_parameters_and_result() self.set_parameters_and_result()
if frappe.flags.in_test or frappe.flags.in_install or frappe.flags.in_setup_wizard:
return
response, value = self.validate_parameters() response, value = self.validate_parameters()
self.validate_result(response, value) self.validate_result(response, value)
@ -35,9 +37,6 @@ class CurrencyExchangeSettings(Document):
self.append("req_params", {"key": "symbols", "value": "{to_currency}"}) self.append("req_params", {"key": "symbols", "value": "{to_currency}"})
def validate_parameters(self): def validate_parameters(self):
if frappe.flags.in_test:
return None, None
params = {} params = {}
for row in self.req_params: for row in self.req_params:
params[row.key] = row.value.format( params[row.key] = row.value.format(
@ -59,18 +58,14 @@ class CurrencyExchangeSettings(Document):
return response, value return response, value
def validate_result(self, response, value): def validate_result(self, response, value):
if frappe.flags.in_test:
return
try: try:
for key in self.result_key: for key in self.result_key:
value = value[ value = value[
str(key.key).format(transaction_date=nowdate(), to_currency="INR", from_currency="USD") str(key.key).format(transaction_date=nowdate(), to_currency="INR", from_currency="USD")
] ]
except Exception: except Exception:
frappe.throw("Invalid result key. Response: " + response.text) frappe.throw(_("Invalid result key. Response:") + " " + response.text)
if not isinstance(value, (int, float)): if not isinstance(value, (int, float)):
frappe.throw(_("Returned exchange rate is neither integer not float.")) frappe.throw(_("Returned exchange rate is neither integer not float."))
self.url = response.url self.url = response.url
frappe.msgprint("Exchange rate of USD to INR is " + str(value))

View File

@ -3,6 +3,6 @@
frappe.ui.form.on('GL Entry', { frappe.ui.form.on('GL Entry', {
refresh: function(frm) { refresh: function(frm) {
frm.page.btn_secondary.hide()
} }
}); });

View File

@ -269,6 +269,11 @@ class GLEntry(Document):
if not self.fiscal_year: if not self.fiscal_year:
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0] self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
def on_cancel(self):
msg = _("Individual GL Entry cannot be cancelled.")
msg += "<br>" + _("Please cancel related transaction.")
frappe.throw(msg)
def validate_balance_type(account, adv_adj=False): def validate_balance_type(account, adv_adj=False):
if not adv_adj and account: if not adv_adj and account:

View File

@ -10,6 +10,7 @@
"sgst_account", "sgst_account",
"igst_account", "igst_account",
"cess_account", "cess_account",
"utgst_account",
"is_reverse_charge_account" "is_reverse_charge_account"
], ],
"fields": [ "fields": [
@ -64,12 +65,18 @@
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1, "in_list_view": 1,
"label": "Is Reverse Charge Account" "label": "Is Reverse Charge Account"
},
{
"fieldname": "utgst_account",
"fieldtype": "Link",
"label": "UTGST Account",
"options": "Account"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-04-09 12:30:25.889993", "modified": "2022-04-07 12:59:14.039768",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "GST Account", "name": "GST Account",
@ -78,5 +85,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -3,7 +3,7 @@
"allow_auto_repeat": 1, "allow_auto_repeat": 1,
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-03-25 10:53:52", "creation": "2022-01-25 10:29:58.717206",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Document", "document_type": "Document",
"engine": "InnoDB", "engine": "InnoDB",
@ -13,6 +13,7 @@
"voucher_type", "voucher_type",
"naming_series", "naming_series",
"finance_book", "finance_book",
"process_deferred_accounting",
"reversal_of", "reversal_of",
"tax_withholding_category", "tax_withholding_category",
"column_break1", "column_break1",
@ -524,13 +525,20 @@
"label": "Reversal Of", "label": "Reversal Of",
"options": "Journal Entry", "options": "Journal Entry",
"read_only": 1 "read_only": 1
},
{
"fieldname": "process_deferred_accounting",
"fieldtype": "Link",
"label": "Process Deferred Accounting",
"options": "Process Deferred Accounting",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 176, "idx": 176,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-01-04 13:39:36.485954", "modified": "2022-04-06 17:18:46.865259",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry", "name": "Journal Entry",
@ -578,6 +586,7 @@
"search_fields": "voucher_type,posting_date, due_date, cheque_no", "search_fields": "voucher_type,posting_date, due_date, cheque_no",
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "title", "title_field": "title",
"track_changes": 1 "track_changes": 1
} }

View File

@ -18,7 +18,6 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
) )
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import ( from erpnext.accounts.utils import (
check_if_stock_and_account_balance_synced,
get_account_currency, get_account_currency,
get_balance_on, get_balance_on,
get_stock_accounts, get_stock_accounts,
@ -88,9 +87,6 @@ class JournalEntry(AccountsController):
self.update_inter_company_jv() self.update_inter_company_jv()
self.update_invoice_discounting() self.update_invoice_discounting()
self.update_status_for_full_and_final_statement() self.update_status_for_full_and_final_statement()
check_if_stock_and_account_balance_synced(
self.posting_date, self.company, self.doctype, self.name
)
def on_cancel(self): def on_cancel(self):
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries

View File

@ -62,7 +62,7 @@ def start_merge(docname):
) )
except Exception: except Exception:
frappe.db.rollback() frappe.db.rollback()
frappe.log_error(title=ledger_merge.name) ledger_merge.log_error("Ledger merge failed")
finally: finally:
if successful_merges == total: if successful_merges == total:
ledger_merge.db_set("status", "Success") ledger_merge.db_set("status", "Success")

View File

@ -42,12 +42,7 @@ class ModeofPayment(Document):
pos_profiles = list(map(lambda x: x[0], pos_profiles)) pos_profiles = list(map(lambda x: x[0], pos_profiles))
if pos_profiles: if pos_profiles:
message = ( message = _(
"POS Profile " "POS Profile {} contains Mode of Payment {}. Please remove them to disable this mode."
+ frappe.bold(", ".join(pos_profiles)) ).format(frappe.bold(", ".join(pos_profiles)), frappe.bold(str(self.name)))
+ " contains \ frappe.throw(message, title=_("Not Allowed"))
Mode of Payment "
+ frappe.bold(str(self.name))
+ ". Please remove them to disable this mode."
)
frappe.throw(_(message), title="Not Allowed")

View File

@ -2,9 +2,6 @@
# For license information, please see license.txt # For license information, please see license.txt
import traceback
from json import dumps
import frappe import frappe
from frappe import _, scrub from frappe import _, scrub
from frappe.model.document import Document from frappe.model.document import Document
@ -114,10 +111,13 @@ class OpeningInvoiceCreationTool(Document):
) )
or {} or {}
) )
default_currency = frappe.db.get_value(row.party_type, row.party, "default_currency")
if company_details: if company_details:
invoice.update( invoice.update(
{ {
"currency": company_details.get("default_currency"), "currency": default_currency or company_details.get("default_currency"),
"letter_head": company_details.get("default_letter_head"), "letter_head": company_details.get("default_letter_head"),
} }
) )
@ -244,11 +244,7 @@ def start_import(invoices):
except Exception: except Exception:
errors += 1 errors += 1
frappe.db.rollback() frappe.db.rollback()
message = "\n".join( doc.log_error("Opening invoice creation failed")
["Data:", dumps(d, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()]
)
frappe.log_error(title="Error while creating Opening Invoice", message=message)
frappe.db.commit()
if errors: if errors:
frappe.msgprint( frappe.msgprint(
_("You had {} errors while creating opening invoices. Check {} for more details").format( _("You had {} errors while creating opening invoices. Check {} for more details").format(

View File

@ -3,6 +3,7 @@
"creation": "2014-08-29 16:02:39.740505", "creation": "2014-08-29 16:02:39.740505",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB",
"field_order": [ "field_order": [
"company", "company",
"account" "account"
@ -11,6 +12,7 @@
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1, "in_list_view": 1,
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
@ -27,7 +29,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-04-07 18:13:08.833822", "modified": "2022-04-04 12:31:02.994197",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Party Account", "name": "Party Account",
@ -35,5 +37,6 @@
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
"states": []
} }

View File

@ -112,8 +112,6 @@ frappe.ui.form.on('Payment Entry', {
var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"]; var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
} else if (frm.doc.party_type == "Employee") { } else if (frm.doc.party_type == "Employee") {
var doctypes = ["Expense Claim", "Journal Entry"]; var doctypes = ["Expense Claim", "Journal Entry"];
} else if (frm.doc.party_type == "Student") {
var doctypes = ["Fees"];
} else { } else {
var doctypes = ["Journal Entry"]; var doctypes = ["Journal Entry"];
} }
@ -224,10 +222,7 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.total_allocated_amount > party_amount))); (frm.doc.total_allocated_amount > party_amount)));
frm.toggle_display("set_exchange_gain_loss", frm.toggle_display("set_exchange_gain_loss",
(frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount && frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount);
((frm.doc.paid_from_account_currency != company_currency ||
frm.doc.paid_to_account_currency != company_currency) &&
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency)));
frm.refresh_fields(); frm.refresh_fields();
}, },
@ -758,8 +753,7 @@ frappe.ui.form.on('Payment Entry', {
if( if(
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee")
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student")
) { ) {
if(total_positive_outstanding > total_negative_outstanding) if(total_positive_outstanding > total_negative_outstanding)
if (!frm.doc.paid_amount) if (!frm.doc.paid_amount)
@ -801,8 +795,7 @@ frappe.ui.form.on('Payment Entry', {
if ( if (
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee")
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student")
) { ) {
if(total_positive_outstanding_including_order > paid_amount) { if(total_positive_outstanding_including_order > paid_amount) {
var remaining_outstanding = total_positive_outstanding_including_order - paid_amount; var remaining_outstanding = total_positive_outstanding_including_order - paid_amount;

View File

@ -183,9 +183,7 @@ class PaymentEntry(AccountsController):
if not self.party: if not self.party:
frappe.throw(_("Party is mandatory")) frappe.throw(_("Party is mandatory"))
_party_name = ( _party_name = "title" if self.party_type == "Shareholder" else self.party_type.lower() + "_name"
"title" if self.party_type in ("Student", "Shareholder") else self.party_type.lower() + "_name"
)
self.party_name = frappe.db.get_value(self.party_type, self.party, _party_name) self.party_name = frappe.db.get_value(self.party_type, self.party, _party_name)
if self.party: if self.party:
@ -298,9 +296,7 @@ class PaymentEntry(AccountsController):
frappe.throw(_("{0} is mandatory").format(self.meta.get_label(field))) frappe.throw(_("{0} is mandatory").format(self.meta.get_label(field)))
def validate_reference_documents(self): def validate_reference_documents(self):
if self.party_type == "Student": if self.party_type == "Customer":
valid_reference_doctypes = "Fees"
elif self.party_type == "Customer":
valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning") valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
elif self.party_type == "Supplier": elif self.party_type == "Supplier":
valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry") valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry")
@ -338,8 +334,6 @@ class PaymentEntry(AccountsController):
ref_party_account = ( ref_party_account = (
get_party_account_based_on_invoice_discounting(d.reference_name) or ref_doc.debit_to get_party_account_based_on_invoice_discounting(d.reference_name) or ref_doc.debit_to
) )
elif self.party_type == "Student":
ref_party_account = ref_doc.receivable_account
elif self.party_type == "Supplier": elif self.party_type == "Supplier":
ref_party_account = ref_doc.credit_to ref_party_account = ref_doc.credit_to
elif self.party_type == "Employee": elif self.party_type == "Employee":
@ -352,6 +346,12 @@ class PaymentEntry(AccountsController):
) )
) )
if ref_doc.doctype == "Purchase Invoice" and ref_doc.get("on_hold"):
frappe.throw(
_("{0} {1} is on hold").format(d.reference_doctype, d.reference_name),
title=_("Invalid Invoice"),
)
if ref_doc.docstatus != 1: if ref_doc.docstatus != 1:
frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name)) frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name))
@ -1259,7 +1259,6 @@ def get_outstanding_reference_documents(args):
# Get all SO / PO which are not fully billed or against which full advance not paid # Get all SO / PO which are not fully billed or against which full advance not paid
orders_to_be_billed = [] orders_to_be_billed = []
if args.get("party_type") != "Student":
orders_to_be_billed = get_orders_to_be_billed( orders_to_be_billed = get_orders_to_be_billed(
args.get("posting_date"), args.get("posting_date"),
args.get("party_type"), args.get("party_type"),
@ -1272,7 +1271,7 @@ def get_outstanding_reference_documents(args):
# Get negative outstanding sales /purchase invoices # Get negative outstanding sales /purchase invoices
negative_outstanding_invoices = [] negative_outstanding_invoices = []
if args.get("party_type") not in ["Student", "Employee"] and not args.get("voucher_no"): if args.get("party_type") != "Employee" and not args.get("voucher_no"):
negative_outstanding_invoices = get_negative_outstanding_invoices( negative_outstanding_invoices = get_negative_outstanding_invoices(
args.get("party_type"), args.get("party_type"),
args.get("party"), args.get("party"),
@ -1496,9 +1495,7 @@ def get_party_details(company, party_type, party, date, cost_center=None):
account_currency = get_account_currency(party_account) account_currency = get_account_currency(party_account)
account_balance = get_balance_on(party_account, date, cost_center=cost_center) account_balance = get_balance_on(party_account, date, cost_center=cost_center)
_party_name = ( _party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name"
"title" if party_type in ("Student", "Shareholder") else party_type.lower() + "_name"
)
party_name = frappe.db.get_value(party_type, party, _party_name) party_name = frappe.db.get_value(party_type, party, _party_name)
party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center) party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center)
if party_type in ["Customer", "Supplier"]: if party_type in ["Customer", "Supplier"]:
@ -1560,7 +1557,7 @@ def get_company_defaults(company):
def get_outstanding_on_journal_entry(name): def get_outstanding_on_journal_entry(name):
res = frappe.db.sql( res = frappe.db.sql(
"SELECT " "SELECT "
'CASE WHEN party_type IN ("Customer", "Student") ' 'CASE WHEN party_type IN ("Customer") '
"THEN ifnull(sum(debit_in_account_currency - credit_in_account_currency), 0) " "THEN ifnull(sum(debit_in_account_currency - credit_in_account_currency), 0) "
"ELSE ifnull(sum(credit_in_account_currency - debit_in_account_currency), 0) " "ELSE ifnull(sum(credit_in_account_currency - debit_in_account_currency), 0) "
"END as outstanding_amount " "END as outstanding_amount "
@ -1917,8 +1914,6 @@ def set_party_type(dt):
party_type = "Supplier" party_type = "Supplier"
elif dt in ("Expense Claim", "Employee Advance", "Gratuity"): elif dt in ("Expense Claim", "Employee Advance", "Gratuity"):
party_type = "Employee" party_type = "Employee"
elif dt == "Fees":
party_type = "Student"
return party_type return party_type

View File

@ -743,6 +743,21 @@ class TestPaymentEntry(unittest.TestCase):
flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2) flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)
) )
def test_payment_entry_against_onhold_purchase_invoice(self):
pi = make_purchase_invoice()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
# block invoice after creating payment entry
# since `get_payment_entry` will not attach blocked invoice to payment
pi.block_invoice()
with self.assertRaises(frappe.ValidationError) as err:
pe.save()
self.assertTrue("is on hold" in str(err.exception).lower())
def create_payment_entry(**args): def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry") payment_entry = frappe.new_doc("Payment Entry")

View File

@ -38,6 +38,15 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
] ]
}; };
}); });
this.frm.set_query("cost_center", () => {
return {
"filters": {
"company": this.frm.doc.company,
"is_group": 0
}
}
});
} }
refresh() { refresh() {

View File

@ -24,6 +24,7 @@
"invoice_limit", "invoice_limit",
"payment_limit", "payment_limit",
"bank_cash_account", "bank_cash_account",
"cost_center",
"sec_break1", "sec_break1",
"invoices", "invoices",
"column_break_15", "column_break_15",
@ -178,13 +179,19 @@
{ {
"fieldname": "column_break_11", "fieldname": "column_break_11",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
"icon": "icon-resize-horizontal", "icon": "icon-resize-horizontal",
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-10-04 20:27:11.114194", "modified": "2022-04-29 15:37:10.246831",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Reconciliation", "name": "Payment Reconciliation",
@ -209,5 +216,6 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -332,6 +332,9 @@ class PaymentReconciliation(Document):
def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False): def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False):
condition = " and company = '{0}' ".format(self.company) condition = " and company = '{0}' ".format(self.company)
if self.get("cost_center") and (get_invoices or get_payments or get_return_invoices):
condition = " and cost_center = '{0}' ".format(self.cost_center)
if get_invoices: if get_invoices:
condition += ( condition += (
" and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date)) " and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date))
@ -350,9 +353,13 @@ class PaymentReconciliation(Document):
) )
if self.minimum_invoice_amount: if self.minimum_invoice_amount:
condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_invoice_amount)) condition += " and {dr_or_cr} >= {amount}".format(
dr_or_cr=dr_or_cr, amount=flt(self.minimum_invoice_amount)
)
if self.maximum_invoice_amount: if self.maximum_invoice_amount:
condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_invoice_amount)) condition += " and {dr_or_cr} <= {amount}".format(
dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount)
)
elif get_return_invoices: elif get_return_invoices:
condition = " and doc.company = '{0}' ".format(self.company) condition = " and doc.company = '{0}' ".format(self.company)
@ -367,15 +374,19 @@ class PaymentReconciliation(Document):
else "" else ""
) )
dr_or_cr = ( dr_or_cr = (
"gl.debit_in_account_currency" "debit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable" if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "gl.credit_in_account_currency" else "credit_in_account_currency"
) )
if self.minimum_invoice_amount: if self.minimum_invoice_amount:
condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_payment_amount)) condition += " and gl.{dr_or_cr} >= {amount}".format(
dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount)
)
if self.maximum_invoice_amount: if self.maximum_invoice_amount:
condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_payment_amount)) condition += " and gl.{dr_or_cr} <= {amount}".format(
dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount)
)
else: else:
condition += ( condition += (

View File

@ -1,9 +1,96 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
# import frappe
import unittest import unittest
import frappe
from frappe.utils import add_days, getdate
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
class TestPaymentReconciliation(unittest.TestCase): class TestPaymentReconciliation(unittest.TestCase):
pass @classmethod
def setUpClass(cls):
make_customer()
make_invoice_and_payment()
def test_payment_reconciliation(self):
payment_reco = frappe.get_doc("Payment Reconciliation")
payment_reco.company = "_Test Company"
payment_reco.party_type = "Customer"
payment_reco.party = "_Test Payment Reco Customer"
payment_reco.receivable_payable_account = "Debtors - _TC"
payment_reco.from_invoice_date = add_days(getdate(), -1)
payment_reco.to_invoice_date = getdate()
payment_reco.from_payment_date = add_days(getdate(), -1)
payment_reco.to_payment_date = getdate()
payment_reco.maximum_invoice_amount = 1000
payment_reco.maximum_payment_amount = 1000
payment_reco.invoice_limit = 10
payment_reco.payment_limit = 10
payment_reco.bank_cash_account = "_Test Bank - _TC"
payment_reco.cost_center = "_Test Cost Center - _TC"
payment_reco.get_unreconciled_entries()
self.assertEqual(len(payment_reco.get("invoices")), 1)
self.assertEqual(len(payment_reco.get("payments")), 1)
payment_entry = payment_reco.get("payments")[0].reference_name
invoice = payment_reco.get("invoices")[0].invoice_number
payment_reco.allocate_entries(
{
"payments": [payment_reco.get("payments")[0].as_dict()],
"invoices": [payment_reco.get("invoices")[0].as_dict()],
}
)
payment_reco.reconcile()
payment_entry_doc = frappe.get_doc("Payment Entry", payment_entry)
self.assertEqual(payment_entry_doc.get("references")[0].reference_name, invoice)
def make_customer():
if not frappe.db.get_value("Customer", "_Test Payment Reco Customer"):
frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "_Test Payment Reco Customer",
"customer_type": "Individual",
"customer_group": "_Test Customer Group",
"territory": "_Test Territory",
}
).insert()
def make_invoice_and_payment():
si = create_sales_invoice(
customer="_Test Payment Reco Customer", qty=1, rate=690, do_not_save=True
)
si.cost_center = "_Test Cost Center - _TC"
si.save()
si.submit()
pe = frappe.get_doc(
{
"doctype": "Payment Entry",
"payment_type": "Receive",
"party_type": "Customer",
"party": "_Test Payment Reco Customer",
"company": "_Test Company",
"paid_from_account_currency": "INR",
"paid_to_account_currency": "INR",
"source_exchange_rate": 1,
"target_exchange_rate": 1,
"reference_no": "1",
"reference_date": getdate(),
"received_amount": 690,
"paid_amount": 690,
"paid_from": "Debtors - _TC",
"paid_to": "_Test Bank - _TC",
"cost_center": "_Test Cost Center - _TC",
}
)
pe.insert()
pe.submit()

View File

@ -64,13 +64,15 @@ frappe.ui.form.on('POS Closing Entry', {
pos_opening_entry(frm) { pos_opening_entry(frm) {
if (frm.doc.pos_opening_entry && frm.doc.period_start_date && frm.doc.period_end_date && frm.doc.user) { if (frm.doc.pos_opening_entry && frm.doc.period_start_date && frm.doc.period_end_date && frm.doc.user) {
reset_values(frm); reset_values(frm);
frm.trigger("set_opening_amounts"); frappe.run_serially([
frm.trigger("get_pos_invoices"); () => frm.trigger("set_opening_amounts"),
() => frm.trigger("get_pos_invoices")
]);
} }
}, },
set_opening_amounts(frm) { set_opening_amounts(frm) {
frappe.db.get_doc("POS Opening Entry", frm.doc.pos_opening_entry) return frappe.db.get_doc("POS Opening Entry", frm.doc.pos_opening_entry)
.then(({ balance_details }) => { .then(({ balance_details }) => {
balance_details.forEach(detail => { balance_details.forEach(detail => {
frm.add_child("payment_reconciliation", { frm.add_child("payment_reconciliation", {
@ -83,7 +85,7 @@ frappe.ui.form.on('POS Closing Entry', {
}, },
get_pos_invoices(frm) { get_pos_invoices(frm) {
frappe.call({ return frappe.call({
method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices', method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices',
args: { args: {
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date), start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),

View File

@ -61,13 +61,13 @@ class POSProfile(Document):
if len(item_groups) != len(set(item_groups)): if len(item_groups) != len(set(item_groups)):
frappe.throw( frappe.throw(
_("Duplicate item group found in the item group table"), title="Duplicate Item Group" _("Duplicate item group found in the item group table"), title=_("Duplicate Item Group")
) )
if len(customer_groups) != len(set(customer_groups)): if len(customer_groups) != len(set(customer_groups)):
frappe.throw( frappe.throw(
_("Duplicate customer group found in the cutomer group table"), _("Duplicate customer group found in the cutomer group table"),
title="Duplicate Customer Group", title=_("Duplicate Customer Group"),
) )
def validate_payment_methods(self): def validate_payment_methods(self):

View File

@ -35,6 +35,7 @@ class PricingRule(Document):
self.margin_rate_or_amount = 0.0 self.margin_rate_or_amount = 0.0
def validate_duplicate_apply_on(self): def validate_duplicate_apply_on(self):
if self.apply_on != "Transaction":
field = apply_on_dict.get(self.apply_on) field = apply_on_dict.get(self.apply_on)
values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field] values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field]
if len(values) != len(set(values)): if len(values) != len(set(values)):

View File

@ -11,7 +11,7 @@ from erpnext.accounts.deferred_revenue import (
convert_deferred_expense_to_expense, convert_deferred_expense_to_expense,
convert_deferred_revenue_to_income, convert_deferred_revenue_to_income,
) )
from erpnext.accounts.general_ledger import make_reverse_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
class ProcessDeferredAccounting(Document): class ProcessDeferredAccounting(Document):
@ -34,4 +34,4 @@ class ProcessDeferredAccounting(Document):
filters={"against_voucher_type": self.doctype, "against_voucher": self.name}, filters={"against_voucher_type": self.doctype, "against_voucher": self.name},
) )
make_reverse_gl_entries(gl_entries=gl_entries) make_gl_entries(gl_entries=gl_entries, cancel=1)

View File

@ -8,7 +8,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
}, },
refresh: function(frm){ refresh: function(frm){
if(!frm.doc.__islocal) { if(!frm.doc.__islocal) {
frm.add_custom_button('Send Emails',function(){ frm.add_custom_button(__('Send Emails'), function(){
frappe.call({ frappe.call({
method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_emails", method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_emails",
args: { args: {
@ -24,7 +24,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
} }
}); });
}); });
frm.add_custom_button('Download',function(){ frm.add_custom_button(__('Download'), function(){
var url = frappe.urllib.get_full_url( var url = frappe.urllib.get_full_url(
'/api/method/erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.download_statements?' '/api/method/erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.download_statements?'
+ 'document_name='+encodeURIComponent(frm.doc.name)) + 'document_name='+encodeURIComponent(frm.doc.name))

View File

@ -34,6 +34,7 @@ class ProcessStatementOfAccounts(Document):
frappe.throw(_("Customers not selected.")) frappe.throw(_("Customers not selected."))
if self.enable_auto_email: if self.enable_auto_email:
if self.start_date and getdate(self.start_date) >= getdate(today()):
self.to_date = self.start_date self.to_date = self.start_date
self.from_date = add_months(self.to_date, -1 * self.filter_duration) self.from_date = add_months(self.to_date, -1 * self.filter_duration)

View File

@ -30,6 +30,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
onload() { onload() {
super.onload(); super.onload();
// Ignore linked advances
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry'];
if(!this.frm.doc.__islocal) { if(!this.frm.doc.__islocal) {
// show credit_to in print format // show credit_to in print format
if(!this.frm.doc.supplier && this.frm.doc.credit_to) { if(!this.frm.doc.supplier && this.frm.doc.credit_to) {
@ -570,6 +573,10 @@ frappe.ui.form.on("Purchase Invoice", {
}); });
}, },
is_subcontracted: function(frm) {
frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted);
},
update_stock: function(frm) { update_stock: function(frm) {
hide_fields(frm.doc); hide_fields(frm.doc);
frm.fields_dict.items.grid.toggle_reqd("item_code", frm.doc.update_stock? true: false); frm.fields_dict.items.grid.toggle_reqd("item_code", frm.doc.update_stock? true: false);

View File

@ -668,7 +668,7 @@ class PurchaseInvoice(BuyingController):
exchange_rate_map, net_rate_map = get_purchase_document_details(self) exchange_rate_map, net_rate_map = get_purchase_document_details(self)
enable_discount_accounting = cint( enable_discount_accounting = cint(
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
) )
provisional_accounting_for_non_stock_items = cint( provisional_accounting_for_non_stock_items = cint(
frappe.db.get_value( frappe.db.get_value(
@ -810,7 +810,9 @@ class PurchaseInvoice(BuyingController):
if provisional_accounting_for_non_stock_items: if provisional_accounting_for_non_stock_items:
if item.purchase_receipt: if item.purchase_receipt:
provisional_account = self.get_company_default("default_provisional_account") provisional_account = frappe.db.get_value(
"Purchase Receipt Item", item.pr_detail, "provisional_expense_account"
) or self.get_company_default("default_provisional_account")
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt) purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
if not purchase_receipt_doc: if not purchase_receipt_doc:
@ -833,7 +835,7 @@ class PurchaseInvoice(BuyingController):
if expense_booked_in_pr: if expense_booked_in_pr:
# Intentionally passing purchase invoice item to handle partial billing # Intentionally passing purchase invoice item to handle partial billing
purchase_receipt_doc.add_provisional_gl_entry( purchase_receipt_doc.add_provisional_gl_entry(
item, gl_entries, self.posting_date, reverse=1 item, gl_entries, self.posting_date, provisional_account, reverse=1
) )
if not self.is_internal_transfer(): if not self.is_internal_transfer():
@ -1156,7 +1158,7 @@ class PurchaseInvoice(BuyingController):
# tax table gl entries # tax table gl entries
valuation_tax = {} valuation_tax = {}
enable_discount_accounting = cint( enable_discount_accounting = cint(
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
) )
for tax in self.get("taxes"): for tax in self.get("taxes"):
@ -1249,7 +1251,7 @@ class PurchaseInvoice(BuyingController):
def enable_discount_accounting(self): def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"): if not hasattr(self, "_enable_discount_accounting"):
self._enable_discount_accounting = cint( self._enable_discount_accounting = cint(
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
) )
return self._enable_discount_accounting return self._enable_discount_accounting
@ -1366,7 +1368,9 @@ class PurchaseInvoice(BuyingController):
if ( if (
not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment
): ):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company) round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name
)
gl_entries.append( gl_entries.append(
self.get_gl_dict( self.get_gl_dict(

View File

@ -5,6 +5,7 @@
import unittest import unittest
import frappe import frappe
from frappe.tests.utils import change_settings
from frappe.utils import add_days, cint, flt, getdate, nowdate, today from frappe.utils import add_days, cint, flt, getdate, nowdate, today
import erpnext import erpnext
@ -336,8 +337,8 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
@change_settings("Buying Settings", {"enable_discount_accounting": 1})
def test_purchase_invoice_with_discount_accounting_enabled(self): def test_purchase_invoice_with_discount_accounting_enabled(self):
enable_discount_accounting()
discount_account = create_account( discount_account = create_account(
account_name="Discount Account", account_name="Discount Account",
@ -353,10 +354,10 @@ class TestPurchaseInvoice(unittest.TestCase):
] ]
check_gl_entries(self, pi.name, expected_gle, nowdate()) check_gl_entries(self, pi.name, expected_gle, nowdate())
enable_discount_accounting(enable=0)
@change_settings("Buying Settings", {"enable_discount_accounting": 1})
def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self): def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self):
enable_discount_accounting()
additional_discount_account = create_account( additional_discount_account = create_account(
account_name="Discount Account", account_name="Discount Account",
parent_account="Indirect Expenses - _TC", parent_account="Indirect Expenses - _TC",
@ -1427,7 +1428,8 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
def test_provisional_accounting_entry(self): def test_provisional_accounting_entry(self):
item = create_item("_Test Non Stock Item", is_stock_item=0) create_item("_Test Non Stock Item", is_stock_item=0)
provisional_account = create_account( provisional_account = create_account(
account_name="Provision Account", account_name="Provision Account",
parent_account="Current Liabilities - _TC", parent_account="Current Liabilities - _TC",
@ -1450,6 +1452,8 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.save() pi.save()
pi.submit() pi.submit()
self.assertEquals(pr.items[0].provisional_expense_account, "Provision Account - _TC")
# Check GLE for Purchase Invoice # Check GLE for Purchase Invoice
expected_gle = [ expected_gle = [
["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, -1)], ["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, -1)],
@ -1530,12 +1534,6 @@ def unlink_payment_on_cancel_of_invoice(enable=1):
accounts_settings.save() accounts_settings.save()
def enable_discount_accounting(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
accounts_settings.enable_discount_accounting = enable
accounts_settings.save()
def make_purchase_invoice(**args): def make_purchase_invoice(**args):
pi = frappe.new_doc("Purchase Invoice") pi = frappe.new_doc("Purchase Invoice")
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -33,7 +33,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
var me = this; var me = this;
super.onload(); super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', 'POS Closing Entry']; this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
'POS Closing Entry', 'Journal Entry', 'Payment Entry'];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format // show debit_to in print format
this.frm.set_df_property("debit_to", "print_hide", 0); this.frm.set_df_property("debit_to", "print_hide", 0);
@ -822,29 +824,6 @@ frappe.ui.form.on('Sales Invoice', {
} }
}, },
// Healthcare
patient: function(frm) {
if (frappe.boot.active_domains.includes("Healthcare")){
if(frm.doc.patient){
frappe.call({
method: "frappe.client.get_value",
args:{
doctype: "Patient",
filters: {
"name": frm.doc.patient
},
fieldname: "customer"
},
callback:function(r) {
if(r && r.message.customer){
frm.set_value("customer", r.message.customer);
}
}
});
}
}
},
project: function(frm) { project: function(frm) {
if (frm.doc.project) { if (frm.doc.project) {
frm.events.add_timesheet_data(frm, { frm.events.add_timesheet_data(frm, {
@ -974,25 +953,6 @@ frappe.ui.form.on('Sales Invoice', {
if (frm.doc.is_debit_note) { if (frm.doc.is_debit_note) {
frm.set_df_property('return_against', 'label', __('Adjustment Against')); frm.set_df_property('return_against', 'label', __('Adjustment Against'));
} }
if (frappe.boot.active_domains.includes("Healthcare")) {
frm.set_df_property("patient", "hidden", 0);
frm.set_df_property("patient_name", "hidden", 0);
frm.set_df_property("ref_practitioner", "hidden", 0);
if (cint(frm.doc.docstatus==0) && cur_frm.page.current_view_name!=="pos" && !frm.doc.is_return) {
frm.add_custom_button(__('Healthcare Services'), function() {
get_healthcare_services_to_invoice(frm);
},__("Get Items From"));
frm.add_custom_button(__('Prescriptions'), function() {
get_drugs_to_invoice(frm);
},__("Get Items From"));
}
}
else {
frm.set_df_property("patient", "hidden", 1);
frm.set_df_property("patient_name", "hidden", 1);
frm.set_df_property("ref_practitioner", "hidden", 1);
}
}, },
create_invoice_discounting: function(frm) { create_invoice_discounting: function(frm) {

View File

@ -1051,7 +1051,7 @@ class SalesInvoice(SellingController):
def make_tax_gl_entries(self, gl_entries): def make_tax_gl_entries(self, gl_entries):
enable_discount_accounting = cint( enable_discount_accounting = cint(
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
) )
for tax in self.get("taxes"): for tax in self.get("taxes"):
@ -1097,7 +1097,7 @@ class SalesInvoice(SellingController):
def make_item_gl_entries(self, gl_entries): def make_item_gl_entries(self, gl_entries):
# income account gl entries # income account gl entries
enable_discount_accounting = cint( enable_discount_accounting = cint(
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
) )
for item in self.get("items"): for item in self.get("items"):
@ -1276,7 +1276,7 @@ class SalesInvoice(SellingController):
def enable_discount_accounting(self): def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"): if not hasattr(self, "_enable_discount_accounting"):
self._enable_discount_accounting = cint( self._enable_discount_accounting = cint(
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
) )
return self._enable_discount_accounting return self._enable_discount_accounting
@ -1412,7 +1412,7 @@ class SalesInvoice(SellingController):
) )
) )
else: else:
frappe.throw(_("Select change amount account"), title="Mandatory Field") frappe.throw(_("Select change amount account"), title=_("Mandatory Field"))
def make_write_off_gl_entry(self, gl_entries): def make_write_off_gl_entry(self, gl_entries):
# write off entries, applicable if only pos # write off entries, applicable if only pos
@ -1466,7 +1466,9 @@ class SalesInvoice(SellingController):
and self.base_rounding_adjustment and self.base_rounding_adjustment
and not self.is_internal_transfer() and not self.is_internal_transfer()
): ):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company) round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Sales Invoice", self.name
)
gl_entries.append( gl_entries.append(
self.get_gl_dict( self.get_gl_dict(

View File

@ -12,7 +12,10 @@ def get_data():
"Sales Invoice": "return_against", "Sales Invoice": "return_against",
"Auto Repeat": "reference_document", "Auto Repeat": "reference_document",
}, },
"internal_links": {"Sales Order": ["items", "sales_order"]}, "internal_links": {
"Sales Order": ["items", "sales_order"],
"Timesheet": ["timesheets", "time_sheet"],
},
"transactions": [ "transactions": [
{ {
"label": _("Payment"), "label": _("Payment"),

View File

@ -7,6 +7,7 @@ import unittest
import frappe import frappe
from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe.tests.utils import change_settings
from frappe.utils import add_days, flt, getdate, nowdate from frappe.utils import add_days, flt, getdate, nowdate
import erpnext import erpnext
@ -1977,6 +1978,13 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][2], gle.credit) self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_rounding_adjustment_3(self): def test_rounding_adjustment_3(self):
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
disable_dimension,
)
create_dimension()
si = create_sales_invoice(do_not_save=True) si = create_sales_invoice(do_not_save=True)
si.items = [] si.items = []
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]: for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
@ -2004,6 +2012,10 @@ class TestSalesInvoice(unittest.TestCase):
"included_in_print_rate": 1, "included_in_print_rate": 1,
}, },
) )
si.cost_center = "_Test Cost Center 2 - _TC"
si.location = "Block 1"
si.save() si.save()
si.submit() si.submit()
self.assertEqual(si.net_total, 4007.16) self.assertEqual(si.net_total, 4007.16)
@ -2039,6 +2051,18 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(debit_credit_diff, 0) self.assertEqual(debit_credit_diff, 0)
round_off_gle = frappe.db.get_value(
"GL Entry",
{"voucher_type": "Sales Invoice", "voucher_no": si.name, "account": "Round Off - _TC"},
["cost_center", "location"],
as_dict=1,
)
self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC")
self.assertEqual(round_off_gle.location, "Block 1")
disable_dimension()
def test_sales_invoice_with_shipping_rule(self): def test_sales_invoice_with_shipping_rule(self):
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
@ -2240,6 +2264,14 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, "2019-01-30") check_gl_entries(self, si.name, expected_gle, "2019-01-30")
def test_deferred_revenue_missing_account(self):
si = create_sales_invoice(posting_date="2019-01-10", do_not_submit=True)
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-10"
si.items[0].service_end_date = "2019-03-15"
self.assertRaises(frappe.ValidationError, si.save)
def test_fixed_deferred_revenue(self): def test_fixed_deferred_revenue(self):
deferred_account = create_account( deferred_account = create_account(
account_name="Deferred Revenue", account_name="Deferred Revenue",
@ -2616,6 +2648,7 @@ class TestSalesInvoice(unittest.TestCase):
# reset # reset
einvoice_settings = frappe.get_doc("E Invoice Settings") einvoice_settings = frappe.get_doc("E Invoice Settings")
einvoice_settings.enable = 0 einvoice_settings.enable = 0
einvoice_settings.save()
frappe.flags.country = country frappe.flags.country = country
def test_einvoice_json(self): def test_einvoice_json(self):
@ -2676,12 +2709,8 @@ class TestSalesInvoice(unittest.TestCase):
sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC" sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC"
) )
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_sales_invoice_with_discount_accounting_enabled(self): def test_sales_invoice_with_discount_accounting_enabled(self):
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
enable_discount_accounting,
)
enable_discount_accounting()
discount_account = create_account( discount_account = create_account(
account_name="Discount Account", account_name="Discount Account",
@ -2697,14 +2726,10 @@ class TestSalesInvoice(unittest.TestCase):
] ]
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
enable_discount_accounting(enable=0)
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self): def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self):
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
enable_discount_accounting,
)
enable_discount_accounting()
additional_discount_account = create_account( additional_discount_account = create_account(
account_name="Discount Account", account_name="Discount Account",
parent_account="Indirect Expenses - _TC", parent_account="Indirect Expenses - _TC",
@ -2735,7 +2760,6 @@ class TestSalesInvoice(unittest.TestCase):
] ]
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
enable_discount_accounting(enable=0)
def test_asset_depreciation_on_sale_with_pro_rata(self): def test_asset_depreciation_on_sale_with_pro_rata(self):
""" """
@ -3104,7 +3128,7 @@ class TestSalesInvoice(unittest.TestCase):
acc_settings = frappe.get_single("Accounts Settings") acc_settings = frappe.get_single("Accounts Settings")
acc_settings.book_deferred_entries_via_journal_entry = 0 acc_settings.book_deferred_entries_via_journal_entry = 0
acc_settings.submit_journal_entriessubmit_journal_entries = 0 acc_settings.submit_journal_entries = 0
acc_settings.save() acc_settings.save()
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None)
@ -3116,6 +3140,95 @@ class TestSalesInvoice(unittest.TestCase):
si.reload() si.reload()
self.assertTrue(si.items[0].serial_no) self.assertTrue(si.items[0].serial_no)
def test_sales_invoice_with_disabled_account(self):
try:
account = frappe.get_doc("Account", "VAT 5% - _TC")
account.disabled = 1
account.save()
si = create_sales_invoice(do_not_save=True)
si.posting_date = add_days(getdate(), 1)
si.taxes = []
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "VAT 5% - _TC",
"cost_center": "Main - _TC",
"description": "VAT @ 5.0",
"rate": 9,
},
)
si.save()
with self.assertRaises(frappe.ValidationError) as err:
si.submit()
self.assertTrue(
"Cannot create accounting entries against disabled accounts" in str(err.exception)
)
finally:
account.disabled = 0
account.save()
def test_gain_loss_with_advance_entry(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
unlink_enabled = frappe.db.get_value(
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
)
frappe.db.set_value(
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1
)
jv = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False)
jv.accounts[0].exchange_rate = 70
jv.accounts[0].credit_in_account_currency = 100
jv.accounts[0].party_type = "Customer"
jv.accounts[0].party = "_Test Customer USD"
jv.save()
jv.submit()
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=75,
do_not_save=1,
rate=100,
)
si.append(
"advances",
{
"reference_type": "Journal Entry",
"reference_name": jv.name,
"reference_row": jv.accounts[0].name,
"advance_amount": 100,
"allocated_amount": 100,
"ref_exchange_rate": 70,
},
)
si.save()
si.submit()
expected_gle = [
["_Test Receivable USD - _TC", 7500.0, 500],
["Exchange Gain/Loss - _TC", 500.0, 0.0],
["Sales - _TC", 0.0, 7500.0],
]
check_gl_entries(self, si.name, expected_gle, nowdate())
frappe.db.set_value(
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
)
def get_sales_invoice_for_e_invoice(): def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill() si = make_sales_invoice_for_ewaybill()

View File

@ -722,9 +722,7 @@ def process(data):
frappe.db.commit() frappe.db.commit()
except frappe.ValidationError: except frappe.ValidationError:
frappe.db.rollback() frappe.db.rollback()
frappe.db.begin() subscription.log_error("Subscription failed")
frappe.log_error(frappe.get_traceback())
frappe.db.commit()
@frappe.whitelist() @frappe.whitelist()

View File

@ -163,12 +163,15 @@ def get_party_details(party, party_type, args=None):
def get_tax_template(posting_date, args): def get_tax_template(posting_date, args):
"""Get matching tax rule""" """Get matching tax rule"""
args = frappe._dict(args) args = frappe._dict(args)
conditions = [ conditions = []
"""(from_date is null or from_date <= '{0}')
and (to_date is null or to_date >= '{0}')""".format( if posting_date:
posting_date conditions.append(
f"""(from_date is null or from_date <= '{posting_date}')
and (to_date is null or to_date >= '{posting_date}')"""
) )
] else:
conditions.append("(from_date is null) and (to_date is null)")
conditions.append( conditions.append(
"ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category")))) "ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category"))))

View File

@ -34,7 +34,9 @@ class TaxWithholdingCategory(Document):
def validate_thresholds(self): def validate_thresholds(self):
for d in self.get("rates"): for d in self.get("rates"):
if d.cumulative_threshold and d.cumulative_threshold < d.single_threshold: if (
d.cumulative_threshold and d.single_threshold and d.cumulative_threshold < d.single_threshold
):
frappe.throw( frappe.throw(
_("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format( _("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format(
d.idx d.idx

View File

@ -31,6 +31,7 @@ def make_gl_entries(
if gl_map: if gl_map:
if not cancel: if not cancel:
validate_accounting_period(gl_map) validate_accounting_period(gl_map)
validate_disabled_accounts(gl_map)
gl_map = process_gl_map(gl_map, merge_entries) gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1: if gl_map and len(gl_map) > 1:
save_entries(gl_map, adv_adj, update_outstanding, from_repost) save_entries(gl_map, adv_adj, update_outstanding, from_repost)
@ -45,6 +46,26 @@ def make_gl_entries(
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding) make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
def validate_disabled_accounts(gl_map):
accounts = [d.account for d in gl_map if d.account]
Account = frappe.qb.DocType("Account")
disabled_accounts = (
frappe.qb.from_(Account)
.where(Account.name.isin(accounts) & Account.disabled == 1)
.select(Account.name, Account.disabled)
).run(as_dict=True)
if disabled_accounts:
account_list = "<br>"
account_list += ", ".join([frappe.bold(d.name) for d in disabled_accounts])
frappe.throw(
_("Cannot create accounting entries against disabled accounts: {0}").format(account_list),
title=_("Disabled Account Selected"),
)
def validate_accounting_period(gl_map): def validate_accounting_period(gl_map):
accounting_periods = frappe.db.sql( accounting_periods = frappe.db.sql(
""" SELECT """ SELECT
@ -355,7 +376,7 @@ def raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_
def make_round_off_gle(gl_map, debit_credit_diff, precision): def make_round_off_gle(gl_map, debit_credit_diff, precision):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
gl_map[0].company gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
) )
round_off_account_exists = False round_off_account_exists = False
round_off_gle = frappe._dict() round_off_gle = frappe._dict()
@ -392,14 +413,43 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
} }
) )
update_accounting_dimensions(round_off_gle)
if not round_off_account_exists: if not round_off_account_exists:
gl_map.append(round_off_gle) gl_map.append(round_off_gle)
def get_round_off_account_and_cost_center(company): def update_accounting_dimensions(round_off_gle):
dimensions = get_accounting_dimensions()
meta = frappe.get_meta(round_off_gle["voucher_type"])
has_all_dimensions = True
for dimension in dimensions:
if not meta.has_field(dimension):
has_all_dimensions = False
if dimensions and has_all_dimensions:
dimension_values = frappe.db.get_value(
round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions, as_dict=1
)
for dimension in dimensions:
round_off_gle[dimension] = dimension_values.get(dimension)
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no):
round_off_account, round_off_cost_center = frappe.get_cached_value( round_off_account, round_off_cost_center = frappe.get_cached_value(
"Company", company, ["round_off_account", "round_off_cost_center"] "Company", company, ["round_off_account", "round_off_cost_center"]
) or [None, None] ) or [None, None]
meta = frappe.get_meta(voucher_type)
# Give first preference to parent cost center for round off GLE
if meta.has_field("cost_center"):
parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center")
if parent_cost_center:
round_off_cost_center = parent_cost_center
if not round_off_account: if not round_off_account:
frappe.throw(_("Please mention Round Off Account in Company")) frappe.throw(_("Please mention Round Off Account in Company"))

View File

@ -831,9 +831,9 @@ def get_party_shipping_address(doctype, name):
"where " "where "
"dl.link_doctype=%s " "dl.link_doctype=%s "
"and dl.link_name=%s " "and dl.link_name=%s "
'and dl.parenttype="Address" ' "and dl.parenttype='Address' "
"and ifnull(ta.disabled, 0) = 0 and" "and ifnull(ta.disabled, 0) = 0 and"
'(ta.address_type="Shipping" or ta.is_shipping_address=1) ' "(ta.address_type='Shipping' or ta.is_shipping_address=1) "
"order by ta.is_shipping_address desc, ta.address_type desc limit 1", "order by ta.is_shipping_address desc, ta.address_type desc limit 1",
(doctype, name), (doctype, name),
) )
@ -881,11 +881,11 @@ def get_default_contact(doctype, name):
""" """
SELECT dl.parent, c.is_primary_contact, c.is_billing_contact SELECT dl.parent, c.is_primary_contact, c.is_billing_contact
FROM `tabDynamic Link` dl FROM `tabDynamic Link` dl
INNER JOIN tabContact c ON c.name = dl.parent INNER JOIN `tabContact` c ON c.name = dl.parent
WHERE WHERE
dl.link_doctype=%s AND dl.link_doctype=%s AND
dl.link_name=%s AND dl.link_name=%s AND
dl.parenttype = "Contact" dl.parenttype = 'Contact'
ORDER BY is_primary_contact DESC, is_billing_contact DESC ORDER BY is_primary_contact DESC, is_billing_contact DESC
""", """,
(doctype, name), (doctype, name),

View File

@ -1,7 +1,8 @@
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%} {%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
<div class="page-break"> <div class="page-break">
{% if doc.signed_einvoice %}
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}> <div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
{% if letter_head and not no_letterhead %} {% if letter_head and not no_letterhead %}
<div class="letter-head">{{ letter_head }}</div> <div class="letter-head">{{ letter_head }}</div>
@ -170,4 +171,10 @@
</tbody> </tbody>
</table> </table>
</div> </div>
{% else %}
<div class="text-center" style="color: var(--gray-500); font-size: 14px;">
You must generate IRN before you can preview GST E-Invoice.
</div>
{% endif %}
</div> </div>

View File

@ -201,17 +201,17 @@ def get_report_summary(
net_provisional_profit_loss += provisional_profit_loss.get(key) net_provisional_profit_loss += provisional_profit_loss.get(key)
return [ return [
{"value": net_asset, "label": "Total Asset", "datatype": "Currency", "currency": currency}, {"value": net_asset, "label": _("Total Asset"), "datatype": "Currency", "currency": currency},
{ {
"value": net_liability, "value": net_liability,
"label": "Total Liability", "label": _("Total Liability"),
"datatype": "Currency", "datatype": "Currency",
"currency": currency, "currency": currency,
}, },
{"value": net_equity, "label": "Total Equity", "datatype": "Currency", "currency": currency}, {"value": net_equity, "label": _("Total Equity"), "datatype": "Currency", "currency": currency},
{ {
"value": net_provisional_profit_loss, "value": net_provisional_profit_loss,
"label": "Provisional Profit / Loss (Credit)", "label": _("Provisional Profit / Loss (Credit)"),
"indicator": "Green" if net_provisional_profit_loss > 0 else "Red", "indicator": "Green" if net_provisional_profit_loss > 0 else "Red",
"datatype": "Currency", "datatype": "Currency",
"currency": currency, "currency": currency,

View File

@ -203,7 +203,7 @@ def get_loan_entries(filters):
posting_date = (loan_doc.posting_date).as_("posting_date") posting_date = (loan_doc.posting_date).as_("posting_date")
account = loan_doc.payment_account account = loan_doc.payment_account
entries = ( query = (
frappe.qb.from_(loan_doc) frappe.qb.from_(loan_doc)
.select( .select(
ConstantColumn(doctype).as_("payment_document"), ConstantColumn(doctype).as_("payment_document"),
@ -217,9 +217,12 @@ def get_loan_entries(filters):
.where(account == filters.get("account")) .where(account == filters.get("account"))
.where(posting_date <= getdate(filters.get("report_date"))) .where(posting_date <= getdate(filters.get("report_date")))
.where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date"))) .where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date")))
.run(as_dict=1)
) )
if doctype == "Loan Repayment":
query.where(loan_doc.repay_from_salary == 0)
entries = query.run(as_dict=1)
loan_docs.extend(entries) loan_docs.extend(entries)
return loan_docs return loan_docs

View File

@ -97,8 +97,8 @@ def get_columns(filters):
if filters["period"] == "Yearly": if filters["period"] == "Yearly":
labels = [ labels = [
_("Budget") + " " + str(year[0]), _("Budget") + " " + str(year[0]),
_("Actual ") + " " + str(year[0]), _("Actual") + " " + str(year[0]),
_("Variance ") + " " + str(year[0]), _("Variance") + " " + str(year[0]),
] ]
for label in labels: for label in labels:
columns.append( columns.append(

View File

@ -230,7 +230,7 @@ def get_columns(dimension_list):
columns.append( columns.append(
{ {
"fieldname": "total", "fieldname": "total",
"label": "Total", "label": _("Total"),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency", "options": "currency",
"width": 150, "width": 150,

View File

@ -507,7 +507,7 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters):
) )
additional_conditions.append("{0} in %({0})s".format(dimension.fieldname)) additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
else: else:
additional_conditions.append("{0} in (%({0})s)".format(dimension.fieldname)) additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else "" return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else ""

View File

@ -133,7 +133,7 @@ def set_account_currency(filters):
else: else:
account_currency = ( account_currency = (
None None
if filters.party_type in ["Employee", "Student", "Shareholder", "Member"] if filters.party_type in ["Employee", "Shareholder", "Member"]
else frappe.db.get_value(filters.party_type, filters.party[0], "default_currency") else frappe.db.get_value(filters.party_type, filters.party[0], "default_currency")
) )
@ -275,7 +275,7 @@ def get_conditions(filters):
) )
conditions.append("{0} in %({0})s".format(dimension.fieldname)) conditions.append("{0} in %({0})s".format(dimension.fieldname))
else: else:
conditions.append("{0} in (%({0})s)".format(dimension.fieldname)) conditions.append("{0} in %({0})s".format(dimension.fieldname))
return "and {}".format(" and ".join(conditions)) if conditions else "" return "and {}".format(" and ".join(conditions)) if conditions else ""
@ -435,7 +435,13 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
gle_map[group_by_value].entries.append(gle) gle_map[group_by_value].entries.append(gle)
elif group_by_voucher_consolidated: elif group_by_voucher_consolidated:
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")] keylist = [
gle.get("voucher_type"),
gle.get("voucher_no"),
gle.get("account"),
gle.get("party_type"),
gle.get("party"),
]
if filters.get("include_dimensions"): if filters.get("include_dimensions"):
for dim in accounting_dimensions: for dim in accounting_dimensions:
keylist.append(gle.get(dim)) keylist.append(gle.get(dim))

View File

@ -29,7 +29,7 @@ def get_columns():
"options": "Item Group", "options": "Item Group",
"width": 150, "width": 150,
}, },
{"fieldname": "item", "fieldtype": "Link", "options": "Item", "label": "Item", "width": 150}, {"fieldname": "item", "fieldtype": "Link", "options": "Item", "label": _("Item"), "width": 150},
{"fieldname": "item_name", "fieldtype": "Data", "label": _("Item Name"), "width": 150}, {"fieldname": "item_name", "fieldtype": "Data", "label": _("Item Name"), "width": 150},
{ {
"fieldname": "customer", "fieldname": "customer",

View File

@ -115,9 +115,9 @@ def get_columns(filters):
{"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 140}, {"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 140},
{"fieldname": "remarks", "label": _("Remarks"), "fieldtype": "Data", "width": 200}, {"fieldname": "remarks", "label": _("Remarks"), "fieldtype": "Data", "width": 200},
{"fieldname": "age", "label": _("Age"), "fieldtype": "Int", "width": 50}, {"fieldname": "age", "label": _("Age"), "fieldtype": "Int", "width": 50},
{"fieldname": "range1", "label": "0-30", "fieldtype": "Currency", "width": 140}, {"fieldname": "range1", "label": _("0-30"), "fieldtype": "Currency", "width": 140},
{"fieldname": "range2", "label": "30-60", "fieldtype": "Currency", "width": 140}, {"fieldname": "range2", "label": _("30-60"), "fieldtype": "Currency", "width": 140},
{"fieldname": "range3", "label": "60-90", "fieldtype": "Currency", "width": 140}, {"fieldname": "range3", "label": _("60-90"), "fieldtype": "Currency", "width": 140},
{"fieldname": "range4", "label": _("90 Above"), "fieldtype": "Currency", "width": 140}, {"fieldname": "range4", "label": _("90 Above"), "fieldtype": "Currency", "width": 140},
{ {
"fieldname": "delay_in_payment", "fieldname": "delay_in_payment",

View File

@ -62,7 +62,7 @@ def get_pos_entries(filters, group_by_field):
""" """
SELECT SELECT
p.posting_date, p.name as pos_invoice, p.pos_profile, p.posting_date, p.name as pos_invoice, p.pos_profile,
p.owner, p.base_grand_total as grand_total, p.base_paid_amount as paid_amount, p.owner, p.base_grand_total as grand_total, p.base_paid_amount - p.change_amount as paid_amount,
p.customer, p.is_return {select_mop_field} p.customer, p.is_return {select_mop_field}
FROM FROM
`tabPOS Invoice` p {from_sales_invoice_payment} `tabPOS Invoice` p {from_sales_invoice_payment}

View File

@ -124,11 +124,10 @@ def get_columns(invoice_list, additional_table_columns):
_("Purchase Receipt") + ":Link/Purchase Receipt:100", _("Purchase Receipt") + ":Link/Purchase Receipt:100",
{"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80}, {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80},
] ]
expense_accounts = (
tax_accounts expense_accounts = []
) = ( tax_accounts = []
expense_columns unrealized_profit_loss_accounts = []
) = tax_columns = unrealized_profit_loss_accounts = unrealized_profit_loss_account_columns = []
if invoice_list: if invoice_list:
expense_accounts = frappe.db.sql_list( expense_accounts = frappe.db.sql_list(
@ -163,10 +162,11 @@ def get_columns(invoice_list, additional_table_columns):
unrealized_profit_loss_account_columns = [ unrealized_profit_loss_account_columns = [
(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts (account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts
] ]
tax_columns = [
for account in tax_accounts: (account + ":Currency/currency:120")
if account not in expense_accounts: for account in tax_accounts
tax_columns.append(account + ":Currency/currency:120") if account not in expense_accounts
]
columns = ( columns = (
columns columns
@ -237,7 +237,7 @@ def get_conditions(filters):
else: else:
conditions += ( conditions += (
common_condition common_condition
+ "and ifnull(`tabPurchase Invoice Item`.{0}, '') in (%({0})s))".format(dimension.fieldname) + "and ifnull(`tabPurchase Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
) )
return conditions return conditions

View File

@ -405,7 +405,7 @@ def get_conditions(filters):
else: else:
conditions += ( conditions += (
common_condition common_condition
+ "and ifnull(`tabSales Invoice Item`.{0}, '') in (%({0})s))".format(dimension.fieldname) + "and ifnull(`tabSales Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
) )
return conditions return conditions

View File

@ -188,9 +188,9 @@ def get_rootwise_opening_balances(filters, report_type):
filters[dimension.fieldname] = get_dimension_with_children( filters[dimension.fieldname] = get_dimension_with_children(
dimension.document_type, filters.get(dimension.fieldname) dimension.document_type, filters.get(dimension.fieldname)
) )
additional_conditions += "and {0} in %({0})s".format(dimension.fieldname) additional_conditions += " and {0} in %({0})s".format(dimension.fieldname)
else: else:
additional_conditions += "and {0} in (%({0})s)".format(dimension.fieldname) additional_conditions += " and {0} in %({0})s".format(dimension.fieldname)
query_filters.update({dimension.fieldname: filters.get(dimension.fieldname)}) query_filters.update({dimension.fieldname: filters.get(dimension.fieldname)})

View File

@ -23,8 +23,6 @@ def execute(filters=None):
def get_data(filters, show_party_name): def get_data(filters, show_party_name):
if filters.get("party_type") in ("Customer", "Supplier", "Employee", "Member"): if filters.get("party_type") in ("Customer", "Supplier", "Employee", "Member"):
party_name_field = "{0}_name".format(frappe.scrub(filters.get("party_type"))) party_name_field = "{0}_name".format(frappe.scrub(filters.get("party_type")))
elif filters.get("party_type") == "Student":
party_name_field = "first_name"
elif filters.get("party_type") == "Shareholder": elif filters.get("party_type") == "Shareholder":
party_name_field = "title" party_name_field = "title"
else: else:

View File

@ -1,10 +1,17 @@
import unittest import unittest
import frappe
from frappe.test_runner import make_test_objects from frappe.test_runner import make_test_objects
from erpnext.accounts.party import get_party_shipping_address from erpnext.accounts.party import get_party_shipping_address
from erpnext.accounts.utils import get_future_stock_vouchers, get_voucherwise_gl_entries from erpnext.accounts.utils import (
get_future_stock_vouchers,
get_voucherwise_gl_entries,
sort_stock_vouchers_by_posting_date,
)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
class TestUtils(unittest.TestCase): class TestUtils(unittest.TestCase):
@ -47,6 +54,25 @@ class TestUtils(unittest.TestCase):
msg="get_voucherwise_gl_entries not returning expected GLes", msg="get_voucherwise_gl_entries not returning expected GLes",
) )
def test_stock_voucher_sorting(self):
vouchers = []
item = make_item().name
stock_entry = {"item": item, "to_warehouse": "_Test Warehouse - _TC", "qty": 1, "rate": 10}
se1 = make_stock_entry(posting_date="2022-01-01", **stock_entry)
se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry)
se3 = make_stock_entry(posting_date="2022-03-01", **stock_entry)
for doc in (se1, se2, se3):
vouchers.append((doc.doctype, doc.name))
vouchers.append(("Stock Entry", "Wat"))
sorted_vouchers = sort_stock_vouchers_by_posting_date(list(reversed(vouchers)))
self.assertEqual(sorted_vouchers, vouchers)
ADDRESS_RECORDS = [ ADDRESS_RECORDS = [
{ {

View File

@ -3,6 +3,7 @@
from json import loads from json import loads
from typing import List, Tuple
import frappe import frappe
import frappe.defaults import frappe.defaults
@ -18,10 +19,6 @@ from erpnext.stock import get_warehouse_account_map
from erpnext.stock.utils import get_stock_value_on from erpnext.stock.utils import get_stock_value_on
class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError):
pass
class FiscalYearError(frappe.ValidationError): class FiscalYearError(frappe.ValidationError):
pass pass
@ -1126,6 +1123,9 @@ def update_gl_entries_after(
def repost_gle_for_stock_vouchers( def repost_gle_for_stock_vouchers(
stock_vouchers, posting_date, company=None, warehouse_account=None stock_vouchers, posting_date, company=None, warehouse_account=None
): ):
if not stock_vouchers:
return
def _delete_gl_entries(voucher_type, voucher_no): def _delete_gl_entries(voucher_type, voucher_no):
frappe.db.sql( frappe.db.sql(
"""delete from `tabGL Entry` """delete from `tabGL Entry`
@ -1133,6 +1133,8 @@ def repost_gle_for_stock_vouchers(
(voucher_type, voucher_no), (voucher_type, voucher_no),
) )
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
if not warehouse_account: if not warehouse_account:
warehouse_account = get_warehouse_account_map(company) warehouse_account = get_warehouse_account_map(company)
@ -1153,6 +1155,27 @@ def repost_gle_for_stock_vouchers(
_delete_gl_entries(voucher_type, voucher_no) _delete_gl_entries(voucher_type, voucher_no)
def sort_stock_vouchers_by_posting_date(
stock_vouchers: List[Tuple[str, str]]
) -> List[Tuple[str, str]]:
sle = frappe.qb.DocType("Stock Ledger Entry")
voucher_nos = [v[1] for v in stock_vouchers]
sles = (
frappe.qb.from_(sle)
.select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
.where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
.groupby(sle.voucher_type, sle.voucher_no)
).run(as_dict=True)
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
unknown_vouchers = set(stock_vouchers) - set(sorted_vouchers)
if unknown_vouchers:
sorted_vouchers.extend(unknown_vouchers)
return sorted_vouchers
def get_future_stock_vouchers( def get_future_stock_vouchers(
posting_date, posting_time, for_warehouses=None, for_items=None, company=None posting_date, posting_time, for_warehouses=None, for_items=None, company=None
): ):
@ -1246,47 +1269,6 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
return matched return matched
def check_if_stock_and_account_balance_synced(
posting_date, company, voucher_type=None, voucher_no=None
):
if not cint(erpnext.is_perpetual_inventory_enabled(company)):
return
accounts = get_stock_accounts(company, voucher_type, voucher_no)
stock_adjustment_account = frappe.db.get_value("Company", company, "stock_adjustment_account")
for account in accounts:
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
account, posting_date, company
)
if abs(account_bal - stock_bal) > 0.1:
precision = get_field_precision(
frappe.get_meta("GL Entry").get_field("debit"),
currency=frappe.get_cached_value("Company", company, "default_currency"),
)
diff = flt(stock_bal - account_bal, precision)
error_reason = _(
"Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses as on {3}."
).format(stock_bal, account_bal, frappe.bold(account), posting_date)
error_resolution = _("Please create an adjustment Journal Entry for amount {0} on {1}").format(
frappe.bold(diff), frappe.bold(posting_date)
)
frappe.msgprint(
msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),
raise_exception=StockValueAndAccountBalanceOutOfSync,
title=_("Values Out Of Sync"),
primary_action={
"label": _("Make Journal Entry"),
"client_action": "erpnext.route_to_adjustment_jv",
"args": get_journal_entry(account, stock_adjustment_account, diff),
},
)
def get_stock_accounts(company, voucher_type=None, voucher_no=None): def get_stock_accounts(company, voucher_type=None, voucher_no=None):
stock_accounts = [ stock_accounts = [
d.name d.name

View File

@ -87,7 +87,7 @@ class AssetCategory(Document):
missing_cwip_accounts_for_company.append(get_link_to_form("Company", d.company_name)) missing_cwip_accounts_for_company.append(get_link_to_form("Company", d.company_name))
if missing_cwip_accounts_for_company: if missing_cwip_accounts_for_company:
msg = _("""To enable Capital Work in Progress Accounting, """) msg = _("""To enable Capital Work in Progress Accounting,""") + " "
msg += _("""you must select Capital Work in Progress Account in accounts table""") msg += _("""you must select Capital Work in Progress Account in accounts table""")
msg += "<br><br>" msg += "<br><br>"
msg += _("You can also set default CWIP account in Company {}").format( msg += _("You can also set default CWIP account in Company {}").format(

View File

@ -46,10 +46,9 @@ class AssetMovement(Document):
if d.target_location: if d.target_location:
frappe.throw( frappe.throw(
_( _(
"Issuing cannot be done to a location. \ "Issuing cannot be done to a location. Please enter employee who has issued Asset {0}"
Please enter employee who has issued Asset {0}"
).format(d.asset), ).format(d.asset),
title="Incorrect Movement Purpose", title=_("Incorrect Movement Purpose"),
) )
if not d.to_employee: if not d.to_employee:
frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset)) frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset))
@ -58,10 +57,9 @@ class AssetMovement(Document):
if d.to_employee: if d.to_employee:
frappe.throw( frappe.throw(
_( _(
"Transferring cannot be done to an Employee. \ "Transferring cannot be done to an Employee. Please enter location where Asset {0} has to be transferred"
Please enter location where Asset {0} has to be transferred"
).format(d.asset), ).format(d.asset),
title="Incorrect Movement Purpose", title=_("Incorrect Movement Purpose"),
) )
if not d.target_location: if not d.target_location:
frappe.throw(_("Target Location is required while transferring Asset {0}").format(d.asset)) frappe.throw(_("Target Location is required while transferring Asset {0}").format(d.asset))
@ -89,8 +87,7 @@ class AssetMovement(Document):
if d.to_employee and d.target_location: if d.to_employee and d.target_location:
frappe.throw( frappe.throw(
_( _(
"Asset {0} cannot be received at a location and \ "Asset {0} cannot be received at a location and given to employee in a single movement"
given to employee in a single movement"
).format(d.asset) ).format(d.asset)
) )

View File

@ -32,7 +32,7 @@ frappe.ui.form.on('Asset Repair', {
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.docstatus) { if (frm.doc.docstatus) {
frm.add_custom_button("View General Ledger", function() { frm.add_custom_button(__("View General Ledger"), function() {
frappe.route_options = { frappe.route_options = {
"voucher_no": frm.doc.name "voucher_no": frm.doc.name
}; };

View File

@ -37,7 +37,7 @@ class AssetValueAdjustment(Document):
_("Asset Value Adjustment cannot be posted before Asset's purchase date <b>{0}</b>.").format( _("Asset Value Adjustment cannot be posted before Asset's purchase date <b>{0}</b>.").format(
formatdate(asset_purchase_date) formatdate(asset_purchase_date)
), ),
title="Incorrect Date", title=_("Incorrect Date"),
) )
def set_difference_amount(self): def set_difference_amount(self):

View File

@ -20,6 +20,7 @@
"maintain_same_rate", "maintain_same_rate",
"allow_multiple_items", "allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice", "bill_for_rejected_quantity_in_purchase_invoice",
"enable_discount_accounting",
"subcontract", "subcontract",
"backflush_raw_materials_of_subcontract_based_on", "backflush_raw_materials_of_subcontract_based_on",
"column_break_11", "column_break_11",
@ -133,6 +134,13 @@
{ {
"fieldname": "column_break_12", "fieldname": "column_break_12",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"default": "0",
"description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account",
"fieldname": "enable_discount_accounting",
"fieldtype": "Check",
"label": "Enable Discount Accounting for Buying"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@ -140,7 +148,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2022-01-27 17:57:58.367048", "modified": "2022-04-14 15:56:42.340223",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View File

@ -5,10 +5,15 @@
import frappe import frappe
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint
class BuyingSettings(Document): class BuyingSettings(Document):
def on_update(self):
self.toggle_discount_accounting_fields()
def validate(self): def validate(self):
for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]: for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]:
frappe.db.set_default(key, self.get(key, "")) frappe.db.set_default(key, self.get(key, ""))
@ -21,3 +26,60 @@ class BuyingSettings(Document):
self.get("supp_master_name") == "Naming Series", self.get("supp_master_name") == "Naming Series",
hide_name_field=False, hide_name_field=False,
) )
def toggle_discount_accounting_fields(self):
enable_discount_accounting = cint(self.enable_discount_accounting)
make_property_setter(
"Purchase Invoice Item",
"discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
if enable_discount_accounting:
make_property_setter(
"Purchase Invoice Item",
"discount_account",
"mandatory_depends_on",
"eval: doc.discount_amount",
"Code",
validate_fields_for_doctype=False,
)
else:
make_property_setter(
"Purchase Invoice Item",
"discount_account",
"mandatory_depends_on",
"",
"Code",
validate_fields_for_doctype=False,
)
make_property_setter(
"Purchase Invoice",
"additional_discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
if enable_discount_accounting:
make_property_setter(
"Purchase Invoice",
"additional_discount_account",
"mandatory_depends_on",
"eval: doc.discount_amount",
"Code",
validate_fields_for_doctype=False,
)
else:
make_property_setter(
"Purchase Invoice",
"additional_discount_account",
"mandatory_depends_on",
"",
"Code",
validate_fields_for_doctype=False,
)

View File

@ -25,6 +25,10 @@
"order_confirmation_no", "order_confirmation_no",
"order_confirmation_date", "order_confirmation_date",
"amended_from", "amended_from",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"drop_ship", "drop_ship",
"customer", "customer",
"customer_name", "customer_name",
@ -1135,6 +1139,28 @@
{ {
"fieldname": "section_break_45", "fieldname": "section_break_45",
"fieldtype": "Section Break" "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": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",

View File

@ -31,7 +31,7 @@ frappe.ui.form.on("Request for Quotation",{
if (frm.doc.docstatus === 1) { if (frm.doc.docstatus === 1) {
frm.add_custom_button(__('Supplier Quotation'), frm.add_custom_button(__('Supplier Quotation'),
function(){ frm.trigger("make_suppplier_quotation") }, __("Create")); function(){ frm.trigger("make_supplier_quotation") }, __("Create"));
frm.add_custom_button(__("Send Emails to Suppliers"), function() { frm.add_custom_button(__("Send Emails to Suppliers"), function() {
@ -87,16 +87,24 @@ frappe.ui.form.on("Request for Quotation",{
}, },
make_suppplier_quotation: function(frm) { make_supplier_quotation: function(frm) {
var doc = frm.doc; var doc = frm.doc;
var dialog = new frappe.ui.Dialog({ var dialog = new frappe.ui.Dialog({
title: __("Create Supplier Quotation"), title: __("Create Supplier Quotation"),
fields: [ fields: [
{ "fieldtype": "Select", "label": __("Supplier"), { "fieldtype": "Link",
"label": __("Supplier"),
"fieldname": "supplier", "fieldname": "supplier",
"options": doc.suppliers.map(d => d.supplier), "options": 'Supplier',
"reqd": 1, "reqd": 1,
"default": doc.suppliers.length === 1 ? doc.suppliers[0].supplier_name : "" }, get_query: () => {
return {
filters: [
["Supplier", "name", "in", frm.doc.suppliers.map((row) => {return row.supplier;})]
]
}
}
}
], ],
primary_action_label: __("Create"), primary_action_label: __("Create"),
primary_action: (args) => { primary_action: (args) => {

View File

@ -32,7 +32,9 @@
"terms", "terms",
"printing_settings", "printing_settings",
"select_print_heading", "select_print_heading",
"letter_head" "letter_head",
"more_info",
"opportunity"
], ],
"fields": [ "fields": [
{ {
@ -193,6 +195,23 @@
"options": "Letter Head", "options": "Letter Head",
"print_hide": 1 "print_hide": 1
}, },
{
"collapsible": 1,
"fieldname": "more_info",
"fieldtype": "Section Break",
"label": "More Information",
"oldfieldtype": "Section Break",
"options": "fa fa-file-text",
"print_hide": 1
},
{
"fieldname": "opportunity",
"fieldtype": "Link",
"label": "Opportunity",
"options": "Opportunity",
"print_hide": 1,
"read_only": 1
},
{ {
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
@ -258,7 +277,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-11-24 17:47:49.909000", "modified": "2022-04-06 17:47:49.909000",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Request for Quotation", "name": "Request for Quotation",

View File

@ -65,7 +65,6 @@ class TestRequestforQuotation(FrappeTestCase):
) )
sq.submit() sq.submit()
frappe.form_dict = frappe.local("form_dict")
frappe.form_dict.name = rfq.name frappe.form_dict.name = rfq.name
self.assertEqual(check_supplier_has_docname_access(supplier_wt_appos[0].get("supplier")), True) self.assertEqual(check_supplier_has_docname_access(supplier_wt_appos[0].get("supplier")), True)

View File

@ -18,16 +18,16 @@
"tax_id", "tax_id",
"tax_category", "tax_category",
"tax_withholding_category", "tax_withholding_category",
"is_transporter",
"is_internal_supplier",
"represents_company",
"image", "image",
"column_break0", "column_break0",
"supplier_group", "supplier_group",
"supplier_type", "supplier_type",
"allow_purchase_invoice_creation_without_purchase_order", "allow_purchase_invoice_creation_without_purchase_order",
"allow_purchase_invoice_creation_without_purchase_receipt", "allow_purchase_invoice_creation_without_purchase_receipt",
"is_internal_supplier",
"represents_company",
"disabled", "disabled",
"is_transporter",
"warn_rfqs", "warn_rfqs",
"warn_pos", "warn_pos",
"prevent_rfqs", "prevent_rfqs",
@ -38,12 +38,6 @@
"default_currency", "default_currency",
"column_break_10", "column_break_10",
"default_price_list", "default_price_list",
"section_credit_limit",
"payment_terms",
"cb_21",
"on_hold",
"hold_type",
"release_date",
"address_contacts", "address_contacts",
"address_html", "address_html",
"column_break1", "column_break1",
@ -57,6 +51,12 @@
"primary_address", "primary_address",
"default_payable_accounts", "default_payable_accounts",
"accounts", "accounts",
"section_credit_limit",
"payment_terms",
"cb_21",
"on_hold",
"hold_type",
"release_date",
"default_tax_withholding_config", "default_tax_withholding_config",
"column_break2", "column_break2",
"website", "website",
@ -258,7 +258,7 @@
"collapsible": 1, "collapsible": 1,
"fieldname": "section_credit_limit", "fieldname": "section_credit_limit",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Credit Limit" "label": "Payment Terms"
}, },
{ {
"fieldname": "payment_terms", "fieldname": "payment_terms",
@ -432,7 +432,7 @@
"link_fieldname": "party" "link_fieldname": "party"
} }
], ],
"modified": "2021-10-20 22:03:33.147249", "modified": "2022-04-16 18:02:27.838623",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier", "name": "Supplier",
@ -497,6 +497,7 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"title_field": "supplier_name", "title_field": "supplier_name",
"track_changes": 1 "track_changes": 1
} }

View File

@ -213,7 +213,8 @@ def make_all_scorecards(docname):
end_date = get_scorecard_date(sc.period, start_date) end_date = get_scorecard_date(sc.period, start_date)
if scp_count > 0: if scp_count > 0:
frappe.msgprint( frappe.msgprint(
_("Created {0} scorecards for {1} between: ").format(scp_count, sc.supplier) _("Created {0} scorecards for {1} between:").format(scp_count, sc.supplier)
+ " "
+ str(first_start_date) + str(first_start_date)
+ " - " + " - "
+ str(last_end_date) + str(last_end_date)

View File

@ -80,6 +80,6 @@ def _get_variables(criteria):
)[0] )[0]
my_variables.append(var) my_variables.append(var)
except Exception: except Exception:
frappe.throw(_("Unable to find variable: ") + str(match.group(1)), InvalidFormulaVariable) frappe.throw(_("Unable to find variable:") + " " + str(match.group(1)), InvalidFormulaVariable)
return my_variables return my_variables

View File

@ -48,7 +48,7 @@ def get_chart_data(data, conditions, filters):
"data": { "data": {
"labels": labels, "labels": labels,
"datasets": [ "datasets": [
{"name": _("{0}").format(filters.get("period")) + _(" Purchase Value"), "values": datapoints} {"name": _(filters.get("period")) + " " + _("Purchase Value"), "values": datapoints}
], ],
}, },
"type": "line", "type": "line",

View File

@ -35,7 +35,6 @@ frappe.query_reports["Subcontract Order Summary"] = {
return { return {
filters: { filters: {
docstatus: 1, docstatus: 1,
company: frappe.query_report.get_filter_value('company') company: frappe.query_report.get_filter_value('company')
} }
} }

View File

@ -114,11 +114,11 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
onload: (report) => { onload: (report) => {
// Create a button for setting the default supplier // Create a button for setting the default supplier
report.page.add_inner_button(__("Select Default Supplier"), () => { report.page.add_inner_button(__("Select Default Supplier"), () => {
let reporter = frappe.query_reports["Quoted Item Comparison"]; let reporter = frappe.query_reports["Supplier Quotation Comparison"];
//Always make a new one so that the latest values get updated //Always make a new one so that the latest values get updated
reporter.make_default_supplier_dialog(report); reporter.make_default_supplier_dialog(report);
}, 'Tools'); }, __("Tools"));
}, },
make_default_supplier_dialog: (report) => { make_default_supplier_dialog: (report) => {
@ -126,7 +126,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
if(!report.data) return; if(!report.data) return;
let filters = report.get_values(); let filters = report.get_values();
let item_code = filters.item; let item_code = filters.item_code;
// Get a list of the suppliers (with a blank as well) for the user to select // Get a list of the suppliers (with a blank as well) for the user to select
let suppliers = $.map(report.data, (row, idx)=>{ return row.supplier_name }) let suppliers = $.map(report.data, (row, idx)=>{ return row.supplier_name })
@ -152,7 +152,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
] ]
}); });
dialog.set_primary_action("Set Default Supplier", () => { dialog.set_primary_action(__("Set Default Supplier"), () => {
let values = dialog.get_values(); let values = dialog.get_values();
if(values) { if(values) {
// Set the default_supplier field of the appropriate Item to the selected supplier // Set the default_supplier field of the appropriate Item to the selected supplier

View File

@ -1,192 +0,0 @@
from frappe import _
def get_data():
return [
{
"label": _("Student"),
"items": [
{
"type": "doctype",
"name": "Student",
"onboard": 1,
},
{"type": "doctype", "name": "Guardian"},
{"type": "doctype", "name": "Student Log"},
{"type": "doctype", "name": "Student Group"},
],
},
{
"label": _("Admission"),
"items": [
{"type": "doctype", "name": "Student Applicant"},
{"type": "doctype", "name": "Web Academy Applicant"},
{"type": "doctype", "name": "Student Admission"},
{"type": "doctype", "name": "Program Enrollment"},
],
},
{
"label": _("Attendance"),
"items": [
{"type": "doctype", "name": "Student Attendance"},
{"type": "doctype", "name": "Student Leave Application"},
{
"type": "report",
"is_query_report": True,
"name": "Absent Student Report",
"doctype": "Student Attendance",
},
{
"type": "report",
"is_query_report": True,
"name": "Student Batch-Wise Attendance",
"doctype": "Student Attendance",
},
],
},
{
"label": _("Tools"),
"items": [
{"type": "doctype", "name": "Student Attendance Tool"},
{"type": "doctype", "name": "Assessment Result Tool"},
{"type": "doctype", "name": "Student Group Creation Tool"},
{"type": "doctype", "name": "Program Enrollment Tool"},
{"type": "doctype", "name": "Course Scheduling Tool"},
],
},
{
"label": _("Assessment"),
"items": [
{"type": "doctype", "name": "Assessment Plan"},
{
"type": "doctype",
"name": "Assessment Group",
"link": "Tree/Assessment Group",
},
{"type": "doctype", "name": "Assessment Result"},
{"type": "doctype", "name": "Assessment Criteria"},
],
},
{
"label": _("Assessment Reports"),
"items": [
{
"type": "report",
"is_query_report": True,
"name": "Course wise Assessment Report",
"doctype": "Assessment Result",
},
{
"type": "report",
"is_query_report": True,
"name": "Final Assessment Grades",
"doctype": "Assessment Result",
},
{
"type": "report",
"is_query_report": True,
"name": "Assessment Plan Status",
"doctype": "Assessment Plan",
},
{"type": "doctype", "name": "Student Report Generation Tool"},
],
},
{
"label": _("Fees"),
"items": [
{"type": "doctype", "name": "Fees"},
{"type": "doctype", "name": "Fee Schedule"},
{"type": "doctype", "name": "Fee Structure"},
{"type": "doctype", "name": "Fee Category"},
],
},
{
"label": _("Schedule"),
"items": [
{"type": "doctype", "name": "Course Schedule", "route": "/app/List/Course Schedule/Calendar"},
{"type": "doctype", "name": "Course Scheduling Tool"},
],
},
{
"label": _("Masters"),
"items": [
{
"type": "doctype",
"name": "Program",
},
{
"type": "doctype",
"name": "Course",
"onboard": 1,
},
{
"type": "doctype",
"name": "Topic",
},
{
"type": "doctype",
"name": "Instructor",
"onboard": 1,
},
{
"type": "doctype",
"name": "Room",
"onboard": 1,
},
],
},
{
"label": _("Content Masters"),
"items": [
{"type": "doctype", "name": "Article"},
{"type": "doctype", "name": "Video"},
{"type": "doctype", "name": "Quiz"},
],
},
{
"label": _("LMS Activity"),
"items": [
{"type": "doctype", "name": "Course Enrollment"},
{"type": "doctype", "name": "Course Activity"},
{"type": "doctype", "name": "Quiz Activity"},
],
},
{
"label": _("Settings"),
"items": [
{"type": "doctype", "name": "Student Category"},
{"type": "doctype", "name": "Student Batch Name"},
{
"type": "doctype",
"name": "Grading Scale",
"onboard": 1,
},
{"type": "doctype", "name": "Academic Term"},
{"type": "doctype", "name": "Academic Year"},
{"type": "doctype", "name": "Education Settings"},
],
},
{
"label": _("Other Reports"),
"items": [
{
"type": "report",
"is_query_report": True,
"name": "Student and Guardian Contact Details",
"doctype": "Program Enrollment",
},
{
"type": "report",
"is_query_report": True,
"name": "Student Monthly Attendance Sheet",
"doctype": "Student Attendance",
},
{
"type": "report",
"name": "Student Fee Collection",
"doctype": "Fees",
"is_query_report": True,
},
],
},
]

View File

@ -148,6 +148,7 @@ class AccountsController(TransactionBase):
self.validate_inter_company_reference() self.validate_inter_company_reference()
self.disable_pricing_rule_on_internal_transfer()
self.set_incoming_rate() self.set_incoming_rate()
if self.meta.get_field("currency"): if self.meta.get_field("currency"):
@ -180,6 +181,7 @@ class AccountsController(TransactionBase):
else: else:
self.validate_deferred_start_and_end_date() self.validate_deferred_start_and_end_date()
self.validate_deferred_income_expense_account()
self.set_inter_company_account() self.set_inter_company_account()
if self.doctype == "Purchase Invoice": if self.doctype == "Purchase Invoice":
@ -208,6 +210,27 @@ class AccountsController(TransactionBase):
(self.doctype, self.name), (self.doctype, self.name),
) )
def validate_deferred_income_expense_account(self):
field_map = {
"Sales Invoice": "deferred_revenue_account",
"Purchase Invoice": "deferred_expense_account",
}
for item in self.get("items"):
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
if not item.get(field_map.get(self.doctype)):
default_deferred_account = frappe.db.get_value(
"Company", self.company, "default_" + field_map.get(self.doctype)
)
if not default_deferred_account:
frappe.throw(
_(
"Row #{0}: Please update deferred revenue/expense account in item row or default account in company master"
).format(item.idx)
)
else:
item.set(field_map.get(self.doctype), default_deferred_account)
def validate_deferred_start_and_end_date(self): def validate_deferred_start_and_end_date(self):
for d in self.items: for d in self.items:
if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"): if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"):
@ -360,6 +383,14 @@ class AccountsController(TransactionBase):
msg += _("Please create purchase from internal sale or delivery document itself") msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing")) frappe.throw(msg, title=_("Internal Sales Reference Missing"))
def disable_pricing_rule_on_internal_transfer(self):
if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
self.ignore_pricing_rule = 1
frappe.msgprint(
_("Disabled pricing rules since this {} is an internal transfer").format(self.doctype),
alert=1,
)
def validate_due_date(self): def validate_due_date(self):
if self.get("is_pos"): if self.get("is_pos"):
return return
@ -1057,8 +1088,13 @@ class AccountsController(TransactionBase):
return amount, base_amount return amount, base_amount
def make_discount_gl_entries(self, gl_entries): def make_discount_gl_entries(self, gl_entries):
if self.doctype == "Purchase Invoice":
enable_discount_accounting = cint( enable_discount_accounting = cint(
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
)
elif self.doctype == "Sales Invoice":
enable_discount_accounting = cint(
frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
) )
if enable_discount_accounting: if enable_discount_accounting:
@ -1094,11 +1130,10 @@ class AccountsController(TransactionBase):
{ {
"account": item.discount_account, "account": item.discount_account,
"against": supplier_or_customer, "against": supplier_or_customer,
dr_or_cr: flt(discount_amount, item.precision("discount_amount")), dr_or_cr: flt(
dr_or_cr
+ "_in_account_currency": flt(
discount_amount * self.get("conversion_rate"), item.precision("discount_amount") discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
), ),
dr_or_cr + "_in_account_currency": flt(discount_amount, item.precision("discount_amount")),
"cost_center": item.cost_center, "cost_center": item.cost_center,
"project": item.project, "project": item.project,
}, },
@ -1113,11 +1148,11 @@ class AccountsController(TransactionBase):
{ {
"account": income_or_expense_account, "account": income_or_expense_account,
"against": supplier_or_customer, "against": supplier_or_customer,
rev_dr_cr: flt(discount_amount, item.precision("discount_amount")), rev_dr_cr: flt(
rev_dr_cr
+ "_in_account_currency": flt(
discount_amount * self.get("conversion_rate"), item.precision("discount_amount") discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
), ),
rev_dr_cr
+ "_in_account_currency": flt(discount_amount, item.precision("discount_amount")),
"cost_center": item.cost_center, "cost_center": item.cost_center,
"project": item.project or self.project, "project": item.project or self.project,
}, },
@ -1711,6 +1746,8 @@ class AccountsController(TransactionBase):
internal_party_field = "is_internal_customer" internal_party_field = "is_internal_customer"
elif self.doctype in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"): elif self.doctype in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"):
internal_party_field = "is_internal_supplier" internal_party_field = "is_internal_supplier"
else:
return False
if self.get(internal_party_field) and (self.represents_company == self.company): if self.get(internal_party_field) and (self.represents_company == self.company):
return True return True
@ -1975,12 +2012,13 @@ def get_advance_journal_entries(
reference_condition = " and (" + " or ".join(conditions) + ")" if conditions else "" reference_condition = " and (" + " or ".join(conditions) + ")" if conditions else ""
# nosemgrep
journal_entries = frappe.db.sql( journal_entries = frappe.db.sql(
""" """
select select
"Journal Entry" as reference_type, t1.name as reference_name, "Journal Entry" as reference_type, t1.name as reference_name,
t1.remark as remarks, t2.{0} as amount, t2.name as reference_row, t1.remark as remarks, t2.{0} as amount, t2.name as reference_row,
t2.reference_name as against_order t2.reference_name as against_order, t2.exchange_rate
from from
`tabJournal Entry` t1, `tabJournal Entry Account` t2 `tabJournal Entry` t1, `tabJournal Entry Account` t2
where where
@ -2423,11 +2461,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row
) )
def validate_quantity(child_item, d): def validate_quantity(child_item, new_data):
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty): if not flt(new_data.get("qty")):
frappe.throw(
_("Row # {0}: Quantity for Item {1} cannot be zero").format(
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
),
title=_("Invalid Qty"),
)
if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty):
frappe.throw(_("Cannot set quantity less than delivered quantity")) frappe.throw(_("Cannot set quantity less than delivered quantity"))
if parent_doctype == "Purchase Order" and flt(d.get("qty")) < flt(child_item.received_qty): if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(
child_item.received_qty
):
frappe.throw(_("Cannot set quantity less than received quantity")) frappe.throw(_("Cannot set quantity less than received quantity"))
data = json.loads(trans_items) data = json.loads(trans_items)

View File

@ -252,9 +252,7 @@ class BuyingController(StockController):
qty_in_stock_uom = flt(item.qty * item.conversion_factor) qty_in_stock_uom = flt(item.qty * item.conversion_factor)
item.valuation_rate = ( item.valuation_rate = (
item.base_net_amount item.base_net_amount + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
+ item.item_tax_amount
+ flt(item.landed_cost_voucher_amount)
) / qty_in_stock_uom ) / qty_in_stock_uom
else: else:
item.valuation_rate = 0.0 item.valuation_rate = 0.0
@ -300,14 +298,15 @@ class BuyingController(StockController):
if self.is_internal_transfer(): if self.is_internal_transfer():
if rate != d.rate: if rate != d.rate:
d.rate = rate d.rate = rate
d.discount_percentage = 0
d.discount_amount = 0
frappe.msgprint( frappe.msgprint(
_( _(
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
).format(d.idx), ).format(d.idx),
alert=1, alert=1,
) )
d.discount_percentage = 0.0
d.discount_amount = 0.0
d.margin_rate_or_amount = 0.0
def validate_for_subcontracting(self): def validate_for_subcontracting(self):
if self.is_subcontracted: if self.is_subcontracted:
@ -398,7 +397,10 @@ class BuyingController(StockController):
stock_items = self.get_stock_items() stock_items = self.get_stock_items()
for d in self.get("items"): for d in self.get("items"):
if d.item_code in stock_items and d.warehouse: if d.item_code not in stock_items:
continue
if d.warehouse:
pr_qty = flt(d.qty) * flt(d.conversion_factor) pr_qty = flt(d.qty) * flt(d.conversion_factor)
if pr_qty: if pr_qty:
@ -423,6 +425,7 @@ class BuyingController(StockController):
sle = self.get_sl_entries( sle = self.get_sl_entries(
d, {"actual_qty": flt(pr_qty), "serial_no": cstr(d.serial_no).strip()} d, {"actual_qty": flt(pr_qty), "serial_no": cstr(d.serial_no).strip()}
) )
if self.is_return: if self.is_return:
outgoing_rate = get_rate_for_return( outgoing_rate = get_rate_for_return(
self.doctype, self.name, d.item_code, self.return_against, item_row=d self.doctype, self.name, d.item_code, self.return_against, item_row=d

View File

@ -162,6 +162,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
{account_type_condition} {account_type_condition}
AND is_group = 0 AND is_group = 0
AND company = %(company)s AND company = %(company)s
AND disabled = %(disabled)s
AND (account_currency = %(currency)s or ifnull(account_currency, '') = '') AND (account_currency = %(currency)s or ifnull(account_currency, '') = '')
AND `{searchfield}` LIKE %(txt)s AND `{searchfield}` LIKE %(txt)s
{mcond} {mcond}
@ -175,6 +176,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
dict( dict(
account_types=filters.get("account_type"), account_types=filters.get("account_type"),
company=filters.get("company"), company=filters.get("company"),
disabled=filters.get("disabled", 0),
currency=company_currency, currency=company_currency,
txt="%{}%".format(txt), txt="%{}%".format(txt),
offset=start, offset=start,

View File

@ -334,7 +334,6 @@ def make_return_doc(doctype, source_name, target_doc=None):
doc = frappe.get_doc(target) doc = frappe.get_doc(target)
doc.is_return = 1 doc.is_return = 1
doc.return_against = source.name doc.return_against = source.name
doc.ignore_pricing_rule = 1
doc.set_warehouse = "" doc.set_warehouse = ""
if doctype == "Sales Invoice" or doctype == "POS Invoice": if doctype == "Sales Invoice" or doctype == "POS Invoice":
doc.is_pos = source.is_pos doc.is_pos = source.is_pos

View File

@ -16,6 +16,9 @@ from erpnext.stock.utils import get_incoming_rate
class SellingController(StockController): class SellingController(StockController):
def __setup__(self):
self.flags.ignore_permlevel_for_fields = ["selling_price_list", "price_list_currency"]
def get_feed(self): def get_feed(self):
return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total) return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total)
@ -444,9 +447,6 @@ class SellingController(StockController):
rate = flt(d.incoming_rate * d.conversion_factor, d.precision("rate")) rate = flt(d.incoming_rate * d.conversion_factor, d.precision("rate"))
if d.rate != rate: if d.rate != rate:
d.rate = rate d.rate = rate
d.discount_percentage = 0
d.discount_amount = 0
frappe.msgprint( frappe.msgprint(
_( _(
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
@ -454,6 +454,10 @@ class SellingController(StockController):
alert=1, alert=1,
) )
d.discount_percentage = 0.0
d.discount_amount = 0.0
d.margin_rate_or_amount = 0.0
elif self.get("return_against"): elif self.get("return_against"):
# Get incoming rate of return entry from reference document # Get incoming rate of return entry from reference document
# based on original item cost as per valuation method # based on original item cost as per valuation method

View File

@ -1,469 +0,0 @@
import copy
from collections import defaultdict
import frappe
from frappe import _
from frappe.utils import cint, flt, get_link_to_form
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 not self.is_subcontracted:
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):
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_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"))

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