mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-16 19:58:49 +00:00
Compare commits
1 Commits
dembug
...
mergify/bp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbd718d98d |
@@ -38,7 +38,3 @@ ec74a5e56617bbd76ac402451468fd4668af543d
|
||||
|
||||
# ruff formatting
|
||||
a308792ee7fda18a681e9181f4fd00b36385bc23
|
||||
|
||||
# noisy typing refactoring of get_item_details
|
||||
7b7211ac79c248a79ba8a999ff34e734d874c0ae
|
||||
d827ed21adc7b36047e247cbb0dc6388d048a7f9
|
||||
|
||||
1
.github/helper/documentation.py
vendored
1
.github/helper/documentation.py
vendored
@@ -10,7 +10,6 @@ WEBSITE_REPOS = [
|
||||
|
||||
DOCUMENTATION_DOMAINS = [
|
||||
"docs.erpnext.com",
|
||||
"docs.frappe.io",
|
||||
"frappeframework.com",
|
||||
]
|
||||
|
||||
|
||||
13
.github/helper/install.sh
vendored
13
.github/helper/install.sh
vendored
@@ -6,7 +6,7 @@ cd ~ || exit
|
||||
|
||||
sudo apt update
|
||||
sudo apt remove mysql-server mysql-client
|
||||
sudo apt install libcups2-dev redis-server mariadb-client
|
||||
sudo apt install libcups2-dev redis-server mariadb-client-10.6
|
||||
|
||||
pip install frappe-bench
|
||||
|
||||
@@ -14,7 +14,6 @@ githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
|
||||
frappeuser=${FRAPPE_USER:-"frappe"}
|
||||
frappecommitish=${FRAPPE_BRANCH:-$githubbranch}
|
||||
|
||||
|
||||
mkdir frappe
|
||||
pushd frappe
|
||||
git init
|
||||
@@ -52,9 +51,13 @@ fi
|
||||
|
||||
|
||||
install_whktml() {
|
||||
wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
|
||||
sudo apt install /tmp/wkhtmltox.deb
|
||||
|
||||
if [ "$(lsb_release -rs)" = "22.04" ]; then
|
||||
wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
|
||||
sudo apt install /tmp/wkhtmltox.deb
|
||||
else
|
||||
echo "Please update this script to support wkhtmltopdf for $(lsb_release -ds)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
install_whktml &
|
||||
wkpid=$!
|
||||
|
||||
16
CODEOWNERS
16
CODEOWNERS
@@ -3,11 +3,11 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
erpnext/accounts/ @ruthra-kumar
|
||||
erpnext/assets/ @khushi8112
|
||||
erpnext/regional @ruthra-kumar
|
||||
erpnext/selling @ruthra-kumar
|
||||
erpnext/support/ @ruthra-kumar
|
||||
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/assets/ @khushi8112 @deepeshgarg007
|
||||
erpnext/regional @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/selling @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @deepeshgarg007
|
||||
pos*
|
||||
|
||||
erpnext/buying/ @rohitwaghchaure
|
||||
@@ -17,8 +17,8 @@ erpnext/quality_management/ @rohitwaghchaure
|
||||
erpnext/stock/ @rohitwaghchaure
|
||||
erpnext/subcontracting @rohitwaghchaure
|
||||
|
||||
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure
|
||||
erpnext/patches/ @ruthra-kumar
|
||||
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
|
||||
erpnext/patches/ @deepeshgarg007
|
||||
|
||||
.github/ @ruthra-kumar
|
||||
.github/ @deepeshgarg007
|
||||
pyproject.toml @akhilnarang
|
||||
|
||||
164
README.md
164
README.md
@@ -1,100 +1,57 @@
|
||||
<div align="center">
|
||||
<a href="https://erpnext.com">
|
||||
<img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80xp"/>
|
||||
<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>Powerful, Intuitive and Open-Source ERP</p>
|
||||
<p>ERP made simple</p>
|
||||
</p>
|
||||
|
||||
[](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml)
|
||||
[](https://www.codetriage.com/frappe/erpnext)
|
||||
[](https://codecov.io/gh/frappe/erpnext)
|
||||
[](https://hub.docker.com/r/frappe/erpnext-worker)
|
||||
|
||||
[https://erpnext.com](https://erpnext.com)
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<img src="./erpnext/public/images/v16/hero_image.png"/>
|
||||
ERPNext as a monolith includes the following areas for managing businesses:
|
||||
|
||||
1. [Accounting](https://erpnext.com/open-source-accounting)
|
||||
1. [Warehouse Management](https://erpnext.com/distribution/warehouse-management-system)
|
||||
1. [CRM](https://erpnext.com/open-source-crm)
|
||||
1. [Sales](https://erpnext.com/open-source-sales-purchase)
|
||||
1. [Purchase](https://erpnext.com/open-source-sales-purchase)
|
||||
1. [HRMS](https://erpnext.com/open-source-hrms)
|
||||
1. [Project Management](https://erpnext.com/open-source-projects)
|
||||
1. [Support](https://erpnext.com/open-source-help-desk-software)
|
||||
1. [Asset Management](https://erpnext.com/open-source-asset-management-software)
|
||||
1. [Quality Management](https://erpnext.com/docs/user/manual/en/quality-management)
|
||||
1. [Manufacturing](https://erpnext.com/open-source-manufacturing-erp-software)
|
||||
1. [Website Management](https://erpnext.com/open-source-website-builder-software)
|
||||
1. [Customize ERPNext](https://erpnext.com/docs/user/manual/en/customize-erpnext)
|
||||
1. [And More](https://erpnext.com/docs/user/manual/en/)
|
||||
|
||||
ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a full-stack web app framework built with Python & JavaScript.
|
||||
|
||||
## Installation
|
||||
|
||||
<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>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://erpnext-demo.frappe.cloud/app/home">Live Demo</a>
|
||||
-
|
||||
<a href="https://erpnext.com">Website</a>
|
||||
-
|
||||
<a href="https://docs.erpnext.com">Documentation</a>
|
||||
</div>
|
||||
> Login for the PWD site: (username: Administrator, password: admin)
|
||||
|
||||
## ERPNext
|
||||
### Containerized Installation
|
||||
|
||||
100% Open-Source ERP system to help you run your business.
|
||||
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.
|
||||
|
||||
### Motivation
|
||||
|
||||
Running a business is a complex task - handling invoices, tracking stock, managing personnel and even more ad-hoc activities. In a market where software is sold separately to manage each of these tasks, ERPNext does all of the above and more, for free.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Accounting**: All the tools you need to manage cash flow in one place, right from recording transactions to summarizing and analyzing financial reports.
|
||||
- **Order Management**: Track inventory levels, replenish stock, and manage sales orders, customers, suppliers, shipments, deliverables, and order fulfillment.
|
||||
- **Manufacturing**: Simplifies the production cycle, helps track material consumption, exhibits capacity planning, handles subcontracting, and more!
|
||||
- **Asset Management**: From purchase to perishment, IT infrastructure to equipment. Cover every branch of your organization, all in one centralized system.
|
||||
- **Projects**: Delivery both internal and external Projects on time, budget and Profitability. Track tasks, timesheets, and issues by project.
|
||||
|
||||
<details open>
|
||||
|
||||
<summary>More</summary>
|
||||
<img src="https://erpnext.com/files/v16_bom.png"/>
|
||||
<img src="https://erpnext.com/files/v16_stock_summary.png"/>
|
||||
<img src="https://erpnext.com/files/v16_job_card.png"/>
|
||||
<img src="https://erpnext.com/files/v16_tasks.png"/>
|
||||
</details>
|
||||
|
||||
### Under the Hood
|
||||
|
||||
- [**Frappe Framework**](https://github.com/frappe/frappe): A full-stack web application framework written in Python and Javascript. The framework provides a robust foundation for building web applications, including a database abstraction layer, user authentication, and a REST API.
|
||||
|
||||
- [**Frappe UI**](https://github.com/frappe/frappe-ui): A Vue-based UI library, to provide a modern user interface. The Frappe UI library provides a variety of components that can be used to build single-page applications on top of the Frappe Framework.
|
||||
|
||||
## Production Setup
|
||||
|
||||
### Managed Hosting
|
||||
|
||||
You can try [Frappe Cloud](https://frappecloud.com), a simple, user-friendly and sophisticated [open-source](https://github.com/frappe/press) platform to host Frappe applications with peace of mind.
|
||||
|
||||
It takes care of installation, setup, upgrades, monitoring, maintenance and support of your Frappe deployments. It is a fully featured developer platform with an ability to manage and control multiple Frappe deployments.
|
||||
|
||||
<div>
|
||||
<a href="https://erpnext-demo.frappe.cloud/app/home" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/try-on-fc-white.png">
|
||||
<img src="https://frappe.io/files/try-on-fc-black.png" alt="Try on Frappe Cloud" height="28" />
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
### Self-Hosted
|
||||
#### Docker
|
||||
|
||||
Prerequisites: docker, docker-compose, git. Refer [Docker Documentation](https://docs.docker.com) for more details on Docker setup.
|
||||
|
||||
Run following commands:
|
||||
|
||||
```
|
||||
git clone https://github.com/frappe/frappe_docker
|
||||
cd frappe_docker
|
||||
docker compose -f pwd.yml up -d
|
||||
```
|
||||
|
||||
After a couple of minutes, site should be accessible on your localhost port: 8080. Use below default login credentials to access the site.
|
||||
- Username: Administrator
|
||||
- Password: admin
|
||||
|
||||
See [Frappe Docker](https://github.com/frappe/frappe_docker?tab=readme-ov-file#to-run-on-arm64-architecture-follow-this-instructions) for ARM based docker setup.
|
||||
|
||||
|
||||
## Development Setup
|
||||
### 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.
|
||||
@@ -102,35 +59,6 @@ The Easy Way: our install script for bench will install all dependencies (e.g. M
|
||||
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).
|
||||
|
||||
|
||||
### Local
|
||||
|
||||
To setup the repository locally follow the steps mentioned below:
|
||||
|
||||
1. Setup bench by following the [Installation Steps](https://frappeframework.com/docs/user/en/installation) and start the server
|
||||
```
|
||||
bench start
|
||||
```
|
||||
|
||||
2. In a separate terminal window, run the following commands:
|
||||
```
|
||||
# Create a new site
|
||||
bench new-site erpnext.dev
|
||||
|
||||
# Map your site to localhost
|
||||
bench --site erpnext.dev add-to-hosts
|
||||
```
|
||||
|
||||
3. Get the ERPNext app and install it
|
||||
```
|
||||
# Get the ERPNext app
|
||||
bench get-app https://github.com/frappe/erpnext
|
||||
|
||||
# Install the app
|
||||
bench --site erpnext.dev install-app erpnext
|
||||
```
|
||||
|
||||
4. Open the URL `http://erpnext.dev:8000/app` in your browser, you should see the app running
|
||||
|
||||
## Learning and community
|
||||
|
||||
1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
|
||||
@@ -145,18 +73,14 @@ To setup the repository locally follow the steps mentioned below:
|
||||
1. [Report Security Vulnerabilities](https://erpnext.com/security)
|
||||
1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
|
||||
|
||||
## License
|
||||
|
||||
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.
|
||||
|
||||
By contributing to ERPNext, you agree that your contributions will be licensed under its GNU General Public License (v3).
|
||||
|
||||
## Logo and Trademark Policy
|
||||
|
||||
Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md).
|
||||
|
||||
<br />
|
||||
<br />
|
||||
<div align="center" style="padding-top: 0.75rem;">
|
||||
<a href="https://frappe.io" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/Frappe-white.png">
|
||||
<img src="https://frappe.io/files/Frappe-black.png" alt="Frappe Technologies" height="28"/>
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import functools
|
||||
import inspect
|
||||
from typing import TypeVar
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "16.0.0-dev"
|
||||
@@ -162,34 +160,3 @@ def check_app_permission():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def normalize_ctx_input(T: type) -> callable:
|
||||
"""
|
||||
Normalizes the first argument (ctx) of the decorated function by:
|
||||
- Converting Document objects to dictionaries
|
||||
- Parsing JSON strings
|
||||
- Casting the result to the specified type T
|
||||
"""
|
||||
|
||||
def decorator(func: callable):
|
||||
# conserve annotations for frappe.utils.typing_validations
|
||||
@functools.wraps(func, assigned=(a for a in functools.WRAPPER_ASSIGNMENTS if a != "__annotations__"))
|
||||
def wrapper(ctx: T | Document | dict | str, *args, **kwargs):
|
||||
if isinstance(ctx, Document):
|
||||
ctx = T(**ctx.as_dict())
|
||||
elif isinstance(ctx, dict):
|
||||
ctx = T(**ctx)
|
||||
else:
|
||||
ctx = T(**frappe.parse_json(ctx))
|
||||
|
||||
return func(ctx, *args, **kwargs)
|
||||
|
||||
# set annotations from function
|
||||
wrapper.__annotations__.update({k: v for k, v in func.__annotations__.items() if k != "ctx"})
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
"description": "Rate at which this tax is applied",
|
||||
"fieldname": "tax_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Tax Rate",
|
||||
"label": "Rate",
|
||||
"oldfieldname": "tax_rate",
|
||||
"oldfieldtype": "Currency"
|
||||
},
|
||||
|
||||
@@ -237,22 +237,19 @@ frappe.treeview_settings["Account"] = {
|
||||
},
|
||||
post_render: function (treeview) {
|
||||
frappe.treeview_settings["Account"].treeview["tree"] = treeview.tree;
|
||||
if (treeview.can_create) {
|
||||
treeview.page.set_primary_action(
|
||||
__("New"),
|
||||
function () {
|
||||
let root_company = treeview.page.fields_dict.root_company.get_value();
|
||||
if (root_company) {
|
||||
frappe.throw(__("Please add the account to root level Company - {0}"), [
|
||||
root_company,
|
||||
]);
|
||||
} else {
|
||||
treeview.new_node();
|
||||
}
|
||||
},
|
||||
"add"
|
||||
);
|
||||
}
|
||||
treeview.page.set_primary_action(
|
||||
__("New"),
|
||||
function () {
|
||||
let root_company = treeview.page.fields_dict.root_company.get_value();
|
||||
|
||||
if (root_company) {
|
||||
frappe.throw(__("Please add the account to root level Company - {0}"), [root_company]);
|
||||
} else {
|
||||
treeview.new_node();
|
||||
}
|
||||
},
|
||||
"add"
|
||||
);
|
||||
},
|
||||
toolbar: [
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,34 +0,0 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
syscohada_countries = [
|
||||
"bj", # Bénin
|
||||
"bf", # Burkina-Faso
|
||||
"cm", # Cameroun
|
||||
"cf", # Centrafrique
|
||||
"ci", # Côte d'Ivoire
|
||||
"cg", # Congo
|
||||
"km", # Comores
|
||||
"ga", # Gabon
|
||||
"gn", # Guinée
|
||||
"gw", # Guinée-Bissau
|
||||
"gq", # Guinée Equatoriale
|
||||
"ml", # Mali
|
||||
"ne", # Niger
|
||||
"cd", # République Démocratique du Congo
|
||||
"sn", # Sénégal
|
||||
"td", # Tchad
|
||||
"tg", # Togo
|
||||
]
|
||||
|
||||
folder = Path(__file__).parent
|
||||
generic_charts = Path(folder).glob("syscohada*.json")
|
||||
|
||||
for file in generic_charts:
|
||||
with open(file) as f:
|
||||
chart = json.load(f)
|
||||
for country in syscohada_countries:
|
||||
chart["country_code"] = country
|
||||
json_object = json.dumps(chart, indent=4)
|
||||
with open(Path(folder, file.name.replace("syscohada", country)), "w") as outfile:
|
||||
outfile.write(json_object)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,6 @@
|
||||
"col_break_1",
|
||||
"description",
|
||||
"included_in_paid_amount",
|
||||
"set_by_item_tax_template",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -104,7 +103,7 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax Rate",
|
||||
"label": "Rate",
|
||||
"oldfieldname": "rate",
|
||||
"oldfieldtype": "Currency"
|
||||
},
|
||||
@@ -184,8 +183,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Net Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "base_net_amount",
|
||||
@@ -195,22 +193,12 @@
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "set_by_item_tax_template",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Set by Item Tax Template",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-22 19:16:22.346267",
|
||||
"modified": "2024-09-24 06:51:07.417348",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Taxes and Charges",
|
||||
@@ -219,4 +207,4 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ class AdvanceTaxesandCharges(Document):
|
||||
parenttype: DF.Data
|
||||
rate: DF.Float
|
||||
row_id: DF.Data | None
|
||||
set_by_item_tax_template: DF.Check
|
||||
tax_amount: DF.Currency
|
||||
total: DF.Currency
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -48,7 +48,7 @@ class BankAccount(Document):
|
||||
self.name = self.account_name + " - " + self.bank
|
||||
|
||||
def on_trash(self):
|
||||
delete_contact_and_address("Bank Account", self.name)
|
||||
delete_contact_and_address("BankAccount", self.name)
|
||||
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
|
||||
@@ -117,9 +117,9 @@ class BankClearance(Document):
|
||||
)
|
||||
|
||||
else:
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
frappe.db.set_value(
|
||||
d.payment_document, d.payment_entry, "clearance_date", d.clearance_date
|
||||
)
|
||||
|
||||
clearance_date_updated = True
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ from frappe.utils import cint, flt
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
|
||||
get_amounts_not_reflected_in_system,
|
||||
get_entries,
|
||||
@@ -305,56 +304,54 @@ def create_payment_entry_bts(
|
||||
bank_transaction = frappe.db.get_values(
|
||||
"Bank Transaction",
|
||||
bank_transaction_name,
|
||||
fieldname=["name", "unallocated_amount", "deposit", "bank_account", "currency"],
|
||||
fieldname=["name", "unallocated_amount", "deposit", "bank_account"],
|
||||
as_dict=True,
|
||||
)[0]
|
||||
|
||||
paid_amount = bank_transaction.unallocated_amount
|
||||
payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
|
||||
|
||||
bank_account = frappe.get_cached_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
company = frappe.get_cached_value("Account", bank_account, "company")
|
||||
party_account = get_party_account(party_type, party, company)
|
||||
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
company = frappe.get_value("Account", company_account, "company")
|
||||
payment_entry_dict = {
|
||||
"company": company,
|
||||
"payment_type": payment_type,
|
||||
"reference_no": reference_number,
|
||||
"reference_date": reference_date,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"posting_date": posting_date,
|
||||
"paid_amount": paid_amount,
|
||||
"received_amount": paid_amount,
|
||||
}
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
bank_currency = bank_transaction.currency
|
||||
party_currency = frappe.get_cached_value("Account", party_account, "account_currency")
|
||||
payment_entry.update(payment_entry_dict)
|
||||
|
||||
exc_rate = get_exchange_rate(bank_currency, party_currency, posting_date)
|
||||
if mode_of_payment:
|
||||
payment_entry.mode_of_payment = mode_of_payment
|
||||
if project:
|
||||
payment_entry.project = project
|
||||
if cost_center:
|
||||
payment_entry.cost_center = cost_center
|
||||
if payment_type == "Receive":
|
||||
payment_entry.paid_to = company_account
|
||||
else:
|
||||
payment_entry.paid_from = company_account
|
||||
|
||||
amt_in_bank_acc_currency = bank_transaction.unallocated_amount
|
||||
amount_in_party_currency = bank_transaction.unallocated_amount * exc_rate
|
||||
|
||||
pe = frappe.new_doc("Payment Entry")
|
||||
pe.payment_type = payment_type
|
||||
pe.company = company
|
||||
pe.reference_no = reference_number
|
||||
pe.reference_date = reference_date
|
||||
pe.party_type = party_type
|
||||
pe.party = party
|
||||
pe.posting_date = posting_date
|
||||
pe.paid_from = party_account if payment_type == "Receive" else bank_account
|
||||
pe.paid_to = party_account if payment_type == "Pay" else bank_account
|
||||
pe.paid_from_account_currency = party_currency if payment_type == "Receive" else bank_currency
|
||||
pe.paid_to_account_currency = party_currency if payment_type == "Pay" else bank_currency
|
||||
pe.paid_amount = amount_in_party_currency if payment_type == "Receive" else amt_in_bank_acc_currency
|
||||
pe.received_amount = amount_in_party_currency if payment_type == "Pay" else amt_in_bank_acc_currency
|
||||
pe.mode_of_payment = mode_of_payment
|
||||
pe.project = project
|
||||
pe.cost_center = cost_center
|
||||
|
||||
pe.validate()
|
||||
payment_entry.validate()
|
||||
|
||||
if allow_edit:
|
||||
return pe
|
||||
return payment_entry
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
payment_entry.insert()
|
||||
|
||||
payment_entry.submit()
|
||||
vouchers = json.dumps(
|
||||
[
|
||||
{
|
||||
"payment_doctype": "Payment Entry",
|
||||
"payment_name": pe.name,
|
||||
"amount": amt_in_bank_acc_currency,
|
||||
"payment_name": payment_entry.name,
|
||||
"amount": paid_amount,
|
||||
}
|
||||
]
|
||||
)
|
||||
@@ -483,12 +480,8 @@ def get_linked_payments(
|
||||
def subtract_allocations(gl_account, vouchers):
|
||||
"Look up & subtract any existing Bank Transaction allocations"
|
||||
copied = []
|
||||
|
||||
voucher_docs = [(voucher.get("doctype"), voucher.get("name")) for voucher in vouchers]
|
||||
voucher_allocated_amounts = get_total_allocated_amount(voucher_docs)
|
||||
|
||||
for voucher in vouchers:
|
||||
rows = voucher_allocated_amounts.get((voucher.get("doctype"), voucher.get("name"))) or []
|
||||
rows = get_total_allocated_amount(voucher.get("doctype"), voucher.get("name"))
|
||||
filtered_row = list(filter(lambda row: row.get("gl_account") == gl_account, rows))
|
||||
|
||||
if amount := None if not filtered_row else filtered_row[0]["total"]:
|
||||
@@ -726,7 +719,7 @@ def get_pe_matching_query(
|
||||
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
|
||||
ConstantColumn("Payment Entry").as_("doctype"),
|
||||
pe.name,
|
||||
pe.base_paid_amount_after_tax.as_("paid_amount"),
|
||||
pe.paid_amount_after_tax.as_("paid_amount"),
|
||||
pe.reference_no,
|
||||
pe.reference_date,
|
||||
pe.party,
|
||||
|
||||
@@ -154,16 +154,10 @@ class BankTransaction(Document):
|
||||
"""
|
||||
remaining_amount = self.unallocated_amount
|
||||
to_remove = []
|
||||
payment_entry_docs = [(pe.payment_document, pe.payment_entry) for pe in self.payment_entries]
|
||||
pe_bt_allocations = get_total_allocated_amount(payment_entry_docs)
|
||||
|
||||
for payment_entry in self.payment_entries:
|
||||
if payment_entry.allocated_amount == 0.0:
|
||||
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
|
||||
self,
|
||||
payment_entry,
|
||||
pe_bt_allocations.get((payment_entry.payment_document, payment_entry.payment_entry))
|
||||
or [],
|
||||
self, payment_entry
|
||||
)
|
||||
|
||||
if 0.0 == unallocated_amount:
|
||||
@@ -214,17 +208,13 @@ class BankTransaction(Document):
|
||||
if self.party_type and self.party:
|
||||
return
|
||||
|
||||
result = None
|
||||
try:
|
||||
result = AutoMatchParty(
|
||||
bank_party_account_number=self.bank_party_account_number,
|
||||
bank_party_iban=self.bank_party_iban,
|
||||
bank_party_name=self.bank_party_name,
|
||||
description=self.description,
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
except Exception:
|
||||
frappe.log_error(title=_("Error in party matching for Bank Transaction {0}").format(self.name))
|
||||
result = AutoMatchParty(
|
||||
bank_party_account_number=self.bank_party_account_number,
|
||||
bank_party_iban=self.bank_party_iban,
|
||||
bank_party_name=self.bank_party_name,
|
||||
description=self.description,
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
|
||||
if not result:
|
||||
return
|
||||
@@ -238,7 +228,7 @@ def get_doctypes_for_bank_reconciliation():
|
||||
return frappe.get_hooks("bank_reconciliation_doctypes")
|
||||
|
||||
|
||||
def get_clearance_details(transaction, payment_entry, bt_allocations):
|
||||
def get_clearance_details(transaction, payment_entry):
|
||||
"""
|
||||
There should only be one bank gle for a voucher.
|
||||
Could be none for a Bank Transaction.
|
||||
@@ -247,6 +237,7 @@ def get_clearance_details(transaction, payment_entry, bt_allocations):
|
||||
"""
|
||||
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
||||
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
|
||||
bt_allocations = get_total_allocated_amount(payment_entry.payment_document, payment_entry.payment_entry)
|
||||
|
||||
unallocated_amount = min(
|
||||
transaction.unallocated_amount,
|
||||
@@ -299,52 +290,44 @@ def get_related_bank_gl_entries(doctype, docname):
|
||||
)
|
||||
|
||||
|
||||
def get_total_allocated_amount(docs):
|
||||
def get_total_allocated_amount(doctype, docname):
|
||||
"""
|
||||
Gets the sum of allocations for a voucher on each bank GL account
|
||||
along with the latest bank transaction name & date
|
||||
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
|
||||
"""
|
||||
if not docs:
|
||||
return {}
|
||||
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||
result = frappe.db.sql(
|
||||
"""
|
||||
SELECT total, latest_name, latest_date, gl_account, payment_document, payment_entry FROM (
|
||||
SELECT total, latest_name, latest_date, gl_account FROM (
|
||||
SELECT
|
||||
ROW_NUMBER() OVER w AS rownum,
|
||||
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account, btp.payment_document, btp.payment_entry) AS total,
|
||||
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total,
|
||||
FIRST_VALUE(bt.name) OVER w AS latest_name,
|
||||
FIRST_VALUE(bt.date) OVER w AS latest_date,
|
||||
ba.account AS gl_account,
|
||||
btp.payment_document,
|
||||
btp.payment_entry
|
||||
ba.account AS gl_account
|
||||
FROM
|
||||
`tabBank Transaction Payments` btp
|
||||
LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
|
||||
LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
|
||||
WHERE
|
||||
(btp.payment_document, btp.payment_entry) IN %(docs)s
|
||||
btp.payment_document = %(doctype)s
|
||||
AND btp.payment_entry = %(docname)s
|
||||
AND bt.docstatus = 1
|
||||
WINDOW w AS (PARTITION BY ba.account, btp.payment_document, btp.payment_entry ORDER BY bt.date DESC)
|
||||
WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc)
|
||||
) temp
|
||||
WHERE
|
||||
rownum = 1
|
||||
""",
|
||||
dict(docs=docs),
|
||||
dict(doctype=doctype, docname=docname),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
payment_allocation_details = {}
|
||||
for row in result:
|
||||
# Why is this *sometimes* a byte string?
|
||||
if isinstance(row["latest_name"], bytes):
|
||||
row["latest_name"] = row["latest_name"].decode()
|
||||
row["latest_date"] = frappe.utils.getdate(row["latest_date"])
|
||||
payment_allocation_details.setdefault((row["payment_document"], row["payment_entry"]), []).append(row)
|
||||
|
||||
return payment_allocation_details
|
||||
return result
|
||||
|
||||
|
||||
def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
|
||||
@@ -490,20 +490,13 @@ def get_actual_expense(args):
|
||||
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
|
||||
distribution = {}
|
||||
if monthly_distribution:
|
||||
mdp = frappe.qb.DocType("Monthly Distribution Percentage")
|
||||
md = frappe.qb.DocType("Monthly Distribution")
|
||||
|
||||
res = (
|
||||
frappe.qb.from_(mdp)
|
||||
.join(md)
|
||||
.on(mdp.parent == md.name)
|
||||
.select(mdp.month, mdp.percentage_allocation)
|
||||
.where(md.fiscal_year == fiscal_year)
|
||||
.where(md.name == monthly_distribution)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for d in res:
|
||||
for d in frappe.db.sql(
|
||||
"""select mdp.month, mdp.percentage_allocation
|
||||
from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md
|
||||
where mdp.parent=md.name and md.fiscal_year=%s""",
|
||||
fiscal_year,
|
||||
as_dict=1,
|
||||
):
|
||||
distribution.setdefault(d.month, d.percentage_allocation)
|
||||
|
||||
dt = frappe.get_cached_value("Fiscal Year", fiscal_year, "year_start_date")
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: !doc.from_external_ecomm_platform",
|
||||
"depends_on": "eval !doc.from_external_ecomm_platform",
|
||||
"fieldname": "pricing_rule",
|
||||
"fieldtype": "Link",
|
||||
"label": "Pricing Rule",
|
||||
@@ -125,7 +125,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-11-19 16:35:11.836441",
|
||||
"modified": "2024-06-28 06:17:01.833399",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Coupon Code",
|
||||
@@ -186,4 +186,4 @@
|
||||
"states": [],
|
||||
"title_field": "coupon_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,16 @@
|
||||
"currency",
|
||||
"column_break_11",
|
||||
"conversion_rate",
|
||||
"address_and_contact_section",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"column_break_16",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"section_break_6",
|
||||
"dunning_type",
|
||||
"column_break_8",
|
||||
@@ -46,21 +56,7 @@
|
||||
"income_account",
|
||||
"column_break_48",
|
||||
"cost_center",
|
||||
"amended_from",
|
||||
"address_and_contact_tab",
|
||||
"address_and_contact_section",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"column_break_vodj",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"section_break_xban",
|
||||
"column_break_16",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"column_break_lqmf"
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -182,8 +178,10 @@
|
||||
"label": "Rate of Interest (%) Yearly"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "address_and_contact_section",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address and Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_display",
|
||||
@@ -379,28 +377,11 @@
|
||||
{
|
||||
"fieldname": "column_break_48",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_and_contact_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_vodj",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_xban",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_lqmf",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-26 13:46:07.760867",
|
||||
"modified": "2024-03-27 13:08:19.176146",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning",
|
||||
|
||||
@@ -188,7 +188,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.paid_amount = 95
|
||||
pe.source_exchange_rate = 84.2105
|
||||
pe.source_exchange_rate = 84.211
|
||||
pe.received_amount = 8000
|
||||
pe.references = []
|
||||
pe.save().submit()
|
||||
@@ -229,7 +229,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
|
||||
row = next(x for x in je.accounts if x.account == self.debtors_usd)
|
||||
self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD
|
||||
row = next(x for x in je.accounts if x.account != self.debtors_usd)
|
||||
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.05) # in INR
|
||||
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR
|
||||
|
||||
# total_debit and total_credit will be 0.0, as JV is posting only to account currency fields
|
||||
self.assertEqual(flt(je.total_debit, precision), 0.0)
|
||||
|
||||
@@ -72,10 +72,10 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "More/Less than 12 months.",
|
||||
"description": "Less than 12 months.",
|
||||
"fieldname": "is_short_year",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Short/Long Year",
|
||||
"label": "Is Short Year",
|
||||
"set_only_once": 1
|
||||
}
|
||||
],
|
||||
|
||||
@@ -13,11 +13,7 @@ import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.accounts.party import (
|
||||
validate_account_party_type,
|
||||
validate_party_frozen_disabled,
|
||||
validate_party_gle_currency,
|
||||
)
|
||||
from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
|
||||
@@ -272,12 +268,8 @@ class GLEntry(Document):
|
||||
|
||||
def validate_party(self):
|
||||
validate_party_frozen_disabled(self.party_type, self.party)
|
||||
validate_account_party_type(self)
|
||||
|
||||
def validate_currency(self):
|
||||
if self.is_cancelled:
|
||||
return
|
||||
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
account_currency = get_account_currency(self.account)
|
||||
|
||||
|
||||
@@ -78,48 +78,3 @@ class TestGLEntry(IntegrationTestCase):
|
||||
"SELECT current from tabSeries where name = %s", naming_series
|
||||
)[0][0]
|
||||
self.assertEqual(old_naming_series_current_value + 2, new_naming_series_current_value)
|
||||
|
||||
def test_validate_account_party_type(self):
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
100,
|
||||
"_Test Cost Center - _TC",
|
||||
save=False,
|
||||
submit=False,
|
||||
)
|
||||
|
||||
for row in jv.accounts:
|
||||
row.party_type = "Supplier"
|
||||
break
|
||||
|
||||
jv.save()
|
||||
try:
|
||||
jv.submit()
|
||||
except Exception as e:
|
||||
self.assertEqual(
|
||||
str(e),
|
||||
"Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC",
|
||||
)
|
||||
|
||||
jv1 = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
100,
|
||||
"_Test Cost Center - _TC",
|
||||
save=False,
|
||||
submit=False,
|
||||
)
|
||||
|
||||
for row in jv.accounts:
|
||||
row.party_type = "Customer"
|
||||
break
|
||||
|
||||
jv1.save()
|
||||
try:
|
||||
jv1.submit()
|
||||
except Exception as e:
|
||||
self.assertEqual(
|
||||
str(e),
|
||||
"Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC",
|
||||
)
|
||||
|
||||
@@ -127,6 +127,9 @@ class JournalEntry(AccountsController):
|
||||
self.set_amounts_in_company_currency()
|
||||
self.validate_debit_credit_amount()
|
||||
self.set_total_debit_credit()
|
||||
# Do not validate while importing via data import
|
||||
if not frappe.flags.in_import:
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
if not frappe.flags.is_reverse_depr_entry:
|
||||
self.validate_against_jv()
|
||||
@@ -181,11 +184,6 @@ class JournalEntry(AccountsController):
|
||||
else:
|
||||
return self._cancel()
|
||||
|
||||
def before_submit(self):
|
||||
# Do not validate while importing via data import
|
||||
if not frappe.flags.in_import:
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_cheque_info()
|
||||
self.check_credit_limit()
|
||||
|
||||
@@ -523,55 +523,6 @@ class TestJournalEntry(IntegrationTestCase):
|
||||
self.assertEqual(row.debit_in_account_currency, 100)
|
||||
self.assertEqual(row.credit_in_account_currency, 100)
|
||||
|
||||
def test_toggle_debit_credit_if_negative(self):
|
||||
from erpnext.accounts.general_ledger import process_gl_map
|
||||
|
||||
# Create JV with defaut cost center - _Test Cost Center
|
||||
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
|
||||
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.posting_date = nowdate()
|
||||
jv.company = "_Test Company"
|
||||
jv.user_remark = "test"
|
||||
jv.extend(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": "_Test Cash - _TC",
|
||||
"debit": 100 * -1,
|
||||
"debit_in_account_currency": 100 * -1,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
{
|
||||
"account": "_Test Bank - _TC",
|
||||
"credit": 100 * -1,
|
||||
"credit_in_account_currency": 100 * -1,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
jv.flags.ignore_validate = True
|
||||
jv.save()
|
||||
|
||||
self.assertEqual(len(jv.accounts), 2)
|
||||
|
||||
gl_map = jv.build_gl_map()
|
||||
|
||||
for row in gl_map:
|
||||
if row.account == "_Test Cash - _TC":
|
||||
self.assertEqual(row.debit, 100 * -1)
|
||||
self.assertEqual(row.debit_in_account_currency, 100 * -1)
|
||||
self.assertEqual(row.debit_in_transaction_currency, 100 * -1)
|
||||
|
||||
gl_map = process_gl_map(gl_map, False)
|
||||
|
||||
for row in gl_map:
|
||||
if row.account == "_Test Cash - _TC":
|
||||
self.assertEqual(row.credit, 100)
|
||||
self.assertEqual(row.credit_in_account_currency, 100)
|
||||
self.assertEqual(row.credit_in_transaction_currency, 100)
|
||||
|
||||
def test_transaction_exchange_rate_on_journals(self):
|
||||
jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable USD - _TC", 100, save=False)
|
||||
jv.accounts[0].update({"debit_in_account_currency": 8500, "exchange_rate": 1})
|
||||
|
||||
@@ -27,18 +27,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
|
||||
// project excluded in setup_dimension_filters
|
||||
frm.set_query("project", function (doc) {
|
||||
let filters = {
|
||||
company: doc.company,
|
||||
};
|
||||
if (doc.party_type == "Customer") filters.customer = doc.party;
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_project_name",
|
||||
filters,
|
||||
};
|
||||
});
|
||||
|
||||
if (frm.is_new()) {
|
||||
set_default_party_type(frm);
|
||||
}
|
||||
@@ -197,10 +185,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
filters: {
|
||||
reference_doctype: row.reference_doctype,
|
||||
reference_name: row.reference_name,
|
||||
company: doc.company,
|
||||
status: ["!=", "Paid"],
|
||||
outstanding_amount: [">", 0], // for compatibility with old data
|
||||
docstatus: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -336,6 +320,11 @@ frappe.ui.form.on("Payment Entry", {
|
||||
"write_off_difference_amount",
|
||||
frm.doc.difference_amount && frm.doc.party && 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
|
||||
);
|
||||
},
|
||||
|
||||
set_dynamic_labels: function (frm) {
|
||||
@@ -1126,34 +1115,36 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
|
||||
set_unallocated_amount: function (frm) {
|
||||
let unallocated_amount = 0;
|
||||
let deductions_to_consider = 0;
|
||||
|
||||
for (const row of frm.doc.deductions || []) {
|
||||
if (!row.is_exchange_gain_loss) deductions_to_consider += flt(row.amount);
|
||||
}
|
||||
const included_taxes = get_included_taxes(frm);
|
||||
var unallocated_amount = 0;
|
||||
var total_deductions = frappe.utils.sum(
|
||||
$.map(frm.doc.deductions || [], function (d) {
|
||||
return flt(d.amount);
|
||||
})
|
||||
);
|
||||
|
||||
if (frm.doc.party) {
|
||||
if (
|
||||
frm.doc.payment_type == "Receive" &&
|
||||
frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount + deductions_to_consider
|
||||
frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions &&
|
||||
frm.doc.total_allocated_amount <
|
||||
frm.doc.paid_amount + total_deductions / frm.doc.source_exchange_rate
|
||||
) {
|
||||
unallocated_amount =
|
||||
(frm.doc.base_paid_amount +
|
||||
deductions_to_consider -
|
||||
frm.doc.base_total_allocated_amount -
|
||||
included_taxes) /
|
||||
(frm.doc.base_received_amount +
|
||||
total_deductions -
|
||||
flt(frm.doc.base_total_taxes_and_charges) -
|
||||
frm.doc.base_total_allocated_amount) /
|
||||
frm.doc.source_exchange_rate;
|
||||
} else if (
|
||||
frm.doc.payment_type == "Pay" &&
|
||||
frm.doc.base_total_allocated_amount < frm.doc.base_received_amount - deductions_to_consider
|
||||
frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions &&
|
||||
frm.doc.total_allocated_amount <
|
||||
frm.doc.received_amount + total_deductions / frm.doc.target_exchange_rate
|
||||
) {
|
||||
unallocated_amount =
|
||||
(frm.doc.base_received_amount -
|
||||
deductions_to_consider -
|
||||
frm.doc.base_total_allocated_amount -
|
||||
included_taxes) /
|
||||
(frm.doc.base_paid_amount +
|
||||
flt(frm.doc.base_total_taxes_and_charges) -
|
||||
(total_deductions + frm.doc.base_total_allocated_amount)) /
|
||||
frm.doc.target_exchange_rate;
|
||||
}
|
||||
}
|
||||
@@ -1247,85 +1238,77 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
|
||||
write_off_difference_amount: function (frm) {
|
||||
frm.events.set_write_off_deduction(frm);
|
||||
frm.events.set_deductions_entry(frm, "write_off_account");
|
||||
},
|
||||
|
||||
base_paid_amount: function (frm) {
|
||||
frm.events.set_exchange_gain_loss_deduction(frm);
|
||||
set_exchange_gain_loss: function (frm) {
|
||||
frm.events.set_deductions_entry(frm, "exchange_gain_loss_account");
|
||||
},
|
||||
|
||||
base_received_amount: function (frm) {
|
||||
frm.events.set_exchange_gain_loss_deduction(frm);
|
||||
},
|
||||
set_deductions_entry: function (frm, account) {
|
||||
if (frm.doc.difference_amount) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_company_defaults",
|
||||
args: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
callback: function (r, rt) {
|
||||
if (r.message) {
|
||||
const write_off_row = $.map(frm.doc["deductions"] || [], function (t) {
|
||||
return t.account == r.message[account] ? t : null;
|
||||
});
|
||||
|
||||
set_exchange_gain_loss_deduction: async function (frm) {
|
||||
// wait for allocate_party_amount_against_ref_docs to finish
|
||||
await frappe.after_ajax();
|
||||
const base_paid_amount = frm.doc.base_paid_amount || 0;
|
||||
const base_received_amount = frm.doc.base_received_amount || 0;
|
||||
const exchange_gain_loss = flt(
|
||||
base_paid_amount - base_received_amount,
|
||||
get_deduction_amount_precision()
|
||||
);
|
||||
const difference_amount = flt(
|
||||
frm.doc.difference_amount,
|
||||
precision("difference_amount")
|
||||
);
|
||||
|
||||
if (!exchange_gain_loss) {
|
||||
frm.events.delete_exchange_gain_loss(frm);
|
||||
return;
|
||||
const add_deductions = (details) => {
|
||||
let row = null;
|
||||
if (!write_off_row.length && difference_amount) {
|
||||
row = frm.add_child("deductions");
|
||||
row.account = details[account];
|
||||
row.cost_center = details["cost_center"];
|
||||
} else {
|
||||
row = write_off_row[0];
|
||||
}
|
||||
|
||||
if (row) {
|
||||
row.amount = flt(row.amount) + difference_amount;
|
||||
} else {
|
||||
frappe.msgprint(__("No gain or loss in the exchange rate"));
|
||||
}
|
||||
refresh_field("deductions");
|
||||
};
|
||||
|
||||
if (!r.message[account]) {
|
||||
frappe.prompt(
|
||||
{
|
||||
label: __("Please Specify Account"),
|
||||
fieldname: account,
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
get_query: () => ({
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
}),
|
||||
},
|
||||
(values) => {
|
||||
const details = Object.assign({}, r.message, values);
|
||||
add_deductions(details);
|
||||
},
|
||||
__(frappe.unscrub(account))
|
||||
);
|
||||
} else {
|
||||
add_deductions(r.message);
|
||||
}
|
||||
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const account_fieldname = "exchange_gain_loss_account";
|
||||
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
|
||||
|
||||
if (!row) {
|
||||
const response = await get_company_defaults(frm.doc.company);
|
||||
|
||||
const account =
|
||||
response.message?.[account_fieldname] ||
|
||||
(await prompt_for_missing_account(frm, account_fieldname));
|
||||
|
||||
row = frm.add_child("deductions");
|
||||
row.account = account;
|
||||
row.cost_center = response.message?.cost_center;
|
||||
row.is_exchange_gain_loss = 1;
|
||||
}
|
||||
|
||||
row.amount = exchange_gain_loss;
|
||||
frm.refresh_field("deductions");
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
delete_exchange_gain_loss: function (frm) {
|
||||
const exchange_gain_loss_row = (frm.doc.deductions || []).find((row) => row.is_exchange_gain_loss);
|
||||
|
||||
if (!exchange_gain_loss_row) return;
|
||||
|
||||
exchange_gain_loss_row.amount = 0;
|
||||
frm.get_field("deductions").grid.grid_rows[exchange_gain_loss_row.idx - 1].remove();
|
||||
frm.refresh_field("deductions");
|
||||
},
|
||||
|
||||
set_write_off_deduction: async function (frm) {
|
||||
const difference_amount = flt(frm.doc.difference_amount, get_deduction_amount_precision());
|
||||
if (!difference_amount) return;
|
||||
|
||||
const account_fieldname = "write_off_account";
|
||||
const response = await get_company_defaults(frm.doc.company);
|
||||
const write_off_account =
|
||||
response.message?.[account_fieldname] ||
|
||||
(await prompt_for_missing_account(frm, account_fieldname));
|
||||
|
||||
if (!write_off_account) return;
|
||||
|
||||
let row = (frm.doc["deductions"] || []).find((t) => t.account == write_off_account);
|
||||
if (!row) {
|
||||
row = frm.add_child("deductions");
|
||||
row.account = write_off_account;
|
||||
row.cost_center = response.message?.cost_center;
|
||||
}
|
||||
|
||||
row.amount = flt(row.amount) + difference_amount;
|
||||
frm.refresh_field("deductions");
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
bank_account: function (frm) {
|
||||
@@ -1340,24 +1323,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
if (r.message) {
|
||||
if (!frm.doc.mode_of_payment) {
|
||||
frm.set_value(field, r.message.account);
|
||||
} else {
|
||||
frappe.call({
|
||||
method: "frappe.client.get_value",
|
||||
args: {
|
||||
doctype: "Mode of Payment Account",
|
||||
filters: {
|
||||
parent: frm.doc.mode_of_payment,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
fieldname: "default_account",
|
||||
parent: "Mode of Payment",
|
||||
},
|
||||
callback: function (res) {
|
||||
if (!res.message.default_account) {
|
||||
frm.set_value(field, r.message.account);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
frm.set_value("bank", r.message.bank);
|
||||
frm.set_value("bank_account_no", r.message.bank_account_no);
|
||||
@@ -1809,13 +1774,6 @@ frappe.ui.form.on("Advance Taxes and Charges", {
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Payment Entry Deduction", {
|
||||
before_deductions_remove: function (doc, cdt, cdn) {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
if (row.is_exchange_gain_loss && row.amount) {
|
||||
frappe.throw(__("Cannot delete Exchange Gain/Loss row"));
|
||||
}
|
||||
},
|
||||
|
||||
amount: function (frm) {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
@@ -1837,53 +1795,3 @@ function set_default_party_type(frm) {
|
||||
|
||||
if (party_type) frm.set_value("party_type", party_type);
|
||||
}
|
||||
|
||||
function get_included_taxes(frm) {
|
||||
let included_taxes = 0;
|
||||
for (const tax of frm.doc.taxes) {
|
||||
if (!tax.included_in_paid_amount) continue;
|
||||
|
||||
if (tax.add_deduct_tax == "Add") {
|
||||
included_taxes += tax.base_tax_amount;
|
||||
} else {
|
||||
included_taxes -= tax.base_tax_amount;
|
||||
}
|
||||
}
|
||||
|
||||
return included_taxes;
|
||||
}
|
||||
|
||||
function get_company_defaults(company) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_company_defaults",
|
||||
args: {
|
||||
company: company,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function prompt_for_missing_account(frm, account) {
|
||||
return new Promise((resolve) => {
|
||||
const dialog = frappe.prompt(
|
||||
{
|
||||
label: __(frappe.unscrub(account)),
|
||||
fieldname: account,
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
get_query: () => ({
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
}),
|
||||
},
|
||||
(values) => resolve(values?.[account]),
|
||||
__("Please Specify Account")
|
||||
);
|
||||
|
||||
dialog.on_hide = () => resolve("");
|
||||
});
|
||||
}
|
||||
|
||||
function get_deduction_amount_precision() {
|
||||
return frappe.meta.get_field_precision(frappe.meta.get_field("Payment Entry Deduction", "amount"));
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"section_break_34",
|
||||
"total_allocated_amount",
|
||||
"base_total_allocated_amount",
|
||||
"set_exchange_gain_loss",
|
||||
"column_break_36",
|
||||
"unallocated_amount",
|
||||
"difference_amount",
|
||||
@@ -390,6 +391,11 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "set_exchange_gain_loss",
|
||||
"fieldtype": "Button",
|
||||
"label": "Set Exchange Gain / Loss"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_36",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -796,7 +802,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2024-11-07 11:19:19.320883",
|
||||
"modified": "2024-05-31 17:07:06.197249",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -985,7 +985,6 @@ class PaymentEntry(AccountsController):
|
||||
self.set_amounts_in_company_currency()
|
||||
self.set_total_allocated_amount()
|
||||
self.set_unallocated_amount()
|
||||
self.set_exchange_gain_loss()
|
||||
self.set_difference_amount()
|
||||
|
||||
def validate_amounts(self):
|
||||
@@ -1084,10 +1083,10 @@ class PaymentEntry(AccountsController):
|
||||
if d.exchange_rate is None:
|
||||
d.exchange_rate = 1
|
||||
|
||||
allocated_amount_in_ref_exchange_rate = flt(
|
||||
allocated_amount_in_pe_exchange_rate = flt(
|
||||
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_ref_exchange_rate
|
||||
d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_pe_exchange_rate
|
||||
return base_allocated_amount
|
||||
|
||||
def set_total_allocated_amount(self):
|
||||
@@ -1105,80 +1104,29 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def set_unallocated_amount(self):
|
||||
self.unallocated_amount = 0
|
||||
if not self.party:
|
||||
return
|
||||
|
||||
deductions_to_consider = sum(
|
||||
flt(d.amount) for d in self.get("deductions") if not d.is_exchange_gain_loss
|
||||
)
|
||||
included_taxes = self.get_included_taxes()
|
||||
|
||||
if self.payment_type == "Receive" and self.base_total_allocated_amount < (
|
||||
self.base_paid_amount + deductions_to_consider
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_paid_amount
|
||||
+ deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
) / self.source_exchange_rate
|
||||
elif self.payment_type == "Pay" and self.base_total_allocated_amount < (
|
||||
self.base_received_amount - deductions_to_consider
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_received_amount
|
||||
- deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
) / self.target_exchange_rate
|
||||
|
||||
def set_exchange_gain_loss(self):
|
||||
exchange_gain_loss = flt(
|
||||
self.base_paid_amount - self.base_received_amount,
|
||||
self.precision("amount", "deductions"),
|
||||
)
|
||||
|
||||
exchange_gain_loss_rows = [row for row in self.get("deductions") if row.is_exchange_gain_loss]
|
||||
exchange_gain_loss_row = exchange_gain_loss_rows.pop(0) if exchange_gain_loss_rows else None
|
||||
|
||||
for row in exchange_gain_loss_rows:
|
||||
self.remove(row)
|
||||
|
||||
if not exchange_gain_loss:
|
||||
if exchange_gain_loss_row:
|
||||
self.remove(exchange_gain_loss_row)
|
||||
|
||||
return
|
||||
|
||||
if not exchange_gain_loss_row:
|
||||
values = frappe.get_cached_value(
|
||||
"Company", self.company, ("exchange_gain_loss_account", "cost_center"), as_dict=True
|
||||
)
|
||||
|
||||
for fieldname, value in values.items():
|
||||
if value:
|
||||
continue
|
||||
|
||||
label = _(frappe.get_meta("Company").get_label(fieldname))
|
||||
return frappe.msgprint(
|
||||
_("Please set {0} in Company {1} to account for Exchange Gain / Loss").format(
|
||||
label, get_link_to_form("Company", self.company)
|
||||
),
|
||||
title=_("Missing Default in Company"),
|
||||
indicator="red" if self.docstatus.is_submitted() else "yellow",
|
||||
raise_exception=self.docstatus.is_submitted(),
|
||||
)
|
||||
|
||||
exchange_gain_loss_row = self.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": values.exchange_gain_loss_account,
|
||||
"cost_center": values.cost_center,
|
||||
"is_exchange_gain_loss": 1,
|
||||
},
|
||||
)
|
||||
|
||||
exchange_gain_loss_row.amount = exchange_gain_loss
|
||||
if self.party:
|
||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
||||
included_taxes = self.get_included_taxes()
|
||||
if (
|
||||
self.payment_type == "Receive"
|
||||
and self.base_total_allocated_amount < self.base_received_amount + total_deductions
|
||||
and self.total_allocated_amount
|
||||
< flt(self.paid_amount) + (total_deductions / self.source_exchange_rate)
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_received_amount + total_deductions - self.base_total_allocated_amount
|
||||
) / self.source_exchange_rate
|
||||
self.unallocated_amount -= included_taxes
|
||||
elif (
|
||||
self.payment_type == "Pay"
|
||||
and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions)
|
||||
and self.total_allocated_amount
|
||||
< flt(self.received_amount) + (total_deductions / self.target_exchange_rate)
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)
|
||||
) / self.target_exchange_rate
|
||||
self.unallocated_amount -= included_taxes
|
||||
|
||||
def set_difference_amount(self):
|
||||
base_unallocated_amount = flt(self.unallocated_amount) * (
|
||||
@@ -1206,13 +1154,11 @@ class PaymentEntry(AccountsController):
|
||||
def get_included_taxes(self):
|
||||
included_taxes = 0
|
||||
for tax in self.get("taxes"):
|
||||
if not tax.included_in_paid_amount:
|
||||
continue
|
||||
|
||||
if tax.add_deduct_tax == "Add":
|
||||
included_taxes += tax.base_tax_amount
|
||||
else:
|
||||
included_taxes -= tax.base_tax_amount
|
||||
if tax.included_in_paid_amount:
|
||||
if tax.add_deduct_tax == "Add":
|
||||
included_taxes += tax.base_tax_amount
|
||||
else:
|
||||
included_taxes -= tax.base_tax_amount
|
||||
|
||||
return included_taxes
|
||||
|
||||
@@ -1372,19 +1318,11 @@ class PaymentEntry(AccountsController):
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gle.update(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": cost_center,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
{
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
"cost_center": cost_center,
|
||||
}
|
||||
)
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
@@ -1910,7 +1848,7 @@ class PaymentEntry(AccountsController):
|
||||
if paid_amount > total_negative_outstanding:
|
||||
if total_negative_outstanding == 0:
|
||||
frappe.msgprint(
|
||||
_("Cannot {0} from {1} without any negative outstanding invoice").format(
|
||||
_("Cannot {0} from {2} without any negative outstanding invoice").format(
|
||||
self.payment_type,
|
||||
self.party_type,
|
||||
)
|
||||
@@ -2068,8 +2006,8 @@ class PaymentEntry(AccountsController):
|
||||
def get_matched_payment_request_of_references(references=None):
|
||||
"""
|
||||
Get those `Payment Requests` which are matched with `References`.\n
|
||||
- Amount must be same.
|
||||
- Only single `Payment Request` available for this amount.
|
||||
- Amount must be same.
|
||||
- Only single `Payment Request` available for this amount.
|
||||
|
||||
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
|
||||
"""
|
||||
@@ -2171,7 +2109,7 @@ def get_outstanding_of_references_with_payment_term(references=None):
|
||||
def get_outstanding_of_references_with_no_payment_term(references):
|
||||
"""
|
||||
Fetch outstanding amount of `References` which have no `Payment Term` set.\n
|
||||
- Fetch outstanding amount from `References` it self.
|
||||
- Fetch outstanding amount from `References` it self.
|
||||
|
||||
Note: `None` is used for allocation of `Payment Request`
|
||||
Example: {(reference_doctype, reference_name, None): outstanding_amount, ...}
|
||||
@@ -2985,6 +2923,9 @@ def get_payment_entry(
|
||||
update_accounting_dimensions(pe, doc)
|
||||
|
||||
if party_account and bank:
|
||||
pe.set_exchange_rate(ref_doc=doc)
|
||||
pe.set_amounts()
|
||||
|
||||
if discount_amount:
|
||||
base_total_discount_loss = 0
|
||||
if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
|
||||
@@ -2994,8 +2935,7 @@ def get_payment_entry(
|
||||
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||
)
|
||||
|
||||
pe.set_exchange_rate(ref_doc=doc)
|
||||
pe.set_amounts()
|
||||
pe.set_difference_amount()
|
||||
|
||||
# If PE is created from PR directly, then no need to find open PRs for the references
|
||||
if not created_from_payment_request:
|
||||
@@ -3007,7 +2947,7 @@ def get_payment_entry(
|
||||
def get_open_payment_requests_for_references(references=None):
|
||||
"""
|
||||
Fetch all unpaid Payment Requests for the references. \n
|
||||
- Each reference can have multiple Payment Requests. \n
|
||||
- Each reference can have multiple Payment Requests. \n
|
||||
|
||||
Example: {("Sales Invoice", "SINV-00001"): {"PREQ-00001": 1000, "PREQ-00002": 2000}}
|
||||
"""
|
||||
@@ -3031,7 +2971,6 @@ def get_open_payment_requests_for_references(references=None):
|
||||
.where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs)))
|
||||
.where(PR.status != "Paid")
|
||||
.where(PR.docstatus == 1)
|
||||
.where(PR.outstanding_amount > 0) # to avoid old PRs with 0 outstanding amount
|
||||
.orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
|
||||
).run(as_dict=True)
|
||||
|
||||
@@ -3346,14 +3285,13 @@ def set_pending_discount_loss(pe, doc, discount_amount, base_total_discount_loss
|
||||
book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss")
|
||||
account_type = "round_off_account" if book_tax_loss else "default_discount_account"
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
pe.set_gain_or_loss(
|
||||
account_details={
|
||||
"account": frappe.get_cached_value("Company", pe.company, account_type),
|
||||
"cost_center": pe.cost_center
|
||||
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": discount_amount * positive_negative,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -488,9 +488,16 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
|
||||
|
||||
# Exchange loss
|
||||
self.assertEqual(pe.deductions[-1].amount, 300.0)
|
||||
pe.deductions[-1].account = "_Test Exchange Gain/Loss - _TC"
|
||||
pe.deductions[-1].cost_center = "_Test Cost Center - _TC"
|
||||
self.assertEqual(pe.difference_amount, 300.0)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 300.0,
|
||||
},
|
||||
)
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
@@ -554,10 +561,16 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = "2016-01-01"
|
||||
|
||||
self.assertEqual(pe.deductions[0].amount, 100)
|
||||
pe.deductions[0].account = "_Test Exchange Gain/Loss - _TC"
|
||||
pe.deductions[0].cost_center = "_Test Cost Center - _TC"
|
||||
self.assertEqual(pe.difference_amount, 100)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 100,
|
||||
},
|
||||
)
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
@@ -647,9 +660,16 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
pe.set_exchange_rate()
|
||||
pe.set_amounts()
|
||||
|
||||
self.assertEqual(pe.deductions[0].amount, 500)
|
||||
pe.deductions[0].account = "_Test Exchange Gain/Loss - _TC"
|
||||
pe.deductions[0].cost_center = "_Test Cost Center - _TC"
|
||||
self.assertEqual(pe.difference_amount, 500)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 500,
|
||||
},
|
||||
)
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
@@ -1519,7 +1539,7 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
parent_account="Current Liabilities - _TC",
|
||||
account_name="Advances Paid",
|
||||
company=company,
|
||||
account_type="Payable",
|
||||
account_type="Liability",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"cost_center",
|
||||
"amount",
|
||||
"column_break_2",
|
||||
"is_exchange_gain_loss",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
@@ -46,20 +45,12 @@
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.is_exchange_gain_loss",
|
||||
"fieldname": "is_exchange_gain_loss",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Exchange Gain / Loss?",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-05 16:07:47.307971",
|
||||
"modified": "2024-03-27 13:10:09.454552",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Deduction",
|
||||
|
||||
@@ -18,7 +18,6 @@ class PaymentEntryDeduction(Document):
|
||||
amount: DF.Currency
|
||||
cost_center: DF.Link
|
||||
description: DF.SmallText | None
|
||||
is_exchange_gain_loss: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -153,7 +153,10 @@ class PaymentReconciliation(Document):
|
||||
self.add_payment_entries(non_reconciled_payments)
|
||||
|
||||
def get_payment_entries(self):
|
||||
party_account = [self.receivable_payable_account]
|
||||
if self.default_advance_account:
|
||||
party_account = [self.receivable_payable_account, self.default_advance_account]
|
||||
else:
|
||||
party_account = [self.receivable_payable_account]
|
||||
|
||||
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
|
||||
condition = frappe._dict(
|
||||
@@ -184,7 +187,6 @@ class PaymentReconciliation(Document):
|
||||
self.party,
|
||||
party_account,
|
||||
order_doctype,
|
||||
default_advance_account=self.default_advance_account,
|
||||
against_all_orders=True,
|
||||
limit=self.payment_limit,
|
||||
condition=condition,
|
||||
@@ -209,14 +211,12 @@ class PaymentReconciliation(Document):
|
||||
if self.get("cost_center"):
|
||||
conditions.append(jea.cost_center == self.cost_center)
|
||||
|
||||
account_type = erpnext.get_party_account_type(self.party_type)
|
||||
|
||||
if account_type == "Receivable":
|
||||
dr_or_cr = jea.credit_in_account_currency - jea.debit_in_account_currency
|
||||
elif account_type == "Payable":
|
||||
dr_or_cr = jea.debit_in_account_currency - jea.credit_in_account_currency
|
||||
|
||||
conditions.append(dr_or_cr.gt(0))
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
conditions.append(jea[dr_or_cr].gt(0))
|
||||
|
||||
if self.bank_cash_account:
|
||||
conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
|
||||
@@ -231,7 +231,7 @@ class PaymentReconciliation(Document):
|
||||
je.posting_date,
|
||||
je.remark.as_("remarks"),
|
||||
jea.name.as_("reference_row"),
|
||||
dr_or_cr.as_("amount"),
|
||||
jea[dr_or_cr].as_("amount"),
|
||||
jea.is_advance,
|
||||
jea.exchange_rate,
|
||||
jea.account_currency.as_("currency"),
|
||||
|
||||
@@ -641,42 +641,6 @@ class TestPaymentReconciliation(IntegrationTestCase):
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
def test_negative_debit_or_credit_journal_against_invoice(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
|
||||
# credit debtors account to record a payment
|
||||
je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
|
||||
je.accounts[1].party_type = "Customer"
|
||||
je.accounts[1].party = self.customer
|
||||
je.accounts[1].credit_in_account_currency = 0
|
||||
je.accounts[1].debit_in_account_currency = -1 * amount
|
||||
je.save()
|
||||
je.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Difference amount should not be calculated for base currency accounts
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
# assert outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
|
||||
# check PR tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
def test_journal_against_journal(self):
|
||||
transaction_date = nowdate()
|
||||
sales = "Sales - _PR"
|
||||
@@ -999,100 +963,6 @@ class TestPaymentReconciliation(IntegrationTestCase):
|
||||
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
||||
)
|
||||
|
||||
def test_difference_amount_via_negative_debit_or_credit_journal_entry(self):
|
||||
# Make Sale Invoice
|
||||
si = self.create_sales_invoice(
|
||||
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
si.customer = self.customer4
|
||||
si.currency = "EUR"
|
||||
si.conversion_rate = 85
|
||||
si.debit_to = self.debtors_eur
|
||||
si.save().submit()
|
||||
|
||||
# Make payment using Journal Entry
|
||||
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
|
||||
je1.multi_currency = 1
|
||||
je1.accounts[0].exchange_rate = 1
|
||||
je1.accounts[0].credit_in_account_currency = -8000
|
||||
je1.accounts[0].credit = -8000
|
||||
je1.accounts[0].debit_in_account_currency = 0
|
||||
je1.accounts[0].debit = 0
|
||||
je1.accounts[1].party_type = "Customer"
|
||||
je1.accounts[1].party = self.customer4
|
||||
je1.accounts[1].exchange_rate = 80
|
||||
je1.accounts[1].credit_in_account_currency = 100
|
||||
je1.accounts[1].credit = 8000
|
||||
je1.accounts[1].debit_in_account_currency = 0
|
||||
je1.accounts[1].debit = 0
|
||||
je1.save()
|
||||
je1.submit()
|
||||
|
||||
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
|
||||
je2.multi_currency = 1
|
||||
je2.accounts[0].exchange_rate = 1
|
||||
je2.accounts[0].credit_in_account_currency = -16000
|
||||
je2.accounts[0].credit = -16000
|
||||
je2.accounts[0].debit_in_account_currency = 0
|
||||
je2.accounts[0].debit = 0
|
||||
je2.accounts[1].party_type = "Customer"
|
||||
je2.accounts[1].party = self.customer4
|
||||
je2.accounts[1].exchange_rate = 80
|
||||
je2.accounts[1].credit_in_account_currency = 200
|
||||
je1.accounts[1].credit = 16000
|
||||
je1.accounts[1].debit_in_account_currency = 0
|
||||
je1.accounts[1].debit = 0
|
||||
je2.save()
|
||||
je2.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party = self.customer4
|
||||
pr.receivable_payable_account = self.debtors_eur
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 2)
|
||||
|
||||
# Test exact payment allocation
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[0].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||
|
||||
# Test partial payment allocation (with excess payment entry)
|
||||
pr.set("allocation", [])
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[1].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
|
||||
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||
|
||||
# Check if difference journal entry gets generated for difference amount after reconciliation
|
||||
pr.reconcile()
|
||||
total_credit_amount = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
|
||||
"sum(credit) as amount",
|
||||
group_by="reference_name",
|
||||
)[0].amount
|
||||
|
||||
# total credit includes the exchange gain/loss amount
|
||||
self.assertEqual(flt(total_credit_amount, 2), 8500)
|
||||
|
||||
jea_parent = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
|
||||
fields=["parent"],
|
||||
)[0]
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
||||
)
|
||||
|
||||
def test_difference_amount_via_payment_entry(self):
|
||||
# Make Sale Invoice
|
||||
si = self.create_sales_invoice(
|
||||
|
||||
@@ -60,9 +60,7 @@
|
||||
"payment_order",
|
||||
"amended_from",
|
||||
"column_break_pnyv",
|
||||
"payment_url",
|
||||
"column_break_iiuv",
|
||||
"phone_number"
|
||||
"payment_url"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -380,7 +378,6 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_channel==\"Phone\"",
|
||||
"fetch_from": "payment_gateway_account.payment_channel",
|
||||
"fieldname": "payment_channel",
|
||||
"fieldtype": "Select",
|
||||
@@ -447,22 +444,13 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_iiuv",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "phone_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Phone Number"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-04 05:39:32.448857",
|
||||
"modified": "2024-10-23 12:23:40.117336",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
|
||||
@@ -81,7 +81,6 @@ class PaymentRequest(Document):
|
||||
payment_order: DF.Link | None
|
||||
payment_request_type: DF.Literal["Outward", "Inward"]
|
||||
payment_url: DF.Data | None
|
||||
phone_number: DF.Data | None
|
||||
print_format: DF.Literal[None]
|
||||
project: DF.Link | None
|
||||
reference_doctype: DF.Link | None
|
||||
@@ -222,7 +221,6 @@ class PaymentRequest(Document):
|
||||
sender=self.email_to,
|
||||
currency=self.currency,
|
||||
payment_gateway=self.payment_gateway,
|
||||
phone_number=self.phone_number,
|
||||
)
|
||||
|
||||
controller.validate_transaction_currency(self.currency)
|
||||
@@ -565,8 +563,6 @@ def make_payment_request(**args):
|
||||
# fetches existing payment request `grand_total` amount
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
|
||||
|
||||
existing_paid_amount = get_existing_paid_amount(ref_doc.doctype, ref_doc.name)
|
||||
|
||||
def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount):
|
||||
grand_total -= existing_payment_request_amount
|
||||
if not grand_total:
|
||||
@@ -586,15 +582,6 @@ def make_payment_request(**args):
|
||||
else:
|
||||
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
|
||||
|
||||
if existing_paid_amount:
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
if ref_doc.conversion_rate:
|
||||
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
|
||||
else:
|
||||
grand_total -= flt(existing_paid_amount)
|
||||
else:
|
||||
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
|
||||
|
||||
if draft_payment_request:
|
||||
frappe.db.set_value(
|
||||
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
|
||||
@@ -645,7 +632,6 @@ def make_payment_request(**args):
|
||||
or args.order_type == "Shopping Cart" # compat for webshop app
|
||||
or gateway_account.get("payment_channel", "Email") != "Email"
|
||||
),
|
||||
"phone_number": args.get("phone_number") if args.get("phone_number") else None,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -772,29 +758,6 @@ def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None =
|
||||
return response[0][0] if response[0] else 0
|
||||
|
||||
|
||||
def get_existing_paid_amount(doctype, name):
|
||||
PL = frappe.qb.DocType("Payment Ledger Entry")
|
||||
PER = frappe.qb.DocType("Payment Entry Reference")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(PL)
|
||||
.left_join(PER)
|
||||
.on(
|
||||
(PER.reference_doctype == PL.against_voucher_type) & (PER.reference_name == PL.against_voucher_no)
|
||||
)
|
||||
.select(Abs(Sum(PL.amount)).as_("total_paid_amount"))
|
||||
.where(PL.against_voucher_type.eq(doctype))
|
||||
.where(PL.against_voucher_no.eq(name))
|
||||
.where(PL.amount < 0)
|
||||
.where(PL.delinked == 0)
|
||||
.where(PER.docstatus == 1)
|
||||
.where(PER.payment_request.isnull())
|
||||
)
|
||||
response = query.run()
|
||||
|
||||
return response[0][0] if response[0] else 0
|
||||
|
||||
|
||||
def get_gateway_details(args): # nosemgrep
|
||||
"""
|
||||
Return gateway and payment account of default payment gateway
|
||||
@@ -982,17 +945,21 @@ def validate_payment(doc, method=None):
|
||||
@frappe.whitelist()
|
||||
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
# permission checks in `get_list()`
|
||||
filters = frappe._dict(filters)
|
||||
reference_doctype = filters.get("reference_doctype")
|
||||
reference_name = filters.get("reference_doctype")
|
||||
|
||||
if not filters.reference_doctype or not filters.reference_name:
|
||||
if not reference_doctype or not reference_name:
|
||||
return []
|
||||
|
||||
if txt:
|
||||
filters.name = ["like", f"%{txt}%"]
|
||||
|
||||
open_payment_requests = frappe.get_list(
|
||||
"Payment Request",
|
||||
filters=filters,
|
||||
filters={
|
||||
"reference_doctype": filters["reference_doctype"],
|
||||
"reference_name": filters["reference_name"],
|
||||
"status": ["!=", "Paid"],
|
||||
"outstanding_amount": ["!=", 0], # for compatibility with old data
|
||||
"docstatus": 1,
|
||||
},
|
||||
fields=["name", "grand_total", "outstanding_amount"],
|
||||
order_by="transaction_date ASC,creation ASC",
|
||||
)
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "payment_request",
|
||||
"internal_links": {
|
||||
"Payment Entry": ["references", "payment_request"],
|
||||
"Payment Order": ["references", "payment_order"],
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Payment"), "items": ["Payment Entry", "Payment Order"]},
|
||||
],
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
const INDICATORS = {
|
||||
"Partially Paid": "orange",
|
||||
Cancelled: "red",
|
||||
Draft: "red",
|
||||
Failed: "red",
|
||||
Initiated: "green",
|
||||
Paid: "blue",
|
||||
Requested: "green",
|
||||
};
|
||||
|
||||
frappe.listview_settings["Payment Request"] = {
|
||||
add_fields: ["status"],
|
||||
get_indicator: function (doc) {
|
||||
if (!doc.status || !INDICATORS[doc.status]) return;
|
||||
|
||||
return [__(doc.status), INDICATORS[doc.status], `status,=,${doc.status}`];
|
||||
if (doc.status == "Draft") {
|
||||
return [__("Draft"), "gray", "status,=,Draft"];
|
||||
}
|
||||
if (doc.status == "Requested") {
|
||||
return [__("Requested"), "green", "status,=,Requested"];
|
||||
} else if (doc.status == "Initiated") {
|
||||
return [__("Initiated"), "green", "status,=,Initiated"];
|
||||
} else if (doc.status == "Partially Paid") {
|
||||
return [__("Partially Paid"), "orange", "status,=,Partially Paid"];
|
||||
} else if (doc.status == "Paid") {
|
||||
return [__("Paid"), "blue", "status,=,Paid"];
|
||||
} else if (doc.status == "Failed") {
|
||||
return [__("Failed"), "red", "status,=,Failed"];
|
||||
} else if (doc.status == "Cancelled") {
|
||||
return [__("Cancelled"), "red", "status,=,Cancelled"];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@ from unittest.mock import patch
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
@@ -690,48 +689,3 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.advance_payment_status, "Requested")
|
||||
|
||||
def test_partial_paid_invoice_with_payment_request(self):
|
||||
si = create_sales_invoice(currency="INR", qty=1, rate=5000)
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PAYEE0002"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
si.load_from_db()
|
||||
pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
|
||||
|
||||
self.assertEqual(pr.grand_total, si.outstanding_amount)
|
||||
|
||||
|
||||
def test_partial_paid_invoice_with_submitted_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0001"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
pe.cancel()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0002"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
pi.load_from_db()
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
@@ -171,7 +171,9 @@ class PeriodClosingVoucher(AccountsController):
|
||||
pl_account_balances = self.get_account_balances_based_on_dimensions(report_type="Profit and Loss")
|
||||
for dimensions, account_balances in pl_account_balances.items():
|
||||
for acc, balances in account_balances.items():
|
||||
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
|
||||
balance_in_company_currency = flt(balances.debit_in_account_currency) - flt(
|
||||
balances.credit_in_account_currency
|
||||
)
|
||||
if balance_in_company_currency and acc != "balances":
|
||||
self.pl_accounts_reverse_gle.append(
|
||||
self.get_gle_for_pl_account(acc, balances, dimensions)
|
||||
|
||||
@@ -12,15 +12,15 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-left font-bold">{{ _("Grand Total") }}</td>
|
||||
<td class="text-left font-bold">{{ _('Grand Total') }}</td>
|
||||
<td class='text-right'> {{ frappe.utils.fmt_money(data.grand_total or '', currency=currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-left font-bold">{{ _("Net Total") }}</td>
|
||||
<td class="text-left font-bold">{{ _('Net Total') }}</td>
|
||||
<td class='text-right'> {{ frappe.utils.fmt_money(data.net_total or '', currency=currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-left font-bold">{{ _("Total Quantity") }}</td>
|
||||
<td class="text-left font-bold">{{ _('Total Quantity') }}</td>
|
||||
<td class='text-right'>{{ data.total_quantity or '' }}</td>
|
||||
</tr>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<tbody>
|
||||
{% for d in data.payment_reconciliation %}
|
||||
<tr>
|
||||
<td class="text-left">{{ _(d.mode_of_payment) }}</td>
|
||||
<td class="text-left">{{ d.mode_of_payment }}</td>
|
||||
<td class='text-right'> {{ frappe.utils.fmt_money(d.expected_amount - d.opening_amount, currency=currency) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -63,7 +63,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">{{ _("Account") }}</th>
|
||||
<th class="text-left">{{ _("Tax Rate") }}</th>
|
||||
<th class="text-left">{{ _("Rate") }}</th>
|
||||
<th class="text-right">{{ _("Amount") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -147,7 +147,7 @@ frappe.ui.form.on("POS Closing Entry", {
|
||||
frm.doc.grand_total += flt(doc.grand_total);
|
||||
frm.doc.net_total += flt(doc.net_total);
|
||||
frm.doc.total_quantity += flt(doc.total_qty);
|
||||
refresh_payments(doc, frm, false);
|
||||
refresh_payments(doc, frm);
|
||||
refresh_taxes(doc, frm);
|
||||
refresh_fields(frm);
|
||||
set_html_data(frm);
|
||||
@@ -172,7 +172,7 @@ function set_form_data(data, frm) {
|
||||
frm.doc.grand_total += flt(d.grand_total);
|
||||
frm.doc.net_total += flt(d.net_total);
|
||||
frm.doc.total_quantity += flt(d.total_qty);
|
||||
refresh_payments(d, frm, true);
|
||||
refresh_payments(d, frm);
|
||||
refresh_taxes(d, frm);
|
||||
});
|
||||
}
|
||||
@@ -186,7 +186,7 @@ function add_to_pos_transaction(d, frm) {
|
||||
});
|
||||
}
|
||||
|
||||
function refresh_payments(d, frm, is_new) {
|
||||
function refresh_payments(d, frm) {
|
||||
d.payments.forEach((p) => {
|
||||
const payment = frm.doc.payment_reconciliation.find(
|
||||
(pay) => pay.mode_of_payment === p.mode_of_payment
|
||||
@@ -196,7 +196,9 @@ function refresh_payments(d, frm, is_new) {
|
||||
}
|
||||
if (payment) {
|
||||
payment.expected_amount += flt(p.amount);
|
||||
if (is_new) payment.closing_amount = payment.expected_amount;
|
||||
if (payment.closing_amount === 0) {
|
||||
payment.closing_amount = payment.expected_amount;
|
||||
}
|
||||
payment.difference = payment.closing_amount - payment.expected_amount;
|
||||
} else {
|
||||
frm.add_child("payment_reconciliation", {
|
||||
|
||||
@@ -218,25 +218,11 @@ class TestPOSClosingEntry(IntegrationTestCase):
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_inv = create_pos_invoice(
|
||||
item_code=item_code,
|
||||
qty=5,
|
||||
rate=300,
|
||||
use_serial_batch_fields=1,
|
||||
batch_no=batch_no,
|
||||
do_not_submit=True,
|
||||
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
|
||||
)
|
||||
pos_inv.payments[0].amount = pos_inv.grand_total
|
||||
pos_inv.submit()
|
||||
pos_inv2 = create_pos_invoice(
|
||||
item_code=item_code,
|
||||
qty=5,
|
||||
rate=300,
|
||||
use_serial_batch_fields=1,
|
||||
batch_no=batch_no,
|
||||
do_not_submit=True,
|
||||
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
|
||||
)
|
||||
pos_inv2.payments[0].amount = pos_inv2.grand_total
|
||||
pos_inv2.submit()
|
||||
|
||||
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
|
||||
self.assertEqual(batch_qty, 10)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax Rate",
|
||||
"label": "Rate",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
"shipping_address",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"company_contact_person",
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
@@ -1550,10 +1549,10 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_medium",
|
||||
"print_hide": 1,
|
||||
"fieldtype": "Link",
|
||||
"label": "Medium",
|
||||
"options": "UTM Medium",
|
||||
"print_hide": 1
|
||||
"options": "UTM Medium"
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_campaign",
|
||||
@@ -1572,19 +1571,12 @@
|
||||
"oldfieldtype": "Select",
|
||||
"options": "UTM Source",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-26 13:10:50.309570",
|
||||
"modified": "2024-06-28 10:55:34.941200",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
@@ -1638,4 +1630,4 @@
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,12 @@ class POSInvoice(SalesInvoice):
|
||||
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
|
||||
from erpnext.accounts.doctype.pos_invoice_item.pos_invoice_item import POSInvoiceItem
|
||||
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
|
||||
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import SalesInvoiceAdvance
|
||||
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import SalesInvoicePayment
|
||||
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import (
|
||||
SalesInvoiceAdvance,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import (
|
||||
SalesInvoicePayment,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import (
|
||||
SalesInvoiceTimesheet,
|
||||
)
|
||||
@@ -70,7 +74,6 @@ class POSInvoice(SalesInvoice):
|
||||
company: DF.Link
|
||||
company_address: DF.Link | None
|
||||
company_address_display: DF.TextEditor | None
|
||||
company_contact_person: DF.Link | None
|
||||
consolidated_invoice: DF.Link | None
|
||||
contact_display: DF.SmallText | None
|
||||
contact_email: DF.Data | None
|
||||
@@ -237,7 +240,6 @@ class POSInvoice(SalesInvoice):
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count
|
||||
|
||||
update_coupon_code_count(self.coupon_code, "used")
|
||||
self.clear_unallocated_mode_of_payments()
|
||||
|
||||
def before_cancel(self):
|
||||
if (
|
||||
@@ -276,12 +278,6 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
self.delink_serial_and_batch_bundle()
|
||||
|
||||
def clear_unallocated_mode_of_payments(self):
|
||||
self.set("payments", self.get("payments", {"amount": ["not in", [0, None, ""]]}))
|
||||
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
frappe.qb.from_(sip).delete().where(sip.parent == self.name).where(sip.amount == 0).run()
|
||||
|
||||
def delink_serial_and_batch_bundle(self):
|
||||
for row in self.items:
|
||||
if row.serial_and_batch_bundle:
|
||||
@@ -537,11 +533,7 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
def set_pos_fields(self, for_validate=False):
|
||||
"""Set retail related fields from POS Profiles"""
|
||||
from erpnext.stock.get_item_details import (
|
||||
ItemDetailsCtx,
|
||||
get_pos_profile,
|
||||
get_pos_profile_item_details_,
|
||||
)
|
||||
from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details
|
||||
|
||||
if not self.pos_profile:
|
||||
pos_profile = get_pos_profile(self.company) or {}
|
||||
@@ -610,8 +602,8 @@ class POSInvoice(SalesInvoice):
|
||||
# set pos values in items
|
||||
for item in self.get("items"):
|
||||
if item.get("item_code"):
|
||||
profile_details = get_pos_profile_item_details_(
|
||||
ItemDetailsCtx(item.as_dict()), profile.get("company"), profile
|
||||
profile_details = get_pos_profile_item_details(
|
||||
profile.get("company"), frappe._dict(item.as_dict()), profile
|
||||
)
|
||||
for fname, val in profile_details.items():
|
||||
if (not for_validate) or (for_validate and not item.get(fname)):
|
||||
|
||||
@@ -118,13 +118,11 @@ class POSInvoiceMergeLog(Document):
|
||||
sales = [d for d in pos_invoice_docs if d.get("is_return") == 0]
|
||||
|
||||
sales_invoice, credit_note = "", ""
|
||||
sales_invoice_doc = None
|
||||
if sales:
|
||||
sales_invoice_doc = self.process_merging_into_sales_invoice(sales)
|
||||
sales_invoice = sales_invoice_doc.name
|
||||
|
||||
if returns:
|
||||
credit_note = self.process_merging_into_credit_note(returns, sales_invoice_doc)
|
||||
credit_note = self.process_merging_into_credit_note(returns)
|
||||
|
||||
if sales:
|
||||
sales_invoice = self.process_merging_into_sales_invoice(sales)
|
||||
|
||||
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
|
||||
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
|
||||
@@ -135,7 +133,6 @@ class POSInvoiceMergeLog(Document):
|
||||
self.update_pos_invoices(pos_invoice_docs)
|
||||
self.serial_and_batch_bundle_reference_for_pos_invoice()
|
||||
self.cancel_linked_invoices()
|
||||
self.delink_serial_and_batch_bundle()
|
||||
|
||||
def process_merging_into_sales_invoice(self, data):
|
||||
sales_invoice = self.get_new_sales_invoice()
|
||||
@@ -143,35 +140,20 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
sales_invoice.is_consolidated = 1
|
||||
sales_invoice.set_posting_time = 1
|
||||
|
||||
if not sales_invoice.posting_date:
|
||||
sales_invoice.posting_date = getdate(self.posting_date)
|
||||
|
||||
if not sales_invoice.posting_time:
|
||||
sales_invoice.posting_time = get_time(self.posting_time)
|
||||
|
||||
sales_invoice.posting_date = getdate(self.posting_date)
|
||||
sales_invoice.posting_time = get_time(self.posting_time)
|
||||
sales_invoice.save()
|
||||
sales_invoice.submit()
|
||||
|
||||
self.consolidated_invoice = sales_invoice.name
|
||||
|
||||
return sales_invoice
|
||||
return sales_invoice.name
|
||||
|
||||
def process_merging_into_credit_note(self, data, sales_invoice_doc=None):
|
||||
def process_merging_into_credit_note(self, data):
|
||||
credit_note = self.get_new_sales_invoice()
|
||||
credit_note.is_return = 1
|
||||
|
||||
credit_note = self.merge_pos_invoice_into(credit_note, data)
|
||||
referenes = {}
|
||||
|
||||
if sales_invoice_doc:
|
||||
credit_note.return_against = sales_invoice_doc.name
|
||||
|
||||
for d in sales_invoice_doc.items:
|
||||
referenes[d.item_code] = d.name
|
||||
|
||||
for d in credit_note.items:
|
||||
d.sales_invoice_item = referenes.get(d.item_code)
|
||||
|
||||
credit_note.is_consolidated = 1
|
||||
credit_note.set_posting_time = 1
|
||||
@@ -199,10 +181,6 @@ class POSInvoiceMergeLog(Document):
|
||||
for doc in data:
|
||||
map_doc(doc, invoice, table_map={"doctype": invoice.doctype})
|
||||
|
||||
if doc.get("posting_date"):
|
||||
invoice.posting_date = getdate(doc.posting_date)
|
||||
invoice.posting_time = get_time(doc.posting_time)
|
||||
|
||||
if doc.redeem_loyalty_points:
|
||||
invoice.loyalty_redemption_account = doc.loyalty_redemption_account
|
||||
invoice.loyalty_redemption_cost_center = doc.loyalty_redemption_cost_center
|
||||
@@ -320,8 +298,6 @@ class POSInvoiceMergeLog(Document):
|
||||
sales_invoice = frappe.new_doc("Sales Invoice")
|
||||
sales_invoice.customer = self.customer
|
||||
sales_invoice.is_pos = 1
|
||||
sales_invoice.posting_date = None
|
||||
sales_invoice.posting_time = None
|
||||
|
||||
return sales_invoice
|
||||
|
||||
@@ -344,45 +320,8 @@ class POSInvoiceMergeLog(Document):
|
||||
for table_name in ["items", "packed_items"]:
|
||||
pos_invoice.set_serial_and_batch_bundle(table_name)
|
||||
|
||||
def delink_serial_and_batch_bundle(self):
|
||||
bundles = self.get_serial_and_batch_bundles()
|
||||
if not bundles:
|
||||
return
|
||||
|
||||
sle_table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
query = (
|
||||
frappe.qb.update(sle_table)
|
||||
.set(sle_table.serial_and_batch_bundle, None)
|
||||
.where(sle_table.serial_and_batch_bundle.isin(bundles) & sle_table.is_cancelled == 1)
|
||||
)
|
||||
|
||||
query.run()
|
||||
|
||||
def get_serial_and_batch_bundles(self):
|
||||
pos_invoices = []
|
||||
for d in self.pos_invoices:
|
||||
pos_invoices.append(d.pos_invoice)
|
||||
|
||||
if pos_invoices:
|
||||
return frappe.get_all(
|
||||
"POS Invoice Item",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"parent": ["in", pos_invoices],
|
||||
"serial_and_batch_bundle": ["is", "set"],
|
||||
},
|
||||
pluck="serial_and_batch_bundle",
|
||||
)
|
||||
|
||||
return []
|
||||
|
||||
def cancel_linked_invoices(self):
|
||||
invoices = [self.consolidated_invoice, self.consolidated_credit_note]
|
||||
if not invoices:
|
||||
return
|
||||
|
||||
invoices.reverse()
|
||||
for si_name in invoices:
|
||||
for si_name in [self.consolidated_invoice, self.consolidated_credit_note]:
|
||||
if not si_name:
|
||||
continue
|
||||
si = frappe.get_doc("Sales Invoice", si_name)
|
||||
@@ -564,9 +503,6 @@ def cancel_merge_logs(merge_logs, closing_entry=None):
|
||||
try:
|
||||
for log in merge_logs:
|
||||
merge_log = frappe.get_doc("POS Invoice Merge Log", log)
|
||||
if merge_log.docstatus == 2:
|
||||
continue
|
||||
|
||||
merge_log.flags.ignore_permissions = True
|
||||
merge_log.cancel()
|
||||
|
||||
|
||||
@@ -134,7 +134,6 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
|
||||
},
|
||||
)
|
||||
inv.insert()
|
||||
inv.payments[0].amount = inv.grand_total
|
||||
inv.submit()
|
||||
|
||||
inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True)
|
||||
@@ -151,7 +150,6 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
|
||||
},
|
||||
)
|
||||
inv2.insert()
|
||||
inv2.payments[0].amount = inv.grand_total
|
||||
inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -159,19 +157,16 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
|
||||
|
||||
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
|
||||
item_wise_tax_detail = json.loads(consolidated_invoice.get("taxes")[0].item_wise_tax_detail)
|
||||
expected_item_wise_tax_detail = {
|
||||
"_Test Item": {
|
||||
"tax_rate": 9,
|
||||
"tax_amount": 9,
|
||||
"net_amount": 100,
|
||||
},
|
||||
"_Test Item 2": {
|
||||
"tax_rate": 5,
|
||||
"tax_amount": 5,
|
||||
"net_amount": 100,
|
||||
},
|
||||
}
|
||||
self.assertEqual(item_wise_tax_detail, expected_item_wise_tax_detail)
|
||||
|
||||
tax_data = item_wise_tax_detail.get("_Test Item")
|
||||
self.assertEqual(tax_data.get("tax_rate"), 9)
|
||||
self.assertEqual(tax_data.get("tax_amount"), 9)
|
||||
self.assertEqual(tax_data.get("net_amount"), 100)
|
||||
|
||||
tax_data = item_wise_tax_detail.get("_Test Item 2")
|
||||
self.assertEqual(tax_data.get("tax_rate"), 5)
|
||||
self.assertEqual(tax_data.get("tax_amount"), 5)
|
||||
self.assertEqual(tax_data.get("net_amount"), 100)
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
@@ -415,8 +415,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
"parent": args.parent,
|
||||
"parenttype": args.parenttype,
|
||||
"child_docname": args.get("child_docname"),
|
||||
"discount_percentage": 0.0,
|
||||
"discount_amount": 0,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -453,16 +451,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or []
|
||||
)
|
||||
|
||||
if pricing_rule.coupon_code_based == 1:
|
||||
if not args.coupon_code:
|
||||
return item_details
|
||||
|
||||
coupon_code = frappe.db.get_value(
|
||||
doctype="Coupon Code", filters={"pricing_rule": pricing_rule.name}, fieldname="name"
|
||||
)
|
||||
if args.coupon_code != coupon_code:
|
||||
continue
|
||||
|
||||
if pricing_rule.get("suggestion"):
|
||||
continue
|
||||
|
||||
@@ -488,6 +476,9 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
pricing_rule.apply_rule_on_other_items
|
||||
)
|
||||
|
||||
if pricing_rule.coupon_code_based == 1 and args.coupon_code is None:
|
||||
return item_details
|
||||
|
||||
if not pricing_rule.validate_applied_rule:
|
||||
if pricing_rule.price_or_product_discount == "Price":
|
||||
apply_price_discount_rule(pricing_rule, item_details, args)
|
||||
|
||||
@@ -1147,45 +1147,6 @@ class TestPricingRule(IntegrationTestCase):
|
||||
so.save()
|
||||
self.assertEqual(len(so.items), 1)
|
||||
|
||||
def test_pricing_rule_for_product_free_item_round_free_qty(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"rate": 0,
|
||||
"min_qty": 100,
|
||||
"max_qty": 0,
|
||||
"price_or_product_discount": "Product",
|
||||
"same_item": 1,
|
||||
"free_qty": 10,
|
||||
"round_free_qty": 1,
|
||||
"is_recursive": 1,
|
||||
"recurse_for": 100,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
# With pricing rule
|
||||
so = make_sales_order(item_code="_Test Item", qty=100)
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||
self.assertEqual(so.items[1].qty, 10)
|
||||
|
||||
so = make_sales_order(item_code="_Test Item", qty=150)
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||
self.assertEqual(so.items[1].qty, 10)
|
||||
|
||||
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
@@ -651,20 +651,11 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
|
||||
qty = pricing_rule.free_qty or 1
|
||||
if pricing_rule.is_recursive:
|
||||
transaction_qty = sum(
|
||||
[
|
||||
row.qty
|
||||
for row in doc.items
|
||||
if not row.is_free_item
|
||||
and row.item_code == args.item_code
|
||||
and row.pricing_rules == args.pricing_rules
|
||||
]
|
||||
)
|
||||
transaction_qty = transaction_qty - pricing_rule.apply_recursion_over
|
||||
if transaction_qty and transaction_qty > 0:
|
||||
transaction_qty = (args.get("qty") if args else doc.total_qty) - pricing_rule.apply_recursion_over
|
||||
if transaction_qty:
|
||||
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
||||
if pricing_rule.round_free_qty:
|
||||
qty = (flt(transaction_qty) // pricing_rule.recurse_for) * (pricing_rule.free_qty or 1)
|
||||
qty = math.floor(qty)
|
||||
|
||||
if not qty:
|
||||
return
|
||||
|
||||
@@ -244,9 +244,9 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "cc_to",
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"fieldtype": "Link",
|
||||
"label": "CC To",
|
||||
"options": "Process Statement Of Accounts CC"
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
@@ -400,7 +400,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-12-11 12:11:13.543134",
|
||||
"modified": "2024-10-18 17:51:39.108481",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -31,9 +31,6 @@ class ProcessStatementOfAccounts(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.process_statement_of_accounts_cc.process_statement_of_accounts_cc import (
|
||||
ProcessStatementOfAccountsCC,
|
||||
)
|
||||
from erpnext.accounts.doctype.process_statement_of_accounts_customer.process_statement_of_accounts_customer import (
|
||||
ProcessStatementOfAccountsCustomer,
|
||||
)
|
||||
@@ -44,7 +41,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
ageing_based_on: DF.Literal["Due Date", "Posting Date"]
|
||||
based_on_payment_terms: DF.Check
|
||||
body: DF.TextEditor | None
|
||||
cc_to: DF.TableMultiSelect[ProcessStatementOfAccountsCC]
|
||||
cc_to: DF.Link | None
|
||||
collection_name: DF.DynamicLink | None
|
||||
company: DF.Link
|
||||
cost_center: DF.TableMultiSelect[PSOACostCenter]
|
||||
@@ -327,7 +324,7 @@ def get_recipients_and_cc(customer, doc):
|
||||
cc = []
|
||||
if doc.cc_to != "":
|
||||
try:
|
||||
cc = [frappe.get_value("User", user.cc, "email") for user in doc.cc_to]
|
||||
cc = [frappe.get_value("User", doc.cc_to, "email")]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -477,7 +474,6 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
|
||||
reference_doctype="Process Statement Of Accounts",
|
||||
reference_name=document_name,
|
||||
attachments=attachments,
|
||||
expose_recipients="header",
|
||||
)
|
||||
|
||||
if doc.enable_auto_email and from_scheduler:
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2024-12-11 12:10:04.654593",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"cc"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "cc",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "CC",
|
||||
"options": "User"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-11 12:10:39.772598",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts CC",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ProcessStatementOfAccountsCC(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
cc: DF.Link | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -403,8 +403,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
hide_fields(this.frm.doc);
|
||||
if (cint(this.frm.doc.is_paid)) {
|
||||
this.frm.set_value("allocate_advances_automatically", 0);
|
||||
this.frm.set_value("payment_terms_template", "");
|
||||
this.frm.set_value("payment_schedule", []);
|
||||
if (!this.frm.doc.company) {
|
||||
this.frm.set_value("is_paid", 0);
|
||||
frappe.msgprint(__("Please specify Company to proceed"));
|
||||
|
||||
@@ -1683,12 +1683,7 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
if pi:
|
||||
pi = pi[0][0]
|
||||
|
||||
frappe.throw(
|
||||
_("Supplier Invoice No exists in Purchase Invoice {0}").format(
|
||||
get_link_to_form("Purchase Invoice", pi)
|
||||
)
|
||||
)
|
||||
frappe.throw(_("Supplier Invoice No exists in Purchase Invoice {0}").format(pi))
|
||||
|
||||
def update_billing_status_in_pr(self, update_modified=True):
|
||||
if self.is_return and not self.update_billed_amount_in_purchase_receipt:
|
||||
|
||||
@@ -1742,30 +1742,6 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||
|
||||
# Cost of Item is zero in Purchase Receipt
|
||||
pr = make_purchase_receipt(qty=1, rate=0)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 0)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.rate = 150
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 150)
|
||||
|
||||
# Increase the cost of the item
|
||||
|
||||
pr = make_purchase_receipt(qty=1, rate=100)
|
||||
@@ -1845,52 +1821,6 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
def test_adjust_incoming_rate_for_rejected_item(self):
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||
|
||||
# Cost of Item is zero in Purchase Receipt
|
||||
pr = make_purchase_receipt(qty=1, rejected_qty=1, rate=0)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 0)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.qty = 1
|
||||
row.rate = 150
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "warehouse": pi.items[0].warehouse},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 150)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"voucher_no": pr.name,
|
||||
"warehouse": pi.items[0].rejected_warehouse,
|
||||
},
|
||||
"stock_value_difference",
|
||||
)
|
||||
|
||||
self.assertFalse(stock_value_difference)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
def test_item_less_defaults(self):
|
||||
pi = frappe.new_doc("Purchase Invoice")
|
||||
pi.supplier = "_Test Supplier"
|
||||
@@ -2511,34 +2441,6 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 0)
|
||||
|
||||
def test_invoice_against_returned_pr(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as make_purchase_invoice_from_pr,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_return_against_rejected_warehouse,
|
||||
)
|
||||
|
||||
item = make_item("_Test Item For Invoice Against Returned PR", properties={"is_stock_item": 1}).name
|
||||
|
||||
original_value = frappe.db.get_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||
)
|
||||
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0)
|
||||
|
||||
pr = make_purchase_receipt(item_code=item, qty=5, rejected_qty=5, rate=100)
|
||||
pr_return = make_purchase_return_against_rejected_warehouse(pr.name)
|
||||
pr_return.submit()
|
||||
|
||||
pi = make_purchase_invoice_from_pr(pr.name)
|
||||
pi.save()
|
||||
self.assertEqual(pi.items[0].qty, 5.0)
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", original_value
|
||||
)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
"advance_amount",
|
||||
"allocated_amount",
|
||||
"exchange_gain_loss",
|
||||
"ref_exchange_rate",
|
||||
"difference_posting_date"
|
||||
"ref_exchange_rate"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -31,7 +30,7 @@
|
||||
"width": "180px"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"columns": 3,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
@@ -41,7 +40,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"columns": 3,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
@@ -112,20 +111,13 @@
|
||||
"label": "Reference Exchange Rate",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "difference_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Difference Posting Date"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-20 12:04:46.729972",
|
||||
"modified": "2024-03-27 13:10:24.072896",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Advance",
|
||||
|
||||
@@ -16,7 +16,6 @@ class PurchaseInvoiceAdvance(Document):
|
||||
|
||||
advance_amount: DF.Currency
|
||||
allocated_amount: DF.Currency
|
||||
difference_posting_date: DF.Date | None
|
||||
exchange_gain_loss: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"account_head",
|
||||
"description",
|
||||
"is_tax_withholding_account",
|
||||
"set_by_item_tax_template",
|
||||
"section_break_10",
|
||||
"rate",
|
||||
"accounting_dimensions_section",
|
||||
@@ -245,8 +244,7 @@
|
||||
"label": "Net Amount",
|
||||
"oldfieldname": "tax_amount",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "base_net_amount",
|
||||
@@ -255,22 +253,12 @@
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "set_by_item_tax_template",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Set by Item Tax Template",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-22 19:17:02.377473",
|
||||
"modified": "2024-09-24 06:47:25.129901",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Taxes and Charges",
|
||||
|
||||
@@ -42,7 +42,6 @@ class PurchaseTaxesandCharges(Document):
|
||||
parenttype: DF.Data
|
||||
rate: DF.Float
|
||||
row_id: DF.Data | None
|
||||
set_by_item_tax_template: DF.Check
|
||||
tax_amount: DF.Currency
|
||||
tax_amount_after_discount_amount: DF.Currency
|
||||
total: DF.Currency
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import inspect
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
@@ -47,7 +45,7 @@ class RepostAccountingLedger(Document):
|
||||
latest_pcv = (
|
||||
frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"company": self.company, "docstatus": 1},
|
||||
filters={"company": self.company},
|
||||
order_by="period_end_date desc",
|
||||
pluck="period_end_date",
|
||||
limit=1,
|
||||
@@ -144,8 +142,6 @@ class RepostAccountingLedger(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def start_repost(account_repost_doc=str) -> None:
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
|
||||
frappe.flags.through_repost_accounting_ledger = True
|
||||
if account_repost_doc:
|
||||
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
|
||||
@@ -181,14 +177,6 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.make_gl_entries(1)
|
||||
doc.make_gl_entries()
|
||||
elif doc.doctype in frappe.get_hooks("repost_allowed_doctypes"):
|
||||
if hasattr(doc, "make_gl_entries") and callable(doc.make_gl_entries):
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
if "cancel" in inspect.getfullargspec(doc.make_gl_entries):
|
||||
doc.make_gl_entries(cancel=1)
|
||||
else:
|
||||
make_reverse_gl_entries(voucher_type=doc.doctype, voucher_no=doc.name)
|
||||
doc.make_gl_entries()
|
||||
|
||||
|
||||
def get_allowed_types_from_settings():
|
||||
|
||||
@@ -740,6 +740,20 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("company_address", function (doc) {
|
||||
if (!doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
|
||||
return {
|
||||
query: "frappe.contacts.doctype.address.address.address_query",
|
||||
filters: {
|
||||
link_doctype: "Company",
|
||||
link_name: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("pos_profile", function (doc) {
|
||||
if (!doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
|
||||
@@ -161,9 +161,8 @@
|
||||
"dispatch_address",
|
||||
"company_address_section",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"company_addr_col_break",
|
||||
"company_contact_person",
|
||||
"company_address_display",
|
||||
"terms_tab",
|
||||
"payment_schedule_section",
|
||||
"ignore_default_payment_terms_template",
|
||||
@@ -2204,13 +2203,6 @@
|
||||
"oldfieldtype": "Select",
|
||||
"options": "UTM Source",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -2223,7 +2215,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-11-26 12:34:09.110690",
|
||||
"modified": "2024-07-18 15:30:39.428519",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
@@ -2278,4 +2270,4 @@
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user