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
},
"parserOptions": {
"ecmaVersion": 9,
"ecmaVersion": 11,
"sourceType": "module"
},
"extends": "eslint:recommended",

View File

@ -26,3 +26,6 @@ b147b85e6ac19a9220cd1e2958a6ebd99373283a
# bulk format python code with black
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
- 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
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.**

View File

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

2
.github/stale.yml vendored
View File

@ -25,7 +25,7 @@ pulls:
ready. Thank you for contributing.
issues:
daysUntilStale: 60
daysUntilStale: 90
daysUntilClose: 7
exemptLabels:
- 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)">
<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"/>

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 }}
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
uses: codecov/codecov-action@v2
with:
name: MariaDB
fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml
verbose: true

View File

@ -88,3 +88,37 @@ pull_request_rules:
- version-12-pre-release
assignees:
- "{{ 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">
<img src="https://raw.githubusercontent.com/frappe/erpnext/develop/erpnext/public/images/erpnext-logo.png" height="128">
<a href="https://erpnext.com">
<img src="https://raw.githubusercontent.com/frappe/erpnext/develop/erpnext/public/images/erpnext-logo.png" height="128">
</a>
<h2>ERPNext</h2>
<p align="center">
<p>ERP made simple</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)
[![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)
[![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)
@ -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. [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.
- [User Guide](https://erpnext.com/docs/user)
- [Discussion Forum](https://discuss.erpnext.com/)
## Installation
---
<div align="center">
<a href="https://frappecloud.com/deploy?apps=frappe,erpnext&source=erpnext_readme">
<div align="center" style="max-height: 40px;">
<a href="https://frappecloud.com/erpnext/signup">
<img src=".github/try-on-f-cloud-button.svg" height="40">
</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>
> Login for the PWD site: (username: Administrator, password: admin)
### 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.
### 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.
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
@ -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. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
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.
### 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)
Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md).

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:
layout: "diff, files"
require_changes: true
after_n_builds: 3
ignore:
- "erpnext/demo"

View File

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

View File

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

View File

@ -18,7 +18,6 @@
"automatically_fetch_payment_terms",
"column_break_17",
"enable_common_party_accounting",
"enable_discount_accounting",
"report_setting_section",
"use_custom_cash_flow",
"deferred_accounting_settings_section",
@ -272,13 +271,6 @@
"fieldtype": "Check",
"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",
"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,
"issingle": 1,
"links": [],
"modified": "2022-02-04 12:32:36.805652",
"modified": "2022-04-08 14:45:06.796418",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@ -28,7 +28,6 @@ class AccountsSettings(Document):
self.validate_stale_days()
self.enable_payment_schedule_in_print()
self.toggle_discount_accounting_fields()
self.validate_pending_reposts()
def validate_stale_days(self):
@ -52,74 +51,6 @@ class AccountsSettings(Document):
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):
if self.acc_frozen_upto:
check_pending_reposting(self.acc_frozen_upto)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,8 @@ from frappe.utils import nowdate
class CurrencyExchangeSettings(Document):
def validate(self):
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()
self.validate_result(response, value)
@ -35,9 +37,6 @@ class CurrencyExchangeSettings(Document):
self.append("req_params", {"key": "symbols", "value": "{to_currency}"})
def validate_parameters(self):
if frappe.flags.in_test:
return None, None
params = {}
for row in self.req_params:
params[row.key] = row.value.format(
@ -59,18 +58,14 @@ class CurrencyExchangeSettings(Document):
return response, value
def validate_result(self, response, value):
if frappe.flags.in_test:
return
try:
for key in self.result_key:
value = value[
str(key.key).format(transaction_date=nowdate(), to_currency="INR", from_currency="USD")
]
except Exception:
frappe.throw("Invalid result key. Response: " + response.text)
frappe.throw(_("Invalid result key. Response:") + " " + response.text)
if not isinstance(value, (int, float)):
frappe.throw(_("Returned exchange rate is neither integer not float."))
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', {
refresh: function(frm) {
frm.page.btn_secondary.hide()
}
});

View File

@ -269,6 +269,11 @@ class GLEntry(Document):
if not self.fiscal_year:
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):
if not adv_adj and account:

View File

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

View File

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

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.utils import (
check_if_stock_and_account_balance_synced,
get_account_currency,
get_balance_on,
get_stock_accounts,
@ -88,9 +87,6 @@ class JournalEntry(AccountsController):
self.update_inter_company_jv()
self.update_invoice_discounting()
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):
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries

View File

@ -62,7 +62,7 @@ def start_merge(docname):
)
except Exception:
frappe.db.rollback()
frappe.log_error(title=ledger_merge.name)
ledger_merge.log_error("Ledger merge failed")
finally:
if successful_merges == total:
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))
if pos_profiles:
message = (
"POS Profile "
+ frappe.bold(", ".join(pos_profiles))
+ " contains \
Mode of Payment "
+ frappe.bold(str(self.name))
+ ". Please remove them to disable this mode."
)
frappe.throw(_(message), title="Not Allowed")
message = _(
"POS Profile {} contains Mode of Payment {}. Please remove them to disable this mode."
).format(frappe.bold(", ".join(pos_profiles)), frappe.bold(str(self.name)))
frappe.throw(message, title=_("Not Allowed"))

View File

@ -2,9 +2,6 @@
# For license information, please see license.txt
import traceback
from json import dumps
import frappe
from frappe import _, scrub
from frappe.model.document import Document
@ -114,10 +111,13 @@ class OpeningInvoiceCreationTool(Document):
)
or {}
)
default_currency = frappe.db.get_value(row.party_type, row.party, "default_currency")
if company_details:
invoice.update(
{
"currency": company_details.get("default_currency"),
"currency": default_currency or company_details.get("default_currency"),
"letter_head": company_details.get("default_letter_head"),
}
)
@ -244,11 +244,7 @@ def start_import(invoices):
except Exception:
errors += 1
frappe.db.rollback()
message = "\n".join(
["Data:", dumps(d, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()]
)
frappe.log_error(title="Error while creating Opening Invoice", message=message)
frappe.db.commit()
doc.log_error("Opening invoice creation failed")
if errors:
frappe.msgprint(
_("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",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"account"
@ -11,6 +12,7 @@
{
"fieldname": "company",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Company",
"options": "Company",
@ -27,7 +29,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-04-07 18:13:08.833822",
"modified": "2022-04-04 12:31:02.994197",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Party Account",
@ -35,5 +37,6 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@ -112,8 +112,6 @@ frappe.ui.form.on('Payment Entry', {
var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
} else if (frm.doc.party_type == "Employee") {
var doctypes = ["Expense Claim", "Journal Entry"];
} else if (frm.doc.party_type == "Student") {
var doctypes = ["Fees"];
} else {
var doctypes = ["Journal Entry"];
}
@ -224,10 +222,7 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.total_allocated_amount > party_amount)));
frm.toggle_display("set_exchange_gain_loss",
(frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount &&
((frm.doc.paid_from_account_currency != company_currency ||
frm.doc.paid_to_account_currency != company_currency) &&
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency)));
frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount);
frm.refresh_fields();
},
@ -758,8 +753,7 @@ frappe.ui.form.on('Payment Entry', {
if(
(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=="Employee") ||
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student")
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee")
) {
if(total_positive_outstanding > total_negative_outstanding)
if (!frm.doc.paid_amount)
@ -801,8 +795,7 @@ frappe.ui.form.on('Payment Entry', {
if (
(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=="Employee") ||
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student")
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee")
) {
if(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:
frappe.throw(_("Party is mandatory"))
_party_name = (
"title" if self.party_type in ("Student", "Shareholder") else self.party_type.lower() + "_name"
)
_party_name = "title" if self.party_type == "Shareholder" else self.party_type.lower() + "_name"
self.party_name = frappe.db.get_value(self.party_type, self.party, _party_name)
if self.party:
@ -298,9 +296,7 @@ class PaymentEntry(AccountsController):
frappe.throw(_("{0} is mandatory").format(self.meta.get_label(field)))
def validate_reference_documents(self):
if self.party_type == "Student":
valid_reference_doctypes = "Fees"
elif self.party_type == "Customer":
if self.party_type == "Customer":
valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
elif self.party_type == "Supplier":
valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry")
@ -338,8 +334,6 @@ class PaymentEntry(AccountsController):
ref_party_account = (
get_party_account_based_on_invoice_discounting(d.reference_name) or ref_doc.debit_to
)
elif self.party_type == "Student":
ref_party_account = ref_doc.receivable_account
elif self.party_type == "Supplier":
ref_party_account = ref_doc.credit_to
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:
frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name))
@ -1259,20 +1259,19 @@ def get_outstanding_reference_documents(args):
# Get all SO / PO which are not fully billed or against which full advance not paid
orders_to_be_billed = []
if args.get("party_type") != "Student":
orders_to_be_billed = get_orders_to_be_billed(
args.get("posting_date"),
args.get("party_type"),
args.get("party"),
args.get("company"),
party_account_currency,
company_currency,
filters=args,
)
orders_to_be_billed = get_orders_to_be_billed(
args.get("posting_date"),
args.get("party_type"),
args.get("party"),
args.get("company"),
party_account_currency,
company_currency,
filters=args,
)
# Get negative outstanding sales /purchase invoices
negative_outstanding_invoices = []
if args.get("party_type") not in ["Student", "Employee"] and not args.get("voucher_no"):
if args.get("party_type") != "Employee" and not args.get("voucher_no"):
negative_outstanding_invoices = get_negative_outstanding_invoices(
args.get("party_type"),
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_balance = get_balance_on(party_account, date, cost_center=cost_center)
_party_name = (
"title" if party_type in ("Student", "Shareholder") else party_type.lower() + "_name"
)
_party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name"
party_name = frappe.db.get_value(party_type, party, _party_name)
party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center)
if party_type in ["Customer", "Supplier"]:
@ -1560,7 +1557,7 @@ def get_company_defaults(company):
def get_outstanding_on_journal_entry(name):
res = frappe.db.sql(
"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) "
"ELSE ifnull(sum(credit_in_account_currency - debit_in_account_currency), 0) "
"END as outstanding_amount "
@ -1917,8 +1914,6 @@ def set_party_type(dt):
party_type = "Supplier"
elif dt in ("Expense Claim", "Employee Advance", "Gratuity"):
party_type = "Employee"
elif dt == "Fees":
party_type = "Student"
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)
)
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):
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() {

View File

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

View File

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

View File

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

View File

@ -64,13 +64,15 @@ frappe.ui.form.on('POS Closing Entry', {
pos_opening_entry(frm) {
if (frm.doc.pos_opening_entry && frm.doc.period_start_date && frm.doc.period_end_date && frm.doc.user) {
reset_values(frm);
frm.trigger("set_opening_amounts");
frm.trigger("get_pos_invoices");
frappe.run_serially([
() => frm.trigger("set_opening_amounts"),
() => frm.trigger("get_pos_invoices")
]);
}
},
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 }) => {
balance_details.forEach(detail => {
frm.add_child("payment_reconciliation", {
@ -83,7 +85,7 @@ frappe.ui.form.on('POS Closing Entry', {
},
get_pos_invoices(frm) {
frappe.call({
return frappe.call({
method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices',
args: {
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)):
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)):
frappe.throw(
_("Duplicate customer group found in the cutomer group table"),
title="Duplicate Customer Group",
title=_("Duplicate Customer Group"),
)
def validate_payment_methods(self):

View File

@ -35,10 +35,11 @@ class PricingRule(Document):
self.margin_rate_or_amount = 0.0
def validate_duplicate_apply_on(self):
field = apply_on_dict.get(self.apply_on)
values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field]
if len(values) != len(set(values)):
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
if self.apply_on != "Transaction":
field = apply_on_dict.get(self.apply_on)
values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field]
if len(values) != len(set(values)):
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
def validate_mandatory(self):
for apply_on, field in apply_on_dict.items():

View File

@ -11,7 +11,7 @@ from erpnext.accounts.deferred_revenue import (
convert_deferred_expense_to_expense,
convert_deferred_revenue_to_income,
)
from erpnext.accounts.general_ledger import make_reverse_gl_entries
from erpnext.accounts.general_ledger import make_gl_entries
class ProcessDeferredAccounting(Document):
@ -34,4 +34,4 @@ class ProcessDeferredAccounting(Document):
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){
if(!frm.doc.__islocal) {
frm.add_custom_button('Send Emails',function(){
frm.add_custom_button(__('Send Emails'), function(){
frappe.call({
method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_emails",
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(
'/api/method/erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.download_statements?'
+ 'document_name='+encodeURIComponent(frm.doc.name))

View File

@ -34,8 +34,9 @@ class ProcessStatementOfAccounts(Document):
frappe.throw(_("Customers not selected."))
if self.enable_auto_email:
self.to_date = self.start_date
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
if self.start_date and getdate(self.start_date) >= getdate(today()):
self.to_date = self.start_date
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
def get_report_pdf(doc, consolidated=True):

View File

@ -30,6 +30,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
onload() {
super.onload();
// Ignore linked advances
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry'];
if(!this.frm.doc.__islocal) {
// show credit_to in print format
if(!this.frm.doc.supplier && this.frm.doc.credit_to) {
@ -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) {
hide_fields(frm.doc);
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)
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(
frappe.db.get_value(
@ -810,7 +810,9 @@ class PurchaseInvoice(BuyingController):
if provisional_accounting_for_non_stock_items:
if item.purchase_receipt:
provisional_account = self.get_company_default("default_provisional_account")
provisional_account = frappe.db.get_value(
"Purchase Receipt Item", item.pr_detail, "provisional_expense_account"
) or self.get_company_default("default_provisional_account")
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
if not purchase_receipt_doc:
@ -833,7 +835,7 @@ class PurchaseInvoice(BuyingController):
if expense_booked_in_pr:
# Intentionally passing purchase invoice item to handle partial billing
purchase_receipt_doc.add_provisional_gl_entry(
item, gl_entries, self.posting_date, reverse=1
item, gl_entries, self.posting_date, provisional_account, reverse=1
)
if not self.is_internal_transfer():
@ -1156,7 +1158,7 @@ class PurchaseInvoice(BuyingController):
# tax table gl entries
valuation_tax = {}
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"):
@ -1249,7 +1251,7 @@ class PurchaseInvoice(BuyingController):
def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"):
self._enable_discount_accounting = cint(
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting")
frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
)
return self._enable_discount_accounting
@ -1366,7 +1368,9 @@ class PurchaseInvoice(BuyingController):
if (
not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment
):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company)
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name
)
gl_entries.append(
self.get_gl_dict(

View File

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

View File

@ -33,7 +33,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
var me = this;
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) {
// show debit_to in print format
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) {
if (frm.doc.project) {
frm.events.add_timesheet_data(frm, {
@ -974,25 +953,6 @@ frappe.ui.form.on('Sales Invoice', {
if (frm.doc.is_debit_note) {
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) {

View File

@ -1051,7 +1051,7 @@ class SalesInvoice(SellingController):
def make_tax_gl_entries(self, gl_entries):
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"):
@ -1097,7 +1097,7 @@ class SalesInvoice(SellingController):
def make_item_gl_entries(self, gl_entries):
# income account gl entries
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"):
@ -1276,7 +1276,7 @@ class SalesInvoice(SellingController):
def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"):
self._enable_discount_accounting = cint(
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting")
frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
)
return self._enable_discount_accounting
@ -1412,7 +1412,7 @@ class SalesInvoice(SellingController):
)
)
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):
# write off entries, applicable if only pos
@ -1466,7 +1466,9 @@ class SalesInvoice(SellingController):
and self.base_rounding_adjustment
and not self.is_internal_transfer()
):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company)
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Sales Invoice", self.name
)
gl_entries.append(
self.get_gl_dict(

View File

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

View File

@ -7,6 +7,7 @@ import unittest
import frappe
from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.model.naming import make_autoname
from frappe.tests.utils import change_settings
from frappe.utils import add_days, flt, getdate, nowdate
import erpnext
@ -1977,6 +1978,13 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_rounding_adjustment_3(self):
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
disable_dimension,
)
create_dimension()
si = create_sales_invoice(do_not_save=True)
si.items = []
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
@ -2004,6 +2012,10 @@ class TestSalesInvoice(unittest.TestCase):
"included_in_print_rate": 1,
},
)
si.cost_center = "_Test Cost Center 2 - _TC"
si.location = "Block 1"
si.save()
si.submit()
self.assertEqual(si.net_total, 4007.16)
@ -2039,6 +2051,18 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(debit_credit_diff, 0)
round_off_gle = frappe.db.get_value(
"GL Entry",
{"voucher_type": "Sales Invoice", "voucher_no": si.name, "account": "Round Off - _TC"},
["cost_center", "location"],
as_dict=1,
)
self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC")
self.assertEqual(round_off_gle.location, "Block 1")
disable_dimension()
def test_sales_invoice_with_shipping_rule(self):
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
@ -2240,6 +2264,14 @@ class TestSalesInvoice(unittest.TestCase):
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):
deferred_account = create_account(
account_name="Deferred Revenue",
@ -2616,6 +2648,7 @@ class TestSalesInvoice(unittest.TestCase):
# reset
einvoice_settings = frappe.get_doc("E Invoice Settings")
einvoice_settings.enable = 0
einvoice_settings.save()
frappe.flags.country = country
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"
)
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
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(
account_name="Discount Account",
@ -2697,14 +2726,10 @@ class TestSalesInvoice(unittest.TestCase):
]
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):
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
enable_discount_accounting,
)
enable_discount_accounting()
additional_discount_account = create_account(
account_name="Discount Account",
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))
enable_discount_accounting(enable=0)
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.book_deferred_entries_via_journal_entry = 0
acc_settings.submit_journal_entriessubmit_journal_entries = 0
acc_settings.submit_journal_entries = 0
acc_settings.save()
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None)
@ -3116,6 +3140,95 @@ class TestSalesInvoice(unittest.TestCase):
si.reload()
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():
si = make_sales_invoice_for_ewaybill()

View File

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

View File

@ -163,12 +163,15 @@ def get_party_details(party, party_type, args=None):
def get_tax_template(posting_date, args):
"""Get matching tax rule"""
args = frappe._dict(args)
conditions = [
"""(from_date is null or from_date <= '{0}')
and (to_date is null or to_date >= '{0}')""".format(
posting_date
conditions = []
if 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(
"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):
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(
_("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format(
d.idx

View File

@ -31,6 +31,7 @@ def make_gl_entries(
if gl_map:
if not cancel:
validate_accounting_period(gl_map)
validate_disabled_accounts(gl_map)
gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1:
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)
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):
accounting_periods = frappe.db.sql(
""" 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):
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_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:
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(
"Company", company, ["round_off_account", "round_off_cost_center"]
) or [None, None]
meta = frappe.get_meta(voucher_type)
# Give first preference to parent cost center for round off GLE
if meta.has_field("cost_center"):
parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center")
if parent_cost_center:
round_off_cost_center = parent_cost_center
if not round_off_account:
frappe.throw(_("Please mention Round Off Account in Company"))

View File

@ -831,9 +831,9 @@ def get_party_shipping_address(doctype, name):
"where "
"dl.link_doctype=%s "
"and dl.link_name=%s "
'and dl.parenttype="Address" '
"and dl.parenttype='Address' "
"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",
(doctype, name),
)
@ -881,11 +881,11 @@ def get_default_contact(doctype, name):
"""
SELECT dl.parent, c.is_primary_contact, c.is_billing_contact
FROM `tabDynamic Link` dl
INNER JOIN tabContact c ON c.name = dl.parent
INNER JOIN `tabContact` c ON c.name = dl.parent
WHERE
dl.link_doctype=%s AND
dl.link_name=%s AND
dl.parenttype = "Contact"
dl.parenttype = 'Contact'
ORDER BY is_primary_contact DESC, is_billing_contact DESC
""",
(doctype, name),

View File

@ -1,7 +1,8 @@
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
<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 %}>
{% if letter_head and not no_letterhead %}
<div class="letter-head">{{ letter_head }}</div>
@ -170,4 +171,10 @@
</tbody>
</table>
</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>

View File

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

View File

@ -203,7 +203,7 @@ def get_loan_entries(filters):
posting_date = (loan_doc.posting_date).as_("posting_date")
account = loan_doc.payment_account
entries = (
query = (
frappe.qb.from_(loan_doc)
.select(
ConstantColumn(doctype).as_("payment_document"),
@ -217,9 +217,12 @@ def get_loan_entries(filters):
.where(account == filters.get("account"))
.where(posting_date <= 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)
return loan_docs

View File

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

View File

@ -230,7 +230,7 @@ def get_columns(dimension_list):
columns.append(
{
"fieldname": "total",
"label": "Total",
"label": _("Total"),
"fieldtype": "Currency",
"options": "currency",
"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))
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 ""

View File

@ -133,7 +133,7 @@ def set_account_currency(filters):
else:
account_currency = (
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")
)
@ -275,7 +275,7 @@ def get_conditions(filters):
)
conditions.append("{0} in %({0})s".format(dimension.fieldname))
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 ""
@ -435,7 +435,13 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
gle_map[group_by_value].entries.append(gle)
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"):
for dim in accounting_dimensions:
keylist.append(gle.get(dim))

View File

@ -29,7 +29,7 @@ def get_columns():
"options": "Item Group",
"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": "customer",

View File

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

View File

@ -62,7 +62,7 @@ def get_pos_entries(filters, group_by_field):
"""
SELECT
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}
FROM
`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",
{"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80},
]
expense_accounts = (
tax_accounts
) = (
expense_columns
) = tax_columns = unrealized_profit_loss_accounts = unrealized_profit_loss_account_columns = []
expense_accounts = []
tax_accounts = []
unrealized_profit_loss_accounts = []
if invoice_list:
expense_accounts = frappe.db.sql_list(
@ -163,10 +162,11 @@ def get_columns(invoice_list, additional_table_columns):
unrealized_profit_loss_account_columns = [
(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts
]
for account in tax_accounts:
if account not in expense_accounts:
tax_columns.append(account + ":Currency/currency:120")
tax_columns = [
(account + ":Currency/currency:120")
for account in tax_accounts
if account not in expense_accounts
]
columns = (
columns
@ -237,7 +237,7 @@ def get_conditions(filters):
else:
conditions += (
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

View File

@ -405,7 +405,7 @@ def get_conditions(filters):
else:
conditions += (
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

View File

@ -188,9 +188,9 @@ def get_rootwise_opening_balances(filters, report_type):
filters[dimension.fieldname] = get_dimension_with_children(
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:
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)})

View File

@ -23,8 +23,6 @@ def execute(filters=None):
def get_data(filters, show_party_name):
if filters.get("party_type") in ("Customer", "Supplier", "Employee", "Member"):
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":
party_name_field = "title"
else:

View File

@ -1,10 +1,17 @@
import unittest
import frappe
from frappe.test_runner import make_test_objects
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.stock_entry.stock_entry_utils import make_stock_entry
class TestUtils(unittest.TestCase):
@ -47,6 +54,25 @@ class TestUtils(unittest.TestCase):
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 = [
{

View File

@ -3,6 +3,7 @@
from json import loads
from typing import List, Tuple
import frappe
import frappe.defaults
@ -18,10 +19,6 @@ from erpnext.stock import get_warehouse_account_map
from erpnext.stock.utils import get_stock_value_on
class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError):
pass
class FiscalYearError(frappe.ValidationError):
pass
@ -1126,6 +1123,9 @@ def update_gl_entries_after(
def repost_gle_for_stock_vouchers(
stock_vouchers, posting_date, company=None, warehouse_account=None
):
if not stock_vouchers:
return
def _delete_gl_entries(voucher_type, voucher_no):
frappe.db.sql(
"""delete from `tabGL Entry`
@ -1133,6 +1133,8 @@ def repost_gle_for_stock_vouchers(
(voucher_type, voucher_no),
)
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
if not warehouse_account:
warehouse_account = get_warehouse_account_map(company)
@ -1153,6 +1155,27 @@ def repost_gle_for_stock_vouchers(
_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(
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
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):
stock_accounts = [
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))
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 += "<br><br>"
msg += _("You can also set default CWIP account in Company {}").format(

View File

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

View File

@ -32,7 +32,7 @@ frappe.ui.form.on('Asset Repair', {
refresh: function(frm) {
if (frm.doc.docstatus) {
frm.add_custom_button("View General Ledger", function() {
frm.add_custom_button(__("View General Ledger"), function() {
frappe.route_options = {
"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(
formatdate(asset_purchase_date)
),
title="Incorrect Date",
title=_("Incorrect Date"),
)
def set_difference_amount(self):

View File

@ -20,6 +20,7 @@
"maintain_same_rate",
"allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
"enable_discount_accounting",
"subcontract",
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
@ -133,6 +134,13 @@
{
"fieldname": "column_break_12",
"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",
@ -140,7 +148,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-01-27 17:57:58.367048",
"modified": "2022-04-14 15:56:42.340223",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@ -5,10 +5,15 @@
import frappe
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.model.document import Document
from frappe.utils import cint
class BuyingSettings(Document):
def on_update(self):
self.toggle_discount_accounting_fields()
def validate(self):
for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]:
frappe.db.set_default(key, self.get(key, ""))
@ -21,3 +26,60 @@ class BuyingSettings(Document):
self.get("supp_master_name") == "Naming Series",
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

@ -452,4 +452,4 @@ function set_schedule_date(frm) {
}
}
frappe.provide("erpnext.buying");
frappe.provide("erpnext.buying");

View File

@ -25,6 +25,10 @@
"order_confirmation_no",
"order_confirmation_date",
"amended_from",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"drop_ship",
"customer",
"customer_name",
@ -1135,6 +1139,28 @@
{
"fieldname": "section_break_45",
"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",

View File

@ -891,4 +891,4 @@ def get_requested_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC")
test_dependencies = ["BOM", "Item Price"]
test_records = frappe.get_test_records("Purchase Order")
test_records = frappe.get_test_records("Purchase Order")

View File

@ -31,7 +31,7 @@ frappe.ui.form.on("Request for Quotation",{
if (frm.doc.docstatus === 1) {
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() {
@ -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 dialog = new frappe.ui.Dialog({
title: __("Create Supplier Quotation"),
fields: [
{ "fieldtype": "Select", "label": __("Supplier"),
{ "fieldtype": "Link",
"label": __("Supplier"),
"fieldname": "supplier",
"options": doc.suppliers.map(d => d.supplier),
"options": 'Supplier',
"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: (args) => {

View File

@ -32,7 +32,9 @@
"terms",
"printing_settings",
"select_print_heading",
"letter_head"
"letter_head",
"more_info",
"opportunity"
],
"fields": [
{
@ -193,6 +195,23 @@
"options": "Letter Head",
"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",
"fieldtype": "Select",
@ -258,7 +277,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-11-24 17:47:49.909000",
"modified": "2022-04-06 17:47:49.909000",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
@ -327,4 +346,4 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC"
}
}

View File

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

View File

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

View File

@ -213,7 +213,8 @@ def make_all_scorecards(docname):
end_date = get_scorecard_date(sc.period, start_date)
if scp_count > 0:
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(last_end_date)

View File

@ -80,6 +80,6 @@ def _get_variables(criteria):
)[0]
my_variables.append(var)
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

View File

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

View File

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

View File

@ -114,11 +114,11 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
onload: (report) => {
// Create a button for setting the 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
reporter.make_default_supplier_dialog(report);
}, 'Tools');
}, __("Tools"));
},
make_default_supplier_dialog: (report) => {
@ -126,7 +126,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
if(!report.data) return;
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
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();
if(values) {
// 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.disable_pricing_rule_on_internal_transfer()
self.set_incoming_rate()
if self.meta.get_field("currency"):
@ -180,6 +181,7 @@ class AccountsController(TransactionBase):
else:
self.validate_deferred_start_and_end_date()
self.validate_deferred_income_expense_account()
self.set_inter_company_account()
if self.doctype == "Purchase Invoice":
@ -208,6 +210,27 @@ class AccountsController(TransactionBase):
(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):
for d in self.items:
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")
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):
if self.get("is_pos"):
return
@ -1057,9 +1088,14 @@ class AccountsController(TransactionBase):
return amount, base_amount
def make_discount_gl_entries(self, gl_entries):
enable_discount_accounting = cint(
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting")
)
if self.doctype == "Purchase Invoice":
enable_discount_accounting = cint(
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 self.doctype == "Purchase Invoice":
@ -1094,11 +1130,10 @@ class AccountsController(TransactionBase):
{
"account": item.discount_account,
"against": supplier_or_customer,
dr_or_cr: flt(discount_amount, item.precision("discount_amount")),
dr_or_cr
+ "_in_account_currency": flt(
dr_or_cr: flt(
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,
"project": item.project,
},
@ -1113,11 +1148,11 @@ class AccountsController(TransactionBase):
{
"account": income_or_expense_account,
"against": supplier_or_customer,
rev_dr_cr: flt(discount_amount, item.precision("discount_amount")),
rev_dr_cr
+ "_in_account_currency": flt(
rev_dr_cr: flt(
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,
"project": item.project or self.project,
},
@ -1711,6 +1746,8 @@ class AccountsController(TransactionBase):
internal_party_field = "is_internal_customer"
elif self.doctype in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"):
internal_party_field = "is_internal_supplier"
else:
return False
if self.get(internal_party_field) and (self.represents_company == self.company):
return True
@ -1975,12 +2012,13 @@ def get_advance_journal_entries(
reference_condition = " and (" + " or ".join(conditions) + ")" if conditions else ""
# nosemgrep
journal_entries = frappe.db.sql(
"""
select
"Journal Entry" as reference_type, t1.name as reference_name,
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
`tabJournal Entry` t1, `tabJournal Entry Account` t2
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
)
def validate_quantity(child_item, d):
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
def validate_quantity(child_item, new_data):
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"))
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"))
data = json.loads(trans_items)

View File

@ -252,9 +252,7 @@ class BuyingController(StockController):
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
item.valuation_rate = (
item.base_net_amount
+ item.item_tax_amount
+ flt(item.landed_cost_voucher_amount)
item.base_net_amount + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
) / qty_in_stock_uom
else:
item.valuation_rate = 0.0
@ -300,14 +298,15 @@ class BuyingController(StockController):
if self.is_internal_transfer():
if rate != d.rate:
d.rate = rate
d.discount_percentage = 0
d.discount_amount = 0
frappe.msgprint(
_(
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
).format(d.idx),
alert=1,
)
d.discount_percentage = 0.0
d.discount_amount = 0.0
d.margin_rate_or_amount = 0.0
def validate_for_subcontracting(self):
if self.is_subcontracted:
@ -398,7 +397,10 @@ class BuyingController(StockController):
stock_items = self.get_stock_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)
if pr_qty:
@ -423,6 +425,7 @@ class BuyingController(StockController):
sle = self.get_sl_entries(
d, {"actual_qty": flt(pr_qty), "serial_no": cstr(d.serial_no).strip()}
)
if self.is_return:
outgoing_rate = get_rate_for_return(
self.doctype, self.name, d.item_code, self.return_against, item_row=d
@ -452,18 +455,18 @@ class BuyingController(StockController):
sl_entries.append(from_warehouse_sle)
if flt(d.rejected_qty) != 0:
sl_entries.append(
self.get_sl_entries(
d,
{
"warehouse": d.rejected_warehouse,
"actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor),
"serial_no": cstr(d.rejected_serial_no).strip(),
"incoming_rate": 0.0,
},
)
if flt(d.rejected_qty) != 0:
sl_entries.append(
self.get_sl_entries(
d,
{
"warehouse": d.rejected_warehouse,
"actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor),
"serial_no": cstr(d.rejected_serial_no).strip(),
"incoming_rate": 0.0,
},
)
)
self.make_sl_entries(
sl_entries,

View File

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

View File

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

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