mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-15 04:45:09 +00:00
Merge branch 'develop' into unit-price-contract-2
This commit is contained in:
2
.github/helper/install.sh
vendored
2
.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 libmariadb-dev
|
||||
|
||||
pip install frappe-bench
|
||||
|
||||
|
||||
1
.github/helper/site_config_mariadb.json
vendored
1
.github/helper/site_config_mariadb.json
vendored
@@ -8,6 +8,7 @@
|
||||
"mail_login": "test@example.com",
|
||||
"mail_password": "test",
|
||||
"admin_password": "admin",
|
||||
"use_mysqlclient": 1,
|
||||
"root_login": "root",
|
||||
"root_password": "root",
|
||||
"host_name": "http://test_site:8000",
|
||||
|
||||
4
.github/release.yml
vendored
Normal file
4
.github/release.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- skip-release-notes
|
||||
30
.github/workflows/label-base-on-title.yml
vendored
Normal file
30
.github/workflows/label-base-on-title.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: "Auto-label PRs based on title"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
add-label-if-prefix-matches:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR title and add label if it matches prefixes
|
||||
uses: actions/github-script@v7
|
||||
continue-on-error: true
|
||||
with:
|
||||
script: |
|
||||
const title = context.payload.pull_request.title.toLowerCase();
|
||||
const prefixes = ['chore', 'ci', 'style', 'test', 'refactor'];
|
||||
|
||||
// Check if the PR title starts with any of the prefixes
|
||||
if (prefixes.some(prefix => title.startsWith(prefix))) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
labels: ['skip-release-notes']
|
||||
});
|
||||
}
|
||||
23
README.md
23
README.md
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<a href="https://erpnext.com">
|
||||
<a href="https://frappe.io/erpnext">
|
||||
<img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80xp"/>
|
||||
</a>
|
||||
<h2>ERPNext</h2>
|
||||
@@ -17,11 +17,11 @@
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://erpnext-demo.frappe.cloud/app/home">Live Demo</a>
|
||||
<a href="https://erpnext-demo.frappe.cloud/api/method/erpnext_demo.erpnext_demo.auth.login_demo">Live Demo</a>
|
||||
-
|
||||
<a href="https://erpnext.com">Website</a>
|
||||
<a href="https://frappe.io/erpnext">Website</a>
|
||||
-
|
||||
<a href="https://docs.erpnext.com">Documentation</a>
|
||||
<a href="https://docs.frappe.io/erpnext/">Documentation</a>
|
||||
</div>
|
||||
|
||||
## ERPNext
|
||||
@@ -114,26 +114,23 @@ To setup the repository locally follow the steps mentioned below:
|
||||
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
|
||||
bench new-site erpnext.localhost
|
||||
```
|
||||
|
||||
|
||||
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
|
||||
bench --site erpnext.localhost install-app erpnext
|
||||
```
|
||||
|
||||
4. Open the URL `http://erpnext.dev:8000/app` in your browser, you should see the app running
|
||||
4. Open the URL `http://erpnext.localhost: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.
|
||||
1. [Frappe School](https://school.frappe.io) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
|
||||
2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
|
||||
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
|
||||
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.
|
||||
|
||||
@@ -4,7 +4,11 @@ files:
|
||||
pull_request_title: "fix: sync translations from crowdin"
|
||||
pull_request_labels:
|
||||
- translation
|
||||
- skip-release-notes
|
||||
pull_request_reviewers:
|
||||
- barredterra # change to your GitHub username if you copied this file
|
||||
commit_message: "fix: %language% translations"
|
||||
append_commit_message: false
|
||||
languages_mapping:
|
||||
two_letters_code:
|
||||
pt-BR: pt_BR
|
||||
|
||||
@@ -500,7 +500,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
"name",
|
||||
)
|
||||
|
||||
if old_name:
|
||||
if old_name and not from_descendant:
|
||||
# same account in parent company exists
|
||||
allow_child_account_creation = _("Allow Account Creation Against Child Company")
|
||||
|
||||
@@ -605,6 +605,7 @@ def _ensure_idle_system():
|
||||
if frappe.flags.in_test:
|
||||
return
|
||||
|
||||
last_gl_update = None
|
||||
try:
|
||||
# We also lock inserts to GL entry table with for_update here.
|
||||
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
|
||||
@@ -612,6 +613,9 @@ def _ensure_idle_system():
|
||||
# wait=False fails immediately if there's an active transaction.
|
||||
last_gl_update = add_to_date(None, seconds=-1)
|
||||
|
||||
if not last_gl_update:
|
||||
return
|
||||
|
||||
if last_gl_update > add_to_date(None, minutes=-5):
|
||||
frappe.throw(
|
||||
_(
|
||||
|
||||
@@ -116,6 +116,7 @@ def identify_is_group(child):
|
||||
return is_group
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_chart(chart_template, existing_company=None):
|
||||
chart = {}
|
||||
if existing_company:
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"Office Maintenance Expenses": {},
|
||||
"Office Rent": {},
|
||||
"Postal Expenses": {},
|
||||
"Print and Stationary": {},
|
||||
"Print and Stationery": {},
|
||||
"Rounded Off": {
|
||||
"account_type": "Round Off"
|
||||
},
|
||||
|
||||
6
erpnext/accounts/doctype/account/test_records.json
Normal file
6
erpnext/accounts/doctype/account/test_records.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"doctype": "Account",
|
||||
"name": "_Test Account 1"
|
||||
}
|
||||
]
|
||||
@@ -1,3 +0,0 @@
|
||||
[[Account]]
|
||||
name = "_Test Account 1"
|
||||
|
||||
@@ -160,9 +160,6 @@ def get_payment_entries_for_bank_clearance(
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if bank_account:
|
||||
condition += "and bank_account = %(bank_account)s"
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
@@ -184,7 +181,6 @@ def get_payment_entries_for_bank_clearance(
|
||||
"account": account,
|
||||
"from": from_date,
|
||||
"to": to_date,
|
||||
"bank_account": bank_account,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
@@ -373,10 +374,37 @@ def auto_reconcile_vouchers(
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
):
|
||||
frappe.flags.auto_reconcile_vouchers = True
|
||||
reconciled, partially_reconciled = set(), set()
|
||||
|
||||
bank_transactions = get_bank_transactions(bank_account)
|
||||
|
||||
if len(bank_transactions) > 10:
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.start_auto_reconcile",
|
||||
queue="long",
|
||||
bank_transactions=bank_transactions,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
filter_by_reference_date=filter_by_reference_date,
|
||||
from_reference_date=from_reference_date,
|
||||
to_reference_date=to_reference_date,
|
||||
)
|
||||
frappe.msgprint(_("Auto Reconciliation has started in the background"))
|
||||
else:
|
||||
start_auto_reconcile(
|
||||
bank_transactions,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
)
|
||||
|
||||
|
||||
def start_auto_reconcile(
|
||||
bank_transactions, from_date, to_date, filter_by_reference_date, from_reference_date, to_reference_date
|
||||
):
|
||||
frappe.flags.auto_reconcile_vouchers = True
|
||||
|
||||
reconciled, partially_reconciled = set(), set()
|
||||
for transaction in bank_transactions:
|
||||
linked_payments = get_linked_payments(
|
||||
transaction.name,
|
||||
@@ -414,7 +442,6 @@ def auto_reconcile_vouchers(
|
||||
frappe.msgprint(title=_("Auto Reconciliation"), msg=alert_message, indicator=indicator)
|
||||
|
||||
frappe.flags.auto_reconcile_vouchers = False
|
||||
return reconciled, partially_reconciled
|
||||
|
||||
|
||||
def get_auto_reconcile_message(partially_reconciled, reconciled):
|
||||
@@ -491,16 +518,23 @@ def subtract_allocations(gl_account, 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 []
|
||||
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"]:
|
||||
if amount := get_allocated_amount(voucher_allocated_amounts, voucher, gl_account):
|
||||
voucher["paid_amount"] -= amount
|
||||
|
||||
copied.append(voucher)
|
||||
return copied
|
||||
|
||||
|
||||
def get_allocated_amount(voucher_allocated_amounts, voucher, gl_account):
|
||||
if not (voucher_details := voucher_allocated_amounts.get((voucher.get("doctype"), voucher.get("name")))):
|
||||
return
|
||||
|
||||
if not (row := voucher_details.get(gl_account)):
|
||||
return
|
||||
|
||||
return row.get("total")
|
||||
|
||||
|
||||
def check_matching(
|
||||
bank_account,
|
||||
company,
|
||||
@@ -770,26 +804,20 @@ def get_je_matching_query(
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
|
||||
ref_condition = je.cheque_no == transaction.reference_number
|
||||
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
|
||||
|
||||
amount_field = f"{cr_or_dr}_in_account_currency"
|
||||
amount_equality = getattr(jea, amount_field) == transaction.unallocated_amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
|
||||
filter_by_date = je.posting_date.between(from_date, to_date)
|
||||
if cint(filter_by_reference_date):
|
||||
filter_by_date = je.cheque_date.between(from_reference_date, to_reference_date)
|
||||
|
||||
query = (
|
||||
subquery = (
|
||||
frappe.qb.from_(jea)
|
||||
.join(je)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
(ref_rank + amount_rank + 1).as_("rank"),
|
||||
Sum(getattr(jea, amount_field)).as_("paid_amount"),
|
||||
ConstantColumn("Journal Entry").as_("doctype"),
|
||||
je.name,
|
||||
getattr(jea, amount_field).as_("paid_amount"),
|
||||
je.cheque_no.as_("reference_no"),
|
||||
je.cheque_date.as_("reference_date"),
|
||||
je.pay_to_recd_from.as_("party"),
|
||||
@@ -801,13 +829,26 @@ def get_je_matching_query(
|
||||
.where(je.voucher_type != "Opening Entry")
|
||||
.where(je.clearance_date.isnull())
|
||||
.where(jea.account == common_filters.bank_account)
|
||||
.where(amount_equality if exact_match else getattr(jea, amount_field) > 0.0)
|
||||
.where(filter_by_date)
|
||||
.groupby(je.name)
|
||||
.orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
|
||||
)
|
||||
|
||||
if frappe.flags.auto_reconcile_vouchers is True:
|
||||
query = query.where(ref_condition)
|
||||
subquery = subquery.where(je.cheque_no == transaction.reference_number)
|
||||
|
||||
ref_rank = frappe.qb.terms.Case().when(subquery.reference_no == transaction.reference_number, 1).else_(0)
|
||||
amount_equality = subquery.paid_amount == transaction.unallocated_amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(subquery)
|
||||
.select(
|
||||
"*",
|
||||
(ref_rank + amount_rank + 1).as_("rank"),
|
||||
)
|
||||
.where(amount_equality if exact_match else subquery.paid_amount > 0.0)
|
||||
)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
@@ -2,27 +2,6 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Bank Transaction", {
|
||||
onload(frm) {
|
||||
frm.set_query("payment_document", "payment_entries", function () {
|
||||
const payment_doctypes = frm.events.get_payment_doctypes(frm);
|
||||
return {
|
||||
filters: {
|
||||
name: ["in", payment_doctypes],
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
refresh(frm) {
|
||||
if (!frm.is_dirty() && frm.doc.payment_entries.length > 0) {
|
||||
frm.add_custom_button(__("Unreconcile Transaction"), () => {
|
||||
frm.call("remove_payment_entries").then(() => frm.refresh());
|
||||
});
|
||||
}
|
||||
},
|
||||
bank_account: function (frm) {
|
||||
set_bank_statement_filter(frm);
|
||||
},
|
||||
|
||||
setup: function (frm) {
|
||||
frm.set_query("party_type", function () {
|
||||
return {
|
||||
@@ -31,6 +10,41 @@ frappe.ui.form.on("Bank Transaction", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("bank_account", function () {
|
||||
return {
|
||||
filters: { is_company_account: 1 },
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("payment_document", "payment_entries", function () {
|
||||
const payment_doctypes = frm.events.get_payment_doctypes(frm);
|
||||
return {
|
||||
filters: {
|
||||
name: ["in", payment_doctypes],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("payment_entry", "payment_entries", function () {
|
||||
return {
|
||||
filters: {
|
||||
docstatus: ["!=", 2],
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh(frm) {
|
||||
if (!frm.is_dirty() && frm.doc.payment_entries.length > 0) {
|
||||
frm.add_custom_button(__("Unreconcile Transaction"), () => {
|
||||
frm.call("remove_payment_entries").then(() => frm.refresh());
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
bank_account: function (frm) {
|
||||
set_bank_statement_filter(frm);
|
||||
},
|
||||
|
||||
get_payment_doctypes: function () {
|
||||
@@ -39,31 +53,6 @@ frappe.ui.form.on("Bank Transaction", {
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Bank Transaction Payments", {
|
||||
payment_entries_remove: function (frm, cdt, cdn) {
|
||||
update_clearance_date(frm, cdt, cdn);
|
||||
},
|
||||
});
|
||||
|
||||
const update_clearance_date = (frm, cdt, cdn) => {
|
||||
if (frm.doc.docstatus === 1) {
|
||||
frappe
|
||||
.xcall("erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment", {
|
||||
doctype: cdt,
|
||||
docname: cdn,
|
||||
bt_name: frm.doc.name,
|
||||
})
|
||||
.then((e) => {
|
||||
if (e == "success") {
|
||||
frappe.show_alert({
|
||||
message: __("Document {0} successfully uncleared", [e]),
|
||||
indicator: "green",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function set_bank_statement_filter(frm) {
|
||||
frm.set_query("bank_statement", function () {
|
||||
return {
|
||||
|
||||
@@ -5,7 +5,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt
|
||||
from frappe.utils import flt, getdate
|
||||
|
||||
|
||||
class BankTransaction(Document):
|
||||
@@ -84,16 +84,16 @@ class BankTransaction(Document):
|
||||
if not self.payment_entries:
|
||||
return
|
||||
|
||||
pe = []
|
||||
references = set()
|
||||
for row in self.payment_entries:
|
||||
reference = (row.payment_document, row.payment_entry)
|
||||
if reference in pe:
|
||||
if reference in references:
|
||||
frappe.throw(
|
||||
_("{0} {1} is allocated twice in this Bank Transaction").format(
|
||||
row.payment_document, row.payment_entry
|
||||
)
|
||||
)
|
||||
pe.append(reference)
|
||||
references.add(reference)
|
||||
|
||||
def update_allocated_amount(self):
|
||||
allocated_amount = (
|
||||
@@ -104,6 +104,19 @@ class BankTransaction(Document):
|
||||
self.allocated_amount = flt(allocated_amount, self.precision("allocated_amount"))
|
||||
self.unallocated_amount = flt(unallocated_amount, self.precision("unallocated_amount"))
|
||||
|
||||
def delink_old_payment_entries(self):
|
||||
if self.flags.updating_linked_bank_transaction:
|
||||
return
|
||||
|
||||
old_doc = self.get_doc_before_save()
|
||||
payment_entry_names = set(pe.name for pe in self.payment_entries)
|
||||
|
||||
for old_pe in old_doc.payment_entries:
|
||||
if old_pe.name in payment_entry_names:
|
||||
continue
|
||||
|
||||
self.delink_payment_entry(old_pe)
|
||||
|
||||
def before_submit(self):
|
||||
self.allocate_payment_entries()
|
||||
self.set_status()
|
||||
@@ -113,13 +126,14 @@ class BankTransaction(Document):
|
||||
|
||||
def before_update_after_submit(self):
|
||||
self.validate_duplicate_references()
|
||||
self.allocate_payment_entries()
|
||||
self.update_allocated_amount()
|
||||
self.delink_old_payment_entries()
|
||||
self.allocate_payment_entries()
|
||||
self.set_status()
|
||||
|
||||
def on_cancel(self):
|
||||
for payment_entry in self.payment_entries:
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
||||
self.delink_payment_entry(payment_entry)
|
||||
|
||||
self.set_status()
|
||||
|
||||
@@ -152,43 +166,55 @@ class BankTransaction(Document):
|
||||
- 0 > a: Error: already over-allocated
|
||||
- clear means: set the latest transaction date as clearance date
|
||||
"""
|
||||
if self.flags.updating_linked_bank_transaction or not self.payment_entries:
|
||||
return
|
||||
|
||||
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)
|
||||
gl_entries = get_related_bank_gl_entries(payment_entry_docs)
|
||||
gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
|
||||
|
||||
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 [],
|
||||
for payment_entry in list(self.payment_entries):
|
||||
if payment_entry.allocated_amount != 0:
|
||||
continue
|
||||
|
||||
allocable_amount, should_clear, clearance_date = get_clearance_details(
|
||||
self,
|
||||
payment_entry,
|
||||
pe_bt_allocations.get((payment_entry.payment_document, payment_entry.payment_entry)) or {},
|
||||
gl_entries.get((payment_entry.payment_document, payment_entry.payment_entry)) or {},
|
||||
gl_bank_account,
|
||||
)
|
||||
|
||||
if allocable_amount < 0:
|
||||
frappe.throw(_("Voucher {0} is over-allocated by {1}").format(allocable_amount))
|
||||
|
||||
if remaining_amount <= 0:
|
||||
self.remove(payment_entry)
|
||||
continue
|
||||
|
||||
if allocable_amount == 0:
|
||||
if should_clear:
|
||||
self.clear_linked_payment_entry(payment_entry, clearance_date=clearance_date)
|
||||
self.remove(payment_entry)
|
||||
continue
|
||||
|
||||
should_clear = should_clear and allocable_amount <= remaining_amount
|
||||
payment_entry.allocated_amount = min(allocable_amount, remaining_amount)
|
||||
remaining_amount = flt(
|
||||
remaining_amount - payment_entry.allocated_amount,
|
||||
self.precision("unallocated_amount"),
|
||||
)
|
||||
|
||||
if payment_entry.payment_document == "Bank Transaction":
|
||||
self.update_linked_bank_transaction(
|
||||
payment_entry.payment_entry, payment_entry.allocated_amount
|
||||
)
|
||||
elif should_clear:
|
||||
self.clear_linked_payment_entry(payment_entry, clearance_date=clearance_date)
|
||||
|
||||
if 0.0 == unallocated_amount:
|
||||
if should_clear:
|
||||
latest_transaction.clear_linked_payment_entry(payment_entry)
|
||||
to_remove.append(payment_entry)
|
||||
|
||||
elif remaining_amount <= 0.0:
|
||||
to_remove.append(payment_entry)
|
||||
|
||||
elif 0.0 < unallocated_amount <= remaining_amount:
|
||||
payment_entry.allocated_amount = unallocated_amount
|
||||
remaining_amount -= unallocated_amount
|
||||
if should_clear:
|
||||
latest_transaction.clear_linked_payment_entry(payment_entry)
|
||||
|
||||
elif 0.0 < unallocated_amount:
|
||||
payment_entry.allocated_amount = remaining_amount
|
||||
remaining_amount = 0.0
|
||||
|
||||
elif 0.0 > unallocated_amount:
|
||||
frappe.throw(_("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
|
||||
|
||||
for payment_entry in to_remove:
|
||||
self.remove(payment_entry)
|
||||
self.update_allocated_amount()
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove_payment_entries(self):
|
||||
@@ -199,14 +225,64 @@ class BankTransaction(Document):
|
||||
|
||||
def remove_payment_entry(self, payment_entry):
|
||||
"Clear payment entry and clearance"
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
||||
self.delink_payment_entry(payment_entry)
|
||||
self.remove(payment_entry)
|
||||
|
||||
def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
|
||||
clearance_date = None if for_cancel else self.date
|
||||
set_voucher_clearance(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self
|
||||
)
|
||||
def delink_payment_entry(self, payment_entry):
|
||||
if payment_entry.payment_document == "Bank Transaction":
|
||||
self.update_linked_bank_transaction(payment_entry.payment_entry, allocated_amount=None)
|
||||
else:
|
||||
self.clear_linked_payment_entry(payment_entry, clearance_date=None)
|
||||
|
||||
def clear_linked_payment_entry(self, payment_entry, clearance_date=None):
|
||||
doctype = payment_entry.payment_document
|
||||
docname = payment_entry.payment_entry
|
||||
|
||||
# might be a bank transaction
|
||||
if doctype not in get_doctypes_for_bank_reconciliation():
|
||||
return
|
||||
|
||||
if doctype == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
dict(parenttype=doctype, parent=docname),
|
||||
"clearance_date",
|
||||
clearance_date,
|
||||
)
|
||||
return
|
||||
|
||||
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
|
||||
|
||||
def update_linked_bank_transaction(self, bank_transaction_name, allocated_amount=None):
|
||||
"""For when a second bank transaction has fixed another, e.g. refund"""
|
||||
|
||||
bt = frappe.get_doc(self.doctype, bank_transaction_name)
|
||||
if allocated_amount:
|
||||
bt.append(
|
||||
"payment_entries",
|
||||
{
|
||||
"payment_document": self.doctype,
|
||||
"payment_entry": self.name,
|
||||
"allocated_amount": allocated_amount,
|
||||
},
|
||||
)
|
||||
|
||||
else:
|
||||
pe = next(
|
||||
(
|
||||
pe
|
||||
for pe in bt.payment_entries
|
||||
if pe.payment_document == self.doctype and pe.payment_entry == self.name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not pe:
|
||||
return
|
||||
|
||||
bt.flags.updating_linked_bank_transaction = True
|
||||
bt.remove(pe)
|
||||
|
||||
bt.save()
|
||||
|
||||
def auto_set_party(self):
|
||||
from erpnext.accounts.doctype.bank_transaction.auto_match_party import AutoMatchParty
|
||||
@@ -238,71 +314,107 @@ 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, bt_allocations, gl_entries, gl_bank_account):
|
||||
"""
|
||||
There should only be one bank gle for a voucher.
|
||||
Could be none for a Bank Transaction.
|
||||
But if a JE, could affect two banks.
|
||||
Should only clear the voucher if all bank gles are allocated.
|
||||
There should only be one bank gl entry for a voucher, except for JE.
|
||||
For JE, there can be multiple bank gl entries for the same account.
|
||||
In this case, the allocable_amount will be the sum of amounts of all gl entries of the account.
|
||||
There will be no gl entry for a Bank Transaction so return the unallocated amount.
|
||||
Should only clear the voucher if all bank gl entries are allocated.
|
||||
"""
|
||||
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)
|
||||
|
||||
unallocated_amount = min(
|
||||
transaction.unallocated_amount,
|
||||
get_paid_amount(payment_entry, transaction.currency, gl_bank_account),
|
||||
)
|
||||
unmatched_gles = len(gles)
|
||||
latest_transaction = transaction
|
||||
for gle in gles:
|
||||
if gle["gl_account"] == gl_bank_account:
|
||||
if gle["amount"] <= 0.0:
|
||||
frappe.throw(
|
||||
_("Voucher {0} value is broken: {1}").format(payment_entry.payment_entry, gle["amount"])
|
||||
transaction_date = getdate(transaction.date)
|
||||
|
||||
if payment_entry.payment_document == "Bank Transaction":
|
||||
bt = frappe.db.get_value(
|
||||
"Bank Transaction",
|
||||
payment_entry.payment_entry,
|
||||
("unallocated_amount", "bank_account"),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if bt.bank_account != gl_bank_account:
|
||||
frappe.throw(
|
||||
_("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format(
|
||||
bt.bank_account, payment_entry.payment_entry, gl_bank_account
|
||||
)
|
||||
)
|
||||
|
||||
unmatched_gles -= 1
|
||||
unallocated_amount = gle["amount"]
|
||||
for a in bt_allocations:
|
||||
if a["gl_account"] == gle["gl_account"]:
|
||||
unallocated_amount = gle["amount"] - a["total"]
|
||||
if frappe.utils.getdate(transaction.date) < a["latest_date"]:
|
||||
latest_transaction = frappe.get_doc("Bank Transaction", a["latest_name"])
|
||||
else:
|
||||
# Must be a Journal Entry affecting more than one bank
|
||||
for a in bt_allocations:
|
||||
if a["gl_account"] == gle["gl_account"] and a["total"] == gle["amount"]:
|
||||
unmatched_gles -= 1
|
||||
return abs(bt.unallocated_amount), True, transaction_date
|
||||
|
||||
return unallocated_amount, unmatched_gles == 0, latest_transaction
|
||||
if gl_bank_account not in gl_entries:
|
||||
frappe.throw(
|
||||
_("{} {} is not affecting bank account {}").format(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, gl_bank_account
|
||||
)
|
||||
)
|
||||
|
||||
allocable_amount = gl_entries.pop(gl_bank_account) or 0
|
||||
if allocable_amount <= 0.0:
|
||||
frappe.throw(
|
||||
_("Invalid amount in accounting entries of {} {} for Account {}: {}").format(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, gl_bank_account, allocable_amount
|
||||
)
|
||||
)
|
||||
|
||||
matching_bt_allocaion = bt_allocations.pop(gl_bank_account, {})
|
||||
|
||||
allocable_amount = flt(
|
||||
allocable_amount - matching_bt_allocaion.get("total", 0), transaction.precision("unallocated_amount")
|
||||
)
|
||||
|
||||
should_clear = all(
|
||||
gl_entries[gle_account] == bt_allocations.get(gle_account, {}).get("total", 0)
|
||||
for gle_account in gl_entries
|
||||
)
|
||||
|
||||
bt_allocation_date = matching_bt_allocaion.get("latest_date", None)
|
||||
clearance_date = transaction_date if not bt_allocation_date else max(transaction_date, bt_allocation_date)
|
||||
|
||||
return allocable_amount, should_clear, clearance_date
|
||||
|
||||
|
||||
def get_related_bank_gl_entries(doctype, docname):
|
||||
def get_related_bank_gl_entries(docs):
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||
return frappe.db.sql(
|
||||
if not docs:
|
||||
return {}
|
||||
|
||||
result = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
|
||||
gle.account AS gl_account
|
||||
FROM
|
||||
`tabGL Entry` gle
|
||||
LEFT JOIN
|
||||
`tabAccount` ac ON ac.name=gle.account
|
||||
WHERE
|
||||
ac.account_type = 'Bank'
|
||||
AND gle.voucher_type = %(doctype)s
|
||||
AND gle.voucher_no = %(docname)s
|
||||
AND is_cancelled = 0
|
||||
""",
|
||||
dict(doctype=doctype, docname=docname),
|
||||
SELECT
|
||||
gle.voucher_type AS doctype,
|
||||
gle.voucher_no AS docname,
|
||||
gle.account AS gl_account,
|
||||
SUM(ABS(gle.credit_in_account_currency - gle.debit_in_account_currency)) AS amount
|
||||
FROM
|
||||
`tabGL Entry` gle
|
||||
LEFT JOIN
|
||||
`tabAccount` ac ON ac.name = gle.account
|
||||
WHERE
|
||||
ac.account_type = 'Bank'
|
||||
AND (gle.voucher_type, gle.voucher_no) IN %(docs)s
|
||||
AND gle.is_cancelled = 0
|
||||
GROUP BY
|
||||
gle.voucher_type, gle.voucher_no, gle.account
|
||||
""",
|
||||
{"docs": docs},
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
entries = {}
|
||||
for row in result:
|
||||
key = (row["doctype"], row["docname"])
|
||||
if key not in entries:
|
||||
entries[key] = {}
|
||||
entries[key][row["gl_account"]] = row["amount"]
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def get_total_allocated_amount(docs):
|
||||
"""
|
||||
Gets the sum of allocations for a voucher on each bank GL account
|
||||
along with the latest bank transaction name & date
|
||||
along with the latest bank transaction date
|
||||
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
|
||||
"""
|
||||
if not docs:
|
||||
@@ -311,11 +423,10 @@ def get_total_allocated_amount(docs):
|
||||
# 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_date, gl_account, payment_document, payment_entry 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,
|
||||
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,
|
||||
@@ -338,104 +449,14 @@ def get_total_allocated_amount(docs):
|
||||
|
||||
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)
|
||||
row["latest_date"] = getdate(row["latest_date"])
|
||||
payment_allocation_details.setdefault((row["payment_document"], row["payment_entry"]), {})[
|
||||
row["gl_account"]
|
||||
] = row
|
||||
|
||||
return payment_allocation_details
|
||||
|
||||
|
||||
def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
|
||||
paid_amount_field = "paid_amount"
|
||||
if payment_entry.payment_document == "Payment Entry":
|
||||
doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry)
|
||||
|
||||
if doc.payment_type == "Receive":
|
||||
paid_amount_field = (
|
||||
"received_amount" if doc.paid_to_account_currency == currency else "base_received_amount"
|
||||
)
|
||||
elif doc.payment_type == "Pay":
|
||||
paid_amount_field = (
|
||||
"paid_amount" if doc.paid_from_account_currency == currency else "base_paid_amount"
|
||||
)
|
||||
|
||||
return frappe.db.get_value(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, paid_amount_field
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Journal Entry":
|
||||
return abs(
|
||||
frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
|
||||
"sum(debit_in_account_currency-credit_in_account_currency)",
|
||||
)
|
||||
or 0
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Expense Claim":
|
||||
return frappe.db.get_value(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed"
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Loan Disbursement":
|
||||
return frappe.db.get_value(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount"
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Loan Repayment":
|
||||
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "amount_paid")
|
||||
|
||||
elif payment_entry.payment_document == "Bank Transaction":
|
||||
dep, wth = frappe.db.get_value(
|
||||
"Bank Transaction", payment_entry.payment_entry, ("deposit", "withdrawal")
|
||||
)
|
||||
return abs(flt(wth) - flt(dep))
|
||||
|
||||
else:
|
||||
frappe.throw(
|
||||
f"Please reconcile {payment_entry.payment_document}: {payment_entry.payment_entry} manually"
|
||||
)
|
||||
|
||||
|
||||
def set_voucher_clearance(doctype, docname, clearance_date, self):
|
||||
if doctype in get_doctypes_for_bank_reconciliation():
|
||||
if (
|
||||
doctype == "Payment Entry"
|
||||
and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer"
|
||||
and len(get_reconciled_bank_transactions(doctype, docname)) < 2
|
||||
):
|
||||
return
|
||||
|
||||
if doctype == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
dict(parenttype=doctype, parent=docname),
|
||||
"clearance_date",
|
||||
clearance_date,
|
||||
)
|
||||
return
|
||||
|
||||
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
|
||||
|
||||
elif doctype == "Bank Transaction":
|
||||
# For when a second bank transaction has fixed another, e.g. refund
|
||||
bt = frappe.get_doc(doctype, docname)
|
||||
if clearance_date:
|
||||
vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}]
|
||||
bt.add_payment_entries(vouchers)
|
||||
bt.save()
|
||||
else:
|
||||
for pe in bt.payment_entries:
|
||||
if pe.payment_document == self.doctype and pe.payment_entry == self.name:
|
||||
bt.remove(pe)
|
||||
bt.save()
|
||||
break
|
||||
|
||||
|
||||
def get_reconciled_bank_transactions(doctype, docname):
|
||||
return frappe.get_all(
|
||||
"Bank Transaction Payments",
|
||||
@@ -444,13 +465,6 @@ def get_reconciled_bank_transactions(doctype, docname):
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def unclear_reference_payment(doctype, docname, bt_name):
|
||||
bt = frappe.get_doc("Bank Transaction", bt_name)
|
||||
set_voucher_clearance(doctype, docname, None, bt)
|
||||
return docname
|
||||
|
||||
|
||||
def remove_from_bank_transaction(doctype, docname):
|
||||
"""Remove a (cancelled) voucher from all Bank Transactions."""
|
||||
for bt_name in get_reconciled_bank_transactions(doctype, docname):
|
||||
|
||||
23
erpnext/accounts/doctype/cost_center/test_records.json
Normal file
23
erpnext/accounts/doctype/cost_center/test_records.json
Normal file
@@ -0,0 +1,23 @@
|
||||
[
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"cost_center_name": "_Test Cost Center",
|
||||
"doctype": "Cost Center",
|
||||
"is_group": 0,
|
||||
"parent_cost_center": "_Test Company - _TC"
|
||||
},
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"cost_center_name": "_Test Cost Center 2",
|
||||
"doctype": "Cost Center",
|
||||
"is_group": 0,
|
||||
"parent_cost_center": "_Test Company - _TC"
|
||||
},
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"cost_center_name": "_Test Write Off Cost Center",
|
||||
"doctype": "Cost Center",
|
||||
"is_group": 0,
|
||||
"parent_cost_center": "_Test Company - _TC"
|
||||
}
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
[["Cost Center"]]
|
||||
company = "_Test Company"
|
||||
cost_center_name = "_Test Cost Center"
|
||||
is_group = 0
|
||||
parent_cost_center = "_Test Company - _TC"
|
||||
|
||||
[["Cost Center"]]
|
||||
company = "_Test Company"
|
||||
cost_center_name = "_Test Cost Center 2"
|
||||
is_group = 0
|
||||
parent_cost_center = "_Test Company - _TC"
|
||||
|
||||
[["Cost Center"]]
|
||||
company = "_Test Company"
|
||||
cost_center_name = "_Test Write Off Cost Center"
|
||||
is_group = 0
|
||||
parent_cost_center = "_Test Company - _TC"
|
||||
|
||||
@@ -128,7 +128,7 @@ class TestCouponCode(IntegrationTestCase):
|
||||
item_code="_Test Tesla Car",
|
||||
rate=5000,
|
||||
qty=1,
|
||||
do_not_submit=True,
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
self.assertEqual(so.items[0].rate, 5000)
|
||||
|
||||
@@ -230,6 +230,7 @@ def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str |
|
||||
if not language:
|
||||
language = doc.get("language")
|
||||
|
||||
letter_text = None
|
||||
if language:
|
||||
letter_text = frappe.db.get_value(
|
||||
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
|
||||
|
||||
36
erpnext/accounts/doctype/dunning_type/test_records.json
Normal file
36
erpnext/accounts/doctype/dunning_type/test_records.json
Normal file
@@ -0,0 +1,36 @@
|
||||
[
|
||||
{
|
||||
"doctype": "Dunning Type",
|
||||
"dunning_type": "_Test First Notice",
|
||||
"company": "_Test Company",
|
||||
"is_default": 1,
|
||||
"dunning_fee": 0.0,
|
||||
"rate_of_interest": 0.0,
|
||||
"dunning_letter_text": [
|
||||
{
|
||||
"language": "en",
|
||||
"body_text": "We have still not received payment for our invoice",
|
||||
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
|
||||
}
|
||||
],
|
||||
"income_account": "Sales - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"doctype": "Dunning Type",
|
||||
"dunning_type": "_Test Second Notice",
|
||||
"company": "_Test Company",
|
||||
"is_default": 0,
|
||||
"dunning_fee": 10.0,
|
||||
"rate_of_interest": 10.0,
|
||||
"dunning_letter_text": [
|
||||
{
|
||||
"language": "en",
|
||||
"body_text": "We have still not received payment for our invoice",
|
||||
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
|
||||
}
|
||||
],
|
||||
"income_account": "Sales - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
[["Dunning Type"]]
|
||||
dunning_type = "_Test First Notice"
|
||||
company = "_Test Company"
|
||||
is_default = 1
|
||||
dunning_fee = 0.0
|
||||
rate_of_interest = 0.0
|
||||
income_account = "Sales - _TC"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
[["Dunning Type".dunning_letter_text]]
|
||||
language = "en"
|
||||
body_text = "We have still not received payment for our invoice"
|
||||
closing_text = "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
|
||||
|
||||
|
||||
[["Dunning Type"]]
|
||||
dunning_type = "_Test Second Notice"
|
||||
company = "_Test Company"
|
||||
is_default = 0
|
||||
dunning_fee = 10.0
|
||||
rate_of_interest = 10.0
|
||||
income_account = "Sales - _TC"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
[["Dunning Type".dunning_letter_text]]
|
||||
language = "en"
|
||||
body_text = "We have still not received payment for our invoice"
|
||||
closing_text = "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
|
||||
|
||||
|
||||
@@ -105,7 +105,8 @@
|
||||
"label": "Cost Center",
|
||||
"oldfieldname": "cost_center",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Cost Center"
|
||||
"options": "Cost Center",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "debit",
|
||||
@@ -279,7 +280,8 @@
|
||||
{
|
||||
"fieldname": "transaction_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Transaction Exchange Rate"
|
||||
"label": "Transaction Exchange Rate",
|
||||
"precision": "9"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_in_transaction_currency",
|
||||
@@ -357,7 +359,7 @@
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2024-08-22 13:03:39.997475",
|
||||
"modified": "2025-03-21 15:29:11.221890",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GL Entry",
|
||||
|
||||
@@ -7,7 +7,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.model.naming import set_name_from_naming_options
|
||||
from frappe.utils import flt, fmt_money
|
||||
from frappe.utils import flt, fmt_money, now
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -456,7 +456,7 @@ def rename_temporarily_named_docs(doctype):
|
||||
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
|
||||
newname = doc.name
|
||||
frappe.db.sql(
|
||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0 where name = %s",
|
||||
(newname, oldname),
|
||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
|
||||
(newname, now(), oldname),
|
||||
auto_commit=True,
|
||||
)
|
||||
|
||||
79
erpnext/accounts/doctype/item_tax_template/test_records.json
Normal file
79
erpnext/accounts/doctype/item_tax_template/test_records.json
Normal file
@@ -0,0 +1,79 @@
|
||||
[
|
||||
{
|
||||
"doctype": "Item Tax Template",
|
||||
"title": "_Test Account Excise Duty @ 10",
|
||||
"company": "_Test Company",
|
||||
"taxes": [
|
||||
{
|
||||
"doctype": "Item Tax Template Detail",
|
||||
"parentfield": "taxes",
|
||||
"tax_rate": 10,
|
||||
"tax_type": "_Test Account Excise Duty - _TC"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"doctype": "Item Tax Template",
|
||||
"title": "_Test Account Excise Duty @ 12",
|
||||
"company": "_Test Company",
|
||||
"taxes": [
|
||||
{
|
||||
"doctype": "Item Tax Template Detail",
|
||||
"parentfield": "taxes",
|
||||
"tax_rate": 12,
|
||||
"tax_type": "_Test Account Excise Duty - _TC"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"doctype": "Item Tax Template",
|
||||
"title": "_Test Account Excise Duty @ 15",
|
||||
"company": "_Test Company",
|
||||
"taxes": [
|
||||
{
|
||||
"doctype": "Item Tax Template Detail",
|
||||
"parentfield": "taxes",
|
||||
"tax_rate": 15,
|
||||
"tax_type": "_Test Account Excise Duty - _TC"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"doctype": "Item Tax Template",
|
||||
"title": "_Test Account Excise Duty @ 20",
|
||||
"company": "_Test Company",
|
||||
"taxes": [
|
||||
{
|
||||
"doctype": "Item Tax Template Detail",
|
||||
"parentfield": "taxes",
|
||||
"tax_rate": 20,
|
||||
"tax_type": "_Test Account Excise Duty - _TC"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"doctype": "Item Tax Template",
|
||||
"title": "_Test Item Tax Template 1",
|
||||
"company": "_Test Company",
|
||||
"taxes": [
|
||||
{
|
||||
"doctype": "Item Tax Template Detail",
|
||||
"parentfield": "taxes",
|
||||
"tax_rate": 5,
|
||||
"tax_type": "_Test Account Excise Duty - _TC"
|
||||
},
|
||||
{
|
||||
"doctype": "Item Tax Template Detail",
|
||||
"parentfield": "taxes",
|
||||
"tax_rate": 10,
|
||||
"tax_type": "_Test Account Education Cess - _TC"
|
||||
},
|
||||
{
|
||||
"doctype": "Item Tax Template Detail",
|
||||
"parentfield": "taxes",
|
||||
"tax_rate": 15,
|
||||
"tax_type": "_Test Account S&H Education Cess - _TC"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,62 +0,0 @@
|
||||
[["Item Tax Template"]]
|
||||
title = "_Test Account Excise Duty @ 10"
|
||||
company = "_Test Company"
|
||||
[["Item Tax Template".taxes]]
|
||||
doctype = "Item Tax Template Detail"
|
||||
parentfield = "taxes"
|
||||
tax_rate = 10
|
||||
tax_type = "_Test Account Excise Duty - _TC"
|
||||
|
||||
|
||||
[["Item Tax Template"]]
|
||||
title = "_Test Account Excise Duty @ 12"
|
||||
company = "_Test Company"
|
||||
[["Item Tax Template".taxes]]
|
||||
doctype = "Item Tax Template Detail"
|
||||
parentfield = "taxes"
|
||||
tax_rate = 12
|
||||
tax_type = "_Test Account Excise Duty - _TC"
|
||||
|
||||
|
||||
[["Item Tax Template"]]
|
||||
title = "_Test Account Excise Duty @ 15"
|
||||
company = "_Test Company"
|
||||
[["Item Tax Template".taxes]]
|
||||
doctype = "Item Tax Template Detail"
|
||||
parentfield = "taxes"
|
||||
tax_rate = 15
|
||||
tax_type = "_Test Account Excise Duty - _TC"
|
||||
|
||||
|
||||
[["Item Tax Template"]]
|
||||
title = "_Test Account Excise Duty @ 20"
|
||||
company = "_Test Company"
|
||||
[["Item Tax Template".taxes]]
|
||||
doctype = "Item Tax Template Detail"
|
||||
parentfield = "taxes"
|
||||
tax_rate = 20
|
||||
tax_type = "_Test Account Excise Duty - _TC"
|
||||
|
||||
|
||||
[["Item Tax Template"]]
|
||||
title = "_Test Item Tax Template 1"
|
||||
company = "_Test Company"
|
||||
[["Item Tax Template".taxes]]
|
||||
doctype = "Item Tax Template Detail"
|
||||
parentfield = "taxes"
|
||||
tax_rate = 5
|
||||
tax_type = "_Test Account Excise Duty - _TC"
|
||||
|
||||
[["Item Tax Template".taxes]]
|
||||
doctype = "Item Tax Template Detail"
|
||||
parentfield = "taxes"
|
||||
tax_rate = 10
|
||||
tax_type = "_Test Account Education Cess - _TC"
|
||||
|
||||
[["Item Tax Template".taxes]]
|
||||
doctype = "Item Tax Template Detail"
|
||||
parentfield = "taxes"
|
||||
tax_rate = 15
|
||||
tax_type = "_Test Account S&H Education Cess - _TC"
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@ class JournalEntry(AccountsController):
|
||||
self.validate_empty_accounts_table()
|
||||
self.validate_inter_company_accounts()
|
||||
self.validate_depr_entry_voucher_type()
|
||||
self.validate_company_in_accounting_dimension()
|
||||
self.validate_advance_accounts()
|
||||
|
||||
if self.docstatus == 0:
|
||||
@@ -578,8 +579,22 @@ class JournalEntry(AccountsController):
|
||||
if customers:
|
||||
from erpnext.selling.doctype.customer.customer import check_credit_limit
|
||||
|
||||
customer_details = frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"Customer Credit Limit",
|
||||
filters={
|
||||
"parent": ["in", customers],
|
||||
"parenttype": ["=", "Customer"],
|
||||
"company": ["=", self.company],
|
||||
},
|
||||
fields=["parent", "bypass_credit_limit_check"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
for customer in customers:
|
||||
check_credit_limit(customer, self.company)
|
||||
ignore_outstanding_sales_order = bool(customer_details.get(customer))
|
||||
check_credit_limit(customer, self.company, ignore_outstanding_sales_order)
|
||||
|
||||
def validate_cheque_info(self):
|
||||
if self.voucher_type in ["Bank Entry"]:
|
||||
@@ -827,14 +842,13 @@ class JournalEntry(AccountsController):
|
||||
"Debit Note",
|
||||
"Credit Note",
|
||||
]:
|
||||
invoice = frappe.db.get_value(
|
||||
reference_type, reference_name, ["docstatus", "outstanding_amount"], as_dict=1
|
||||
)
|
||||
invoice = frappe.get_doc(reference_type, reference_name)
|
||||
|
||||
if invoice.docstatus != 1:
|
||||
frappe.throw(_("{0} {1} is not submitted").format(reference_type, reference_name))
|
||||
|
||||
if total and flt(invoice.outstanding_amount) < total:
|
||||
precision = invoice.precision("outstanding_amount")
|
||||
if total and flt(invoice.outstanding_amount, precision) < flt(total, precision):
|
||||
frappe.throw(
|
||||
_("Payment against {0} {1} cannot be greater than Outstanding Amount {2}").format(
|
||||
reference_type, reference_name, invoice.outstanding_amount
|
||||
@@ -1062,14 +1076,15 @@ class JournalEntry(AccountsController):
|
||||
gl_map = []
|
||||
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
self.transaction_currency = company_currency
|
||||
self.transaction_exchange_rate = 1
|
||||
if self.multi_currency:
|
||||
for row in self.get("accounts"):
|
||||
if row.account_currency != company_currency:
|
||||
self.currency = row.account_currency
|
||||
self.conversion_rate = row.exchange_rate
|
||||
# Journal assumes the first foreign currency as transaction currency
|
||||
self.transaction_currency = row.account_currency
|
||||
self.transaction_exchange_rate = row.exchange_rate
|
||||
break
|
||||
else:
|
||||
self.currency = company_currency
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
|
||||
@@ -1094,6 +1109,18 @@ class JournalEntry(AccountsController):
|
||||
"credit_in_account_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
),
|
||||
"transaction_currency": self.transaction_currency,
|
||||
"transaction_exchange_rate": self.transaction_exchange_rate,
|
||||
"debit_in_transaction_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.debit, d.precision("debit")) / self.transaction_exchange_rate,
|
||||
"credit_in_transaction_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.credit, d.precision("credit")) / self.transaction_exchange_rate,
|
||||
"against_voucher_type": d.reference_type,
|
||||
"against_voucher": d.reference_name,
|
||||
"remarks": remarks,
|
||||
|
||||
@@ -583,7 +583,7 @@ class TestJournalEntry(IntegrationTestCase):
|
||||
order_by="account",
|
||||
)
|
||||
expected = [
|
||||
{"account": "_Test Bank - _TC", "transaction_exchange_rate": 1.0},
|
||||
{"account": "_Test Bank - _TC", "transaction_exchange_rate": 85.0},
|
||||
{"account": "_Test Receivable USD - _TC", "transaction_exchange_rate": 85.0},
|
||||
]
|
||||
self.assertEqual(expected, actual)
|
||||
@@ -599,13 +599,14 @@ def make_journal_entry(
|
||||
save=True,
|
||||
submit=False,
|
||||
project=None,
|
||||
company=None,
|
||||
):
|
||||
if not cost_center:
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.posting_date = posting_date or nowdate()
|
||||
jv.company = "_Test Company"
|
||||
jv.company = company or "_Test Company"
|
||||
jv.user_remark = "test"
|
||||
jv.multi_currency = 1
|
||||
jv.set(
|
||||
|
||||
94
erpnext/accounts/doctype/journal_entry/test_records.json
Normal file
94
erpnext/accounts/doctype/journal_entry/test_records.json
Normal file
@@ -0,0 +1,94 @@
|
||||
[
|
||||
{
|
||||
"cheque_date": "2013-03-14",
|
||||
"cheque_no": "33",
|
||||
"company": "_Test Company",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"credit_in_account_currency": 400.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"account": "_Test Bank - _TC",
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 400.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
],
|
||||
"naming_series": "_T-Journal Entry-",
|
||||
"posting_date": "2013-02-14",
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
},
|
||||
|
||||
{
|
||||
"cheque_date": "2013-02-14",
|
||||
"cheque_no": "33",
|
||||
"company": "_Test Company",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "_Test Payable - _TC",
|
||||
"party_type": "Supplier",
|
||||
"party": "_Test Supplier",
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 400.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"account": "_Test Bank - _TC",
|
||||
"credit_in_account_currency": 400.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
],
|
||||
"naming_series": "_T-Journal Entry-",
|
||||
"posting_date": "2013-02-14",
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
},
|
||||
|
||||
{
|
||||
"cheque_date": "2013-02-14",
|
||||
"cheque_no": "33",
|
||||
"company": "_Test Company",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 400.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"account": "Sales - _TC",
|
||||
"credit_in_account_currency": 400.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
],
|
||||
"naming_series": "_T-Journal Entry-",
|
||||
"posting_date": "2013-02-14",
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
}
|
||||
]
|
||||
@@ -1,81 +0,0 @@
|
||||
[["Journal Entry"]]
|
||||
cheque_date = "2013-03-14"
|
||||
cheque_no = "33"
|
||||
company = "_Test Company"
|
||||
naming_series = "_T-Journal Entry-"
|
||||
posting_date = "2013-02-14"
|
||||
user_remark = "test"
|
||||
voucher_type = "Bank Entry"
|
||||
[["Journal Entry".accounts]]
|
||||
account = "Debtors - _TC"
|
||||
party_type = "Customer"
|
||||
party = "_Test Customer"
|
||||
credit_in_account_currency = 400.0
|
||||
debit_in_account_currency = 0.0
|
||||
doctype = "Journal Entry Account"
|
||||
parentfield = "accounts"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
[["Journal Entry".accounts]]
|
||||
account = "_Test Bank - _TC"
|
||||
credit_in_account_currency = 0.0
|
||||
debit_in_account_currency = 400.0
|
||||
doctype = "Journal Entry Account"
|
||||
parentfield = "accounts"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
|
||||
[["Journal Entry"]]
|
||||
cheque_date = "2013-02-14"
|
||||
cheque_no = "33"
|
||||
company = "_Test Company"
|
||||
naming_series = "_T-Journal Entry-"
|
||||
posting_date = "2013-02-14"
|
||||
user_remark = "test"
|
||||
voucher_type = "Bank Entry"
|
||||
[["Journal Entry".accounts]]
|
||||
account = "_Test Payable - _TC"
|
||||
party_type = "Supplier"
|
||||
party = "_Test Supplier"
|
||||
credit_in_account_currency = 0.0
|
||||
debit_in_account_currency = 400.0
|
||||
doctype = "Journal Entry Account"
|
||||
parentfield = "accounts"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
[["Journal Entry".accounts]]
|
||||
account = "_Test Bank - _TC"
|
||||
credit_in_account_currency = 400.0
|
||||
debit_in_account_currency = 0.0
|
||||
doctype = "Journal Entry Account"
|
||||
parentfield = "accounts"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
|
||||
[["Journal Entry"]]
|
||||
cheque_date = "2013-02-14"
|
||||
cheque_no = "33"
|
||||
company = "_Test Company"
|
||||
naming_series = "_T-Journal Entry-"
|
||||
posting_date = "2013-02-14"
|
||||
user_remark = "test"
|
||||
voucher_type = "Bank Entry"
|
||||
[["Journal Entry".accounts]]
|
||||
account = "Debtors - _TC"
|
||||
party_type = "Customer"
|
||||
party = "_Test Customer"
|
||||
credit_in_account_currency = 0.0
|
||||
debit_in_account_currency = 400.0
|
||||
doctype = "Journal Entry Account"
|
||||
parentfield = "accounts"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
[["Journal Entry".accounts]]
|
||||
account = "Sales - _TC"
|
||||
credit_in_account_currency = 400.0
|
||||
debit_in_account_currency = 0.0
|
||||
doctype = "Journal Entry Account"
|
||||
parentfield = "accounts"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
[{
|
||||
"doctype": "Monthly Distribution",
|
||||
"distribution_id": "_Test Distribution",
|
||||
"fiscal_year": "_Test Fiscal Year 2013",
|
||||
"percentages": [
|
||||
{
|
||||
"month": "January",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "February",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "March",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "April",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "May",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "June",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "July",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "August",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "September",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "October",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "November",
|
||||
"percentage_allocation": "10"
|
||||
}, {
|
||||
"month": "December",
|
||||
"percentage_allocation": "10"
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -1,52 +0,0 @@
|
||||
[["Monthly Distribution"]]
|
||||
distribution_id = "_Test Distribution"
|
||||
fiscal_year = "_Test Fiscal Year 2013"
|
||||
[["Monthly Distribution".percentages]]
|
||||
month = "January"
|
||||
percentage_allocation = "8"
|
||||
|
||||
[["Monthly Distribution".percentages]]
|
||||
month = "February"
|
||||
percentage_allocation = "8"
|
||||
|
||||
[["Monthly Distribution".percentages]]
|
||||
month = "March"
|
||||
percentage_allocation = "8"
|
||||
|
||||
[["Monthly Distribution".percentages]]
|
||||
month = "April"
|
||||
percentage_allocation = "8"
|
||||
|
||||
[["Monthly Distribution".percentages]]
|
||||
month = "May"
|
||||
percentage_allocation = "8"
|
||||
|
||||
[["Monthly Distribution".percentages]]
|
||||
month = "June"
|
||||
percentage_allocation = "8"
|
||||
|
||||
[["Monthly Distribution".percentages]]
|
||||
month = "July"
|
||||
percentage_allocation = "8"
|
||||
|
||||
[["Monthly Distribution".percentages]]
|
||||
month = "August"
|
||||
percentage_allocation = "8"
|
||||
|
||||
[["Monthly Distribution".percentages]]
|
||||
month = "September"
|
||||
percentage_allocation = "8"
|
||||
|
||||
[["Monthly Distribution".percentages]]
|
||||
month = "October"
|
||||
percentage_allocation = "8"
|
||||
|
||||
[["Monthly Distribution".percentages]]
|
||||
month = "November"
|
||||
percentage_allocation = "10"
|
||||
|
||||
[["Monthly Distribution".percentages]]
|
||||
month = "December"
|
||||
percentage_allocation = "10"
|
||||
|
||||
|
||||
@@ -200,14 +200,14 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "party",
|
||||
"depends_on": "eval: doc.party && doc.party_type !== \"Employee\"",
|
||||
"fieldname": "contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Contact",
|
||||
"options": "Contact"
|
||||
},
|
||||
{
|
||||
"depends_on": "contact_person",
|
||||
"depends_on": "eval: (doc.contact_person || doc.party_type === \"Employee\") && doc.contact_email",
|
||||
"fieldname": "contact_email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Email",
|
||||
@@ -777,7 +777,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-01-31 11:24:58.076393",
|
||||
"modified": "2025-03-24 16:18:19.920701",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -7,6 +7,7 @@ from functools import reduce
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _, qb, scrub, throw
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import Tuple
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
||||
@@ -37,7 +38,11 @@ from erpnext.accounts.general_ledger import (
|
||||
make_reverse_gl_entries,
|
||||
process_gl_map,
|
||||
)
|
||||
from erpnext.accounts.party import complete_contact_details, get_party_account, set_contact_details
|
||||
from erpnext.accounts.party import (
|
||||
complete_contact_details,
|
||||
get_default_contact,
|
||||
get_party_account,
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
@@ -334,16 +339,18 @@ class PaymentEntry(AccountsController):
|
||||
reference_names.add(key)
|
||||
|
||||
def set_bank_account_data(self):
|
||||
if self.bank_account:
|
||||
bank_data = get_bank_account_details(self.bank_account)
|
||||
if not self.bank_account:
|
||||
return
|
||||
|
||||
field = "paid_from" if self.payment_type == "Pay" else "paid_to"
|
||||
bank_data = get_bank_account_details(self.bank_account)
|
||||
|
||||
self.bank = bank_data.bank
|
||||
self.bank_account_no = bank_data.bank_account_no
|
||||
field = "paid_from" if self.payment_type == "Pay" else "paid_to"
|
||||
|
||||
if not self.get(field):
|
||||
self.set(field, bank_data.account)
|
||||
self.bank = bank_data.bank
|
||||
self.bank_account_no = bank_data.bank_account_no
|
||||
|
||||
if not self.get(field):
|
||||
self.set(field, bank_data.account)
|
||||
|
||||
def validate_payment_type_with_outstanding(self):
|
||||
total_outstanding = sum(d.allocated_amount for d in self.get("references"))
|
||||
@@ -361,15 +368,16 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
if self.party_type in ("Customer", "Supplier"):
|
||||
self.validate_allocated_amount_with_latest_data()
|
||||
else:
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
for d in self.get("references"):
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(d.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
return
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
for d in self.get("references"):
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(d.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def validate_allocated_amount_as_per_payment_request(self):
|
||||
"""
|
||||
@@ -407,91 +415,89 @@ class PaymentEntry(AccountsController):
|
||||
return False
|
||||
|
||||
def validate_allocated_amount_with_latest_data(self):
|
||||
if self.references:
|
||||
uniq_vouchers = set([(x.reference_doctype, x.reference_name) for x in self.references])
|
||||
vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in uniq_vouchers]
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"company": self.company,
|
||||
"party_type": self.party_type,
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||
"get_outstanding_invoices": True,
|
||||
"get_orders_to_be_billed": True,
|
||||
"vouchers": vouchers,
|
||||
"book_advance_payments_in_separate_party_account": self.book_advance_payments_in_separate_party_account,
|
||||
},
|
||||
validate=True,
|
||||
)
|
||||
if not self.references:
|
||||
return
|
||||
|
||||
# Group latest_references by (voucher_type, voucher_no)
|
||||
latest_lookup = {}
|
||||
for d in latest_references:
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
|
||||
uniq_vouchers = {(x.reference_doctype, x.reference_name) for x in self.references}
|
||||
vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in uniq_vouchers]
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"company": self.company,
|
||||
"party_type": self.party_type,
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||
"get_outstanding_invoices": True,
|
||||
"get_orders_to_be_billed": True,
|
||||
"vouchers": vouchers,
|
||||
"book_advance_payments_in_separate_party_account": self.book_advance_payments_in_separate_party_account,
|
||||
},
|
||||
validate=True,
|
||||
)
|
||||
|
||||
for idx, d in enumerate(self.get("references"), start=1):
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
|
||||
# Group latest_references by (voucher_type, voucher_no)
|
||||
latest_lookup = {}
|
||||
for d in latest_references:
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
|
||||
|
||||
# If term based allocation is enabled, throw
|
||||
if (
|
||||
d.payment_term is None or d.payment_term == ""
|
||||
) and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
|
||||
).format(frappe.bold(d.reference_name), frappe.bold(idx))
|
||||
)
|
||||
for idx, d in enumerate(self.get("references"), start=1):
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
|
||||
|
||||
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
|
||||
latest = latest.get(d.payment_term) or latest.get(None)
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(
|
||||
_(d.reference_doctype), d.reference_name
|
||||
)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif (
|
||||
latest.outstanding_amount < latest.invoice_amount
|
||||
and flt(d.outstanding_amount, d.precision("outstanding_amount"))
|
||||
!= flt(latest.outstanding_amount, d.precision("outstanding_amount"))
|
||||
and d.payment_term == ""
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||
).format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
# If term based allocation is enabled, throw
|
||||
if (
|
||||
d.payment_term is None or d.payment_term == ""
|
||||
) and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
|
||||
).format(frappe.bold(d.reference_name), frappe.bold(idx))
|
||||
)
|
||||
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
|
||||
latest = latest.get(d.payment_term) or latest.get(None)
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif (
|
||||
latest.outstanding_amount < latest.invoice_amount
|
||||
and flt(d.outstanding_amount, d.precision("outstanding_amount"))
|
||||
!= flt(latest.outstanding_amount, d.precision("outstanding_amount"))
|
||||
and d.payment_term == ""
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||
).format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
|
||||
if (
|
||||
d.payment_term
|
||||
and (
|
||||
(flt(d.allocated_amount)) > 0
|
||||
and latest.payment_term_outstanding
|
||||
and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
|
||||
)
|
||||
and self.term_based_allocation_enabled_for_reference(
|
||||
d.reference_doctype, d.reference_name
|
||||
)
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
|
||||
).format(d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term)
|
||||
)
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
if (
|
||||
d.payment_term
|
||||
and (
|
||||
(flt(d.allocated_amount)) > 0
|
||||
and latest.payment_term_outstanding
|
||||
and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
|
||||
)
|
||||
and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
|
||||
).format(d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term)
|
||||
)
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def delink_advance_entry_references(self):
|
||||
for reference in self.references:
|
||||
@@ -524,25 +530,27 @@ class PaymentEntry(AccountsController):
|
||||
self.party_name = frappe.db.get_value(self.party_type, self.party, "name")
|
||||
|
||||
if self.party:
|
||||
if not self.contact_person:
|
||||
set_contact_details(
|
||||
self, party=frappe._dict({"name": self.party}), party_type=self.party_type
|
||||
)
|
||||
else:
|
||||
complete_contact_details(self)
|
||||
if self.party_type == "Employee":
|
||||
self.contact_person = None
|
||||
elif not self.contact_person:
|
||||
self.contact_person = get_default_contact(self.party_type, self.party)
|
||||
|
||||
complete_contact_details(self)
|
||||
|
||||
if not self.party_account:
|
||||
party_account = get_party_account(self.party_type, self.party, self.company)
|
||||
self.set(self.party_account_field, party_account)
|
||||
self.party_account = party_account
|
||||
|
||||
if self.paid_from and not self.paid_from_account_currency:
|
||||
if self.paid_from and (not self.paid_from_account_currency or not self.paid_from_account_type):
|
||||
acc = get_account_details(self.paid_from, self.posting_date, self.cost_center)
|
||||
self.paid_from_account_currency = acc.account_currency
|
||||
self.paid_from_account_type = acc.account_type
|
||||
|
||||
if self.paid_to and not self.paid_to_account_currency:
|
||||
if self.paid_to and (not self.paid_to_account_currency or not self.paid_to_account_type):
|
||||
acc = get_account_details(self.paid_to, self.posting_date, self.cost_center)
|
||||
self.paid_to_account_currency = acc.account_currency
|
||||
self.paid_to_account_type = acc.account_type
|
||||
|
||||
self.party_account_currency = (
|
||||
self.paid_from_account_currency
|
||||
@@ -557,51 +565,52 @@ class PaymentEntry(AccountsController):
|
||||
reference_exchange_details: dict | None = None,
|
||||
) -> None:
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount:
|
||||
if (
|
||||
update_ref_details_only_for
|
||||
and (d.reference_doctype, d.reference_name) not in update_ref_details_only_for
|
||||
):
|
||||
if not d.allocated_amount:
|
||||
continue
|
||||
|
||||
if (
|
||||
update_ref_details_only_for
|
||||
and (d.reference_doctype, d.reference_name) not in update_ref_details_only_for
|
||||
):
|
||||
continue
|
||||
|
||||
ref_details = get_reference_details(
|
||||
d.reference_doctype,
|
||||
d.reference_name,
|
||||
self.party_account_currency,
|
||||
self.party_type,
|
||||
self.party,
|
||||
)
|
||||
|
||||
# Only update exchange rate when the reference is Journal Entry
|
||||
if (
|
||||
reference_exchange_details
|
||||
and d.reference_doctype == reference_exchange_details.reference_doctype
|
||||
and d.reference_name == reference_exchange_details.reference_name
|
||||
):
|
||||
ref_details.update({"exchange_rate": reference_exchange_details.exchange_rate})
|
||||
|
||||
for field, value in ref_details.items():
|
||||
if d.exchange_gain_loss:
|
||||
# for cases where gain/loss is booked into invoice
|
||||
# exchange_gain_loss is calculated from invoice & populated
|
||||
# and row.exchange_rate is already set to payment entry's exchange rate
|
||||
# refer -> `update_reference_in_payment_entry()` in utils.py
|
||||
continue
|
||||
|
||||
ref_details = get_reference_details(
|
||||
d.reference_doctype,
|
||||
d.reference_name,
|
||||
self.party_account_currency,
|
||||
self.party_type,
|
||||
self.party,
|
||||
)
|
||||
|
||||
# Only update exchange rate when the reference is Journal Entry
|
||||
if (
|
||||
reference_exchange_details
|
||||
and d.reference_doctype == reference_exchange_details.reference_doctype
|
||||
and d.reference_name == reference_exchange_details.reference_name
|
||||
):
|
||||
ref_details.update({"exchange_rate": reference_exchange_details.exchange_rate})
|
||||
|
||||
for field, value in ref_details.items():
|
||||
if d.exchange_gain_loss:
|
||||
# for cases where gain/loss is booked into invoice
|
||||
# exchange_gain_loss is calculated from invoice & populated
|
||||
# and row.exchange_rate is already set to payment entry's exchange rate
|
||||
# refer -> `update_reference_in_payment_entry()` in utils.py
|
||||
continue
|
||||
|
||||
if field == "exchange_rate" or not d.get(field) or force:
|
||||
if self.get("_action") in ("submit", "cancel"):
|
||||
d.db_set(field, value)
|
||||
else:
|
||||
d.set(field, value)
|
||||
if field == "exchange_rate" or not d.get(field) or force:
|
||||
if self.get("_action") in ("submit", "cancel"):
|
||||
d.db_set(field, value)
|
||||
else:
|
||||
d.set(field, value)
|
||||
|
||||
def validate_payment_type(self):
|
||||
if self.payment_type not in ("Receive", "Pay", "Internal Transfer"):
|
||||
frappe.throw(_("Payment Type must be one of Receive, Pay and Internal Transfer"))
|
||||
|
||||
def validate_party_details(self):
|
||||
if self.party:
|
||||
if not frappe.db.exists(self.party_type, self.party):
|
||||
frappe.throw(_("{0} {1} does not exist").format(_(self.party_type), self.party))
|
||||
if self.party and not frappe.db.exists(self.party_type, self.party):
|
||||
frappe.throw(_("{0} {1} does not exist").format(_(self.party_type), self.party))
|
||||
|
||||
def set_exchange_rate(self, ref_doc=None):
|
||||
self.set_source_exchange_rate(ref_doc)
|
||||
@@ -611,12 +620,8 @@ class PaymentEntry(AccountsController):
|
||||
if self.paid_from:
|
||||
if self.paid_from_account_currency == self.company_currency:
|
||||
self.source_exchange_rate = 1
|
||||
else:
|
||||
if ref_doc:
|
||||
if self.paid_from_account_currency == ref_doc.currency:
|
||||
self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get(
|
||||
"conversion_rate"
|
||||
)
|
||||
elif ref_doc and self.paid_from_account_currency == ref_doc.currency:
|
||||
self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
|
||||
|
||||
if not self.source_exchange_rate:
|
||||
self.source_exchange_rate = get_exchange_rate(
|
||||
@@ -627,9 +632,8 @@ class PaymentEntry(AccountsController):
|
||||
if self.paid_from_account_currency == self.paid_to_account_currency:
|
||||
self.target_exchange_rate = self.source_exchange_rate
|
||||
elif self.paid_to and not self.target_exchange_rate:
|
||||
if ref_doc:
|
||||
if self.paid_to_account_currency == ref_doc.currency:
|
||||
self.target_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
|
||||
if ref_doc and self.paid_to_account_currency == ref_doc.currency:
|
||||
self.target_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
|
||||
|
||||
if not self.target_exchange_rate:
|
||||
self.target_exchange_rate = get_exchange_rate(
|
||||
@@ -660,63 +664,61 @@ class PaymentEntry(AccountsController):
|
||||
elif d.reference_name:
|
||||
if not frappe.db.exists(d.reference_doctype, d.reference_name):
|
||||
frappe.throw(_("{0} {1} does not exist").format(d.reference_doctype, d.reference_name))
|
||||
else:
|
||||
ref_doc = frappe.get_doc(d.reference_doctype, d.reference_name)
|
||||
|
||||
if d.reference_doctype != "Journal Entry":
|
||||
if self.party != ref_doc.get(scrub(self.party_type)):
|
||||
frappe.throw(
|
||||
_("{0} {1} is not associated with {2} {3}").format(
|
||||
_(d.reference_doctype), d.reference_name, _(self.party_type), self.party
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.validate_journal_entry()
|
||||
ref_doc = frappe.get_doc(d.reference_doctype, d.reference_name)
|
||||
|
||||
if d.reference_doctype in frappe.get_hooks("invoice_doctypes"):
|
||||
if self.party_type == "Customer":
|
||||
ref_party_account = (
|
||||
get_party_account_based_on_invoice_discounting(d.reference_name)
|
||||
or ref_doc.debit_to
|
||||
)
|
||||
elif self.party_type == "Supplier":
|
||||
ref_party_account = ref_doc.credit_to
|
||||
elif self.party_type == "Employee":
|
||||
ref_party_account = ref_doc.payable_account
|
||||
|
||||
if (
|
||||
ref_party_account != self.party_account
|
||||
and not self.book_advance_payments_in_separate_party_account
|
||||
):
|
||||
frappe.throw(
|
||||
_("{0} {1} is associated with {2}, but Party Account is {3}").format(
|
||||
_(d.reference_doctype),
|
||||
d.reference_name,
|
||||
ref_party_account,
|
||||
self.party_account,
|
||||
)
|
||||
)
|
||||
|
||||
if ref_doc.doctype == "Purchase Invoice" and ref_doc.get("on_hold"):
|
||||
frappe.throw(
|
||||
_("{0} {1} is on hold").format(_(d.reference_doctype), d.reference_name),
|
||||
title=_("Invalid Purchase Invoice"),
|
||||
)
|
||||
|
||||
if ref_doc.docstatus != 1:
|
||||
if d.reference_doctype != "Journal Entry":
|
||||
if self.party != ref_doc.get(scrub(self.party_type)):
|
||||
frappe.throw(
|
||||
_("{0} {1} must be submitted").format(_(d.reference_doctype), d.reference_name)
|
||||
_("{0} {1} is not associated with {2} {3}").format(
|
||||
_(d.reference_doctype), d.reference_name, _(self.party_type), self.party
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.validate_journal_entry()
|
||||
|
||||
if d.reference_doctype in frappe.get_hooks("invoice_doctypes"):
|
||||
if self.party_type == "Customer":
|
||||
ref_party_account = (
|
||||
get_party_account_based_on_invoice_discounting(d.reference_name)
|
||||
or ref_doc.debit_to
|
||||
)
|
||||
elif self.party_type == "Supplier":
|
||||
ref_party_account = ref_doc.credit_to
|
||||
elif self.party_type == "Employee":
|
||||
ref_party_account = ref_doc.payable_account
|
||||
|
||||
if (
|
||||
ref_party_account != self.party_account
|
||||
and not self.book_advance_payments_in_separate_party_account
|
||||
):
|
||||
frappe.throw(
|
||||
_("{0} {1} is associated with {2}, but Party Account is {3}").format(
|
||||
_(d.reference_doctype),
|
||||
d.reference_name,
|
||||
ref_party_account,
|
||||
self.party_account,
|
||||
)
|
||||
)
|
||||
|
||||
if ref_doc.doctype == "Purchase Invoice" and ref_doc.get("on_hold"):
|
||||
frappe.throw(
|
||||
_("{0} {1} is on hold").format(_(d.reference_doctype), d.reference_name),
|
||||
title=_("Invalid Purchase Invoice"),
|
||||
)
|
||||
|
||||
if ref_doc.docstatus != 1:
|
||||
frappe.throw(
|
||||
_("{0} {1} must be submitted").format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
|
||||
def get_valid_reference_doctypes(self):
|
||||
if self.party_type == "Customer":
|
||||
return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning", "Payment Entry")
|
||||
elif self.party_type in ["Shareholder", "Employee"]:
|
||||
return ("Journal Entry",)
|
||||
elif self.party_type == "Supplier":
|
||||
return ("Purchase Order", "Purchase Invoice", "Journal Entry", "Payment Entry")
|
||||
elif self.party_type == "Shareholder":
|
||||
return ("Journal Entry",)
|
||||
elif self.party_type == "Employee":
|
||||
return ("Journal Entry",)
|
||||
|
||||
def validate_paid_invoices(self):
|
||||
no_oustanding_refs = {}
|
||||
@@ -782,37 +784,39 @@ class PaymentEntry(AccountsController):
|
||||
invoice_paid_amount_map = {}
|
||||
|
||||
for ref in self.get("references"):
|
||||
if ref.payment_term and ref.reference_name:
|
||||
key = (ref.payment_term, ref.reference_name, ref.reference_doctype)
|
||||
invoice_payment_amount_map.setdefault(key, 0.0)
|
||||
invoice_payment_amount_map[key] += ref.allocated_amount
|
||||
if not ref.payment_term or not ref.reference_name:
|
||||
continue
|
||||
|
||||
if not invoice_paid_amount_map.get(key):
|
||||
payment_schedule = frappe.get_all(
|
||||
"Payment Schedule",
|
||||
filters={"parent": ref.reference_name},
|
||||
fields=[
|
||||
"paid_amount",
|
||||
"payment_amount",
|
||||
"payment_term",
|
||||
"discount",
|
||||
"outstanding",
|
||||
"discount_type",
|
||||
],
|
||||
)
|
||||
for term in payment_schedule:
|
||||
invoice_key = (term.payment_term, ref.reference_name, ref.reference_doctype)
|
||||
invoice_paid_amount_map.setdefault(invoice_key, {})
|
||||
invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
|
||||
if not (term.discount_type and term.discount):
|
||||
continue
|
||||
key = (ref.payment_term, ref.reference_name, ref.reference_doctype)
|
||||
invoice_payment_amount_map.setdefault(key, 0.0)
|
||||
invoice_payment_amount_map[key] += ref.allocated_amount
|
||||
|
||||
if term.discount_type == "Percentage":
|
||||
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
|
||||
term.discount / 100
|
||||
)
|
||||
else:
|
||||
invoice_paid_amount_map[invoice_key]["discounted_amt"] = term.discount
|
||||
if not invoice_paid_amount_map.get(key):
|
||||
payment_schedule = frappe.get_all(
|
||||
"Payment Schedule",
|
||||
filters={"parent": ref.reference_name},
|
||||
fields=[
|
||||
"paid_amount",
|
||||
"payment_amount",
|
||||
"payment_term",
|
||||
"discount",
|
||||
"outstanding",
|
||||
"discount_type",
|
||||
],
|
||||
)
|
||||
for term in payment_schedule:
|
||||
invoice_key = (term.payment_term, ref.reference_name, ref.reference_doctype)
|
||||
invoice_paid_amount_map.setdefault(invoice_key, {})
|
||||
invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
|
||||
if not (term.discount_type and term.discount):
|
||||
continue
|
||||
|
||||
if term.discount_type == "Percentage":
|
||||
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
|
||||
term.discount / 100
|
||||
)
|
||||
else:
|
||||
invoice_paid_amount_map[invoice_key]["discounted_amt"] = term.discount
|
||||
|
||||
for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1):
|
||||
if not invoice_paid_amount_map.get(key):
|
||||
@@ -825,16 +829,39 @@ class PaymentEntry(AccountsController):
|
||||
outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
|
||||
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
|
||||
|
||||
conversion_rate = frappe.db.get_value(key[2], {"name": key[1]}, "conversion_rate")
|
||||
base_paid_amount_precision = get_field_precision(
|
||||
frappe.get_meta("Payment Schedule").get_field("base_paid_amount")
|
||||
)
|
||||
base_outstanding_precision = get_field_precision(
|
||||
frappe.get_meta("Payment Schedule").get_field("base_outstanding")
|
||||
)
|
||||
|
||||
base_paid_amount = flt(
|
||||
(allocated_amount - discounted_amt) * conversion_rate, base_paid_amount_precision
|
||||
)
|
||||
base_outstanding = flt(allocated_amount * conversion_rate, base_outstanding_precision)
|
||||
|
||||
if cancel:
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabPayment Schedule`
|
||||
SET
|
||||
paid_amount = `paid_amount` - %s,
|
||||
base_paid_amount = `base_paid_amount` - %s,
|
||||
discounted_amount = `discounted_amount` - %s,
|
||||
outstanding = `outstanding` + %s
|
||||
outstanding = `outstanding` + %s,
|
||||
base_outstanding = `base_outstanding` - %s
|
||||
WHERE parent = %s and payment_term = %s""",
|
||||
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
|
||||
(
|
||||
allocated_amount - discounted_amt,
|
||||
base_paid_amount,
|
||||
discounted_amt,
|
||||
allocated_amount,
|
||||
base_outstanding,
|
||||
key[1],
|
||||
key[0],
|
||||
),
|
||||
)
|
||||
else:
|
||||
if allocated_amount > outstanding:
|
||||
@@ -850,10 +877,20 @@ class PaymentEntry(AccountsController):
|
||||
UPDATE `tabPayment Schedule`
|
||||
SET
|
||||
paid_amount = `paid_amount` + %s,
|
||||
base_paid_amount = `base_paid_amount` + %s,
|
||||
discounted_amount = `discounted_amount` + %s,
|
||||
outstanding = `outstanding` - %s
|
||||
outstanding = `outstanding` - %s,
|
||||
base_outstanding = `base_outstanding` - %s
|
||||
WHERE parent = %s and payment_term = %s""",
|
||||
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
|
||||
(
|
||||
allocated_amount - discounted_amt,
|
||||
base_paid_amount,
|
||||
discounted_amt,
|
||||
allocated_amount,
|
||||
base_outstanding,
|
||||
key[1],
|
||||
key[0],
|
||||
),
|
||||
)
|
||||
|
||||
def get_allocated_amount_in_transaction_currency(
|
||||
@@ -1026,14 +1063,14 @@ class PaymentEntry(AccountsController):
|
||||
applicable_tax = 0
|
||||
base_applicable_tax = 0
|
||||
for tax in self.get("taxes"):
|
||||
if not tax.included_in_paid_amount:
|
||||
amount = -1 * tax.tax_amount if tax.add_deduct_tax == "Deduct" else tax.tax_amount
|
||||
base_amount = (
|
||||
-1 * tax.base_tax_amount if tax.add_deduct_tax == "Deduct" else tax.base_tax_amount
|
||||
)
|
||||
if tax.included_in_paid_amount:
|
||||
continue
|
||||
|
||||
applicable_tax += amount
|
||||
base_applicable_tax += base_amount
|
||||
amount = -1 * tax.tax_amount if tax.add_deduct_tax == "Deduct" else tax.tax_amount
|
||||
base_amount = -1 * tax.base_tax_amount if tax.add_deduct_tax == "Deduct" else tax.base_tax_amount
|
||||
|
||||
applicable_tax += amount
|
||||
base_applicable_tax += base_amount
|
||||
|
||||
self.paid_amount_after_tax = flt(
|
||||
flt(self.paid_amount) + flt(applicable_tax), self.precision("paid_amount_after_tax")
|
||||
@@ -1311,15 +1348,22 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
self.set("remarks", "\n".join(remarks))
|
||||
|
||||
def set_transaction_currency_and_rate(self):
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
self.transaction_currency = company_currency
|
||||
self.transaction_exchange_rate = 1
|
||||
|
||||
if self.paid_from_account_currency != company_currency:
|
||||
self.transaction_currency = self.paid_from_account_currency
|
||||
self.transaction_exchange_rate = self.source_exchange_rate
|
||||
elif self.paid_to_account_currency != company_currency:
|
||||
self.transaction_currency = self.paid_to_account_currency
|
||||
self.transaction_exchange_rate = self.target_exchange_rate
|
||||
|
||||
def build_gl_map(self):
|
||||
if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"):
|
||||
self.setup_party_account_field()
|
||||
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
if self.paid_from_account_currency != company_currency:
|
||||
self.currency = self.paid_from_account_currency
|
||||
elif self.paid_to_account_currency != company_currency:
|
||||
self.currency = self.paid_to_account_currency
|
||||
self.set_transaction_currency_and_rate()
|
||||
|
||||
gl_entries = []
|
||||
self.add_party_gl_entries(gl_entries)
|
||||
@@ -1400,6 +1444,9 @@ class PaymentEntry(AccountsController):
|
||||
"cost_center": cost_center,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
dr_or_cr + "_in_transaction_currency": d.allocated_amount
|
||||
if self.transaction_currency == self.party_account_currency
|
||||
else allocated_amount_in_company_currency / self.transaction_exchange_rate,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
@@ -1444,6 +1491,9 @@ class PaymentEntry(AccountsController):
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": self.cost_center,
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr + "_in_transaction_currency": self.unallocated_amount
|
||||
if self.party_account_currency == self.transaction_currency
|
||||
else base_unallocated_amount / self.transaction_exchange_rate,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
},
|
||||
item=self,
|
||||
@@ -1461,6 +1511,7 @@ class PaymentEntry(AccountsController):
|
||||
def make_advance_gl_entries(
|
||||
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"
|
||||
):
|
||||
self.set_transaction_currency_and_rate()
|
||||
gl_entries = []
|
||||
self.add_advance_gl_entries(gl_entries, entry)
|
||||
|
||||
@@ -1540,9 +1591,16 @@ class PaymentEntry(AccountsController):
|
||||
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
|
||||
|
||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||
base_allocated_amount = self.calculate_base_allocated_amount_for_reference(invoice)
|
||||
args_dict["account"] = account
|
||||
args_dict[dr_or_cr] = self.calculate_base_allocated_amount_for_reference(invoice)
|
||||
args_dict[dr_or_cr] = base_allocated_amount
|
||||
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
|
||||
args_dict[dr_or_cr + "_in_transaction_currency"] = (
|
||||
invoice.allocated_amount
|
||||
if self.party_account_currency == self.transaction_currency
|
||||
else base_allocated_amount / self.transaction_exchange_rate
|
||||
)
|
||||
|
||||
args_dict.update(
|
||||
{
|
||||
"against_voucher_type": invoice.reference_doctype,
|
||||
@@ -1560,8 +1618,13 @@ class PaymentEntry(AccountsController):
|
||||
args_dict[dr_or_cr + "_in_account_currency"] = 0
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
args_dict["account"] = self.party_account
|
||||
args_dict[dr_or_cr] = self.calculate_base_allocated_amount_for_reference(invoice)
|
||||
args_dict[dr_or_cr] = base_allocated_amount
|
||||
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
|
||||
args_dict[dr_or_cr + "_in_transaction_currency"] = (
|
||||
invoice.allocated_amount
|
||||
if self.party_account_currency == self.transaction_currency
|
||||
else base_allocated_amount / self.transaction_exchange_rate
|
||||
)
|
||||
args_dict.update(
|
||||
{
|
||||
"against_voucher_type": "Payment Entry",
|
||||
@@ -1583,6 +1646,9 @@ class PaymentEntry(AccountsController):
|
||||
"account_currency": self.paid_from_account_currency,
|
||||
"against": self.party if self.payment_type == "Pay" else self.paid_to,
|
||||
"credit_in_account_currency": self.paid_amount,
|
||||
"credit_in_transaction_currency": self.paid_amount
|
||||
if self.paid_from_account_currency == self.transaction_currency
|
||||
else self.base_paid_amount / self.transaction_exchange_rate,
|
||||
"credit": self.base_paid_amount,
|
||||
"cost_center": self.cost_center,
|
||||
"post_net_value": True,
|
||||
@@ -1598,6 +1664,9 @@ class PaymentEntry(AccountsController):
|
||||
"account_currency": self.paid_to_account_currency,
|
||||
"against": self.party if self.payment_type == "Receive" else self.paid_from,
|
||||
"debit_in_account_currency": self.received_amount,
|
||||
"debit_in_transaction_currency": self.received_amount
|
||||
if self.paid_to_account_currency == self.transaction_currency
|
||||
else self.base_received_amount / self.transaction_exchange_rate,
|
||||
"debit": self.base_received_amount,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
@@ -1633,6 +1702,8 @@ class PaymentEntry(AccountsController):
|
||||
dr_or_cr + "_in_account_currency": base_tax_amount
|
||||
if account_currency == self.company_currency
|
||||
else d.tax_amount,
|
||||
dr_or_cr + "_in_transaction_currency": base_tax_amount
|
||||
/ self.transaction_exchange_rate,
|
||||
"cost_center": d.cost_center,
|
||||
"post_net_value": True,
|
||||
},
|
||||
@@ -1658,6 +1729,8 @@ class PaymentEntry(AccountsController):
|
||||
rev_dr_or_cr + "_in_account_currency": base_tax_amount
|
||||
if account_currency == self.company_currency
|
||||
else d.tax_amount,
|
||||
rev_dr_or_cr + "_in_transaction_currency": base_tax_amount
|
||||
/ self.transaction_exchange_rate,
|
||||
"cost_center": self.cost_center,
|
||||
"post_net_value": True,
|
||||
},
|
||||
@@ -1668,24 +1741,27 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def add_deductions_gl_entries(self, gl_entries):
|
||||
for d in self.get("deductions"):
|
||||
if d.amount:
|
||||
account_currency = get_account_currency(d.account)
|
||||
if account_currency != self.company_currency:
|
||||
frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency))
|
||||
if not d.amount:
|
||||
continue
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": d.account,
|
||||
"account_currency": account_currency,
|
||||
"against": self.party or self.paid_from,
|
||||
"debit_in_account_currency": d.amount,
|
||||
"debit": d.amount,
|
||||
"cost_center": d.cost_center,
|
||||
},
|
||||
item=d,
|
||||
)
|
||||
account_currency = get_account_currency(d.account)
|
||||
if account_currency != self.company_currency:
|
||||
frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency))
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": d.account,
|
||||
"account_currency": account_currency,
|
||||
"against": self.party or self.paid_from,
|
||||
"debit_in_account_currency": d.amount,
|
||||
"debit_in_transaction_currency": d.amount / self.transaction_exchange_rate,
|
||||
"debit": d.amount,
|
||||
"cost_center": d.cost_center,
|
||||
},
|
||||
item=d,
|
||||
)
|
||||
)
|
||||
|
||||
def get_party_account_for_taxes(self):
|
||||
if self.payment_type == "Receive":
|
||||
@@ -1702,15 +1778,17 @@ class PaymentEntry(AccountsController):
|
||||
return flt(gl_dict.get(field, 0) / (conversion_rate or 1))
|
||||
|
||||
def update_advance_paid(self):
|
||||
if self.payment_type in ("Receive", "Pay") and self.party:
|
||||
advance_payment_doctypes = frappe.get_hooks(
|
||||
"advance_payment_receivable_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_payable_doctypes")
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
|
||||
frappe.get_doc(
|
||||
d.reference_doctype, d.reference_name, for_update=True
|
||||
).set_total_advance_paid()
|
||||
if self.payment_type not in ("Receive", "Pay") or not self.party:
|
||||
return
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
|
||||
frappe.get_doc(
|
||||
d.reference_doctype, d.reference_name, for_update=True
|
||||
).set_total_advance_paid()
|
||||
|
||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||
self.reference_no = reference_doc.name
|
||||
@@ -1942,7 +2020,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
|
||||
|
||||
elif self.party_type in ("Supplier", "Employee"):
|
||||
elif self.party_type in ("Supplier", "Customer"):
|
||||
if paid_amount > total_negative_outstanding:
|
||||
if total_negative_outstanding == 0:
|
||||
frappe.msgprint(
|
||||
@@ -2950,7 +3028,9 @@ def get_payment_entry(
|
||||
pe.paid_amount = paid_amount
|
||||
pe.received_amount = received_amount
|
||||
pe.letter_head = doc.get("letter_head")
|
||||
pe.bank_account = frappe.db.get_value("Bank Account", {"is_company_account": 1, "is_default": 1}, "name")
|
||||
pe.bank_account = frappe.db.get_value(
|
||||
"Bank Account", {"is_company_account": 1, "is_default": 1, "company": doc.company}, "name"
|
||||
)
|
||||
|
||||
if dt in ["Purchase Order", "Sales Order", "Sales Invoice", "Purchase Invoice"]:
|
||||
pe.project = doc.get("project") or reduce(
|
||||
|
||||
@@ -58,6 +58,8 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
self.assertEqual(pe.paid_to_account_type, "Cash")
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d) for d in [["Debtors - _TC", 0, 1000, so.name], ["_Test Cash - _TC", 1000.0, 0, None]]
|
||||
)
|
||||
@@ -569,6 +571,8 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
self.assertEqual(pe.paid_from_account_type, "Bank")
|
||||
|
||||
outstanding_amount, status = frappe.db.get_value(
|
||||
"Purchase Invoice", pi.name, ["outstanding_amount", "status"]
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe import _
|
||||
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
|
||||
|
||||
@@ -12,7 +12,6 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
get_company_defaults,
|
||||
get_payment_entry,
|
||||
)
|
||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||
@@ -122,16 +121,14 @@ class PaymentRequest(Document):
|
||||
title=_("Invalid Amount"),
|
||||
)
|
||||
|
||||
existing_payment_request_amount = flt(
|
||||
get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
|
||||
)
|
||||
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
if not hasattr(ref_doc, "order_type") or ref_doc.order_type != "Shopping Cart":
|
||||
ref_amount = get_amount(ref_doc, self.payment_account)
|
||||
if not ref_amount:
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
|
||||
existing_payment_request_amount = flt(get_existing_payment_request_amount(ref_doc))
|
||||
|
||||
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
|
||||
frappe.throw(
|
||||
_("Total Payment Request amount cannot be greater than {0} amount").format(
|
||||
@@ -554,19 +551,8 @@ def make_payment_request(**args):
|
||||
ref_doc.db_update()
|
||||
grand_total = grand_total - loyalty_amount
|
||||
|
||||
bank_account = (
|
||||
get_party_bank_account(args.get("party_type"), args.get("party")) if args.get("party_type") else ""
|
||||
)
|
||||
|
||||
draft_payment_request = frappe.db.get_value(
|
||||
"Payment Request",
|
||||
{"reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "docstatus": 0},
|
||||
)
|
||||
|
||||
# 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)
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc)
|
||||
|
||||
def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount):
|
||||
grand_total -= existing_payment_request_amount
|
||||
@@ -578,7 +564,7 @@ def make_payment_request(**args):
|
||||
if args.order_type == "Shopping Cart":
|
||||
# If Payment Request is in an advanced stage, then create for remaining amount.
|
||||
if get_existing_payment_request_amount(
|
||||
ref_doc.doctype, ref_doc.name, ["Initiated", "Partially Paid", "Payment Ordered", "Paid"]
|
||||
ref_doc, ["Initiated", "Partially Paid", "Payment Ordered", "Paid"]
|
||||
):
|
||||
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
|
||||
else:
|
||||
@@ -587,14 +573,10 @@ 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)
|
||||
draft_payment_request = frappe.db.get_value(
|
||||
"Payment Request",
|
||||
{"reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "docstatus": 0},
|
||||
)
|
||||
|
||||
if draft_payment_request:
|
||||
frappe.db.set_value(
|
||||
@@ -602,6 +584,11 @@ def make_payment_request(**args):
|
||||
)
|
||||
pr = frappe.get_doc("Payment Request", draft_payment_request)
|
||||
else:
|
||||
bank_account = (
|
||||
get_party_bank_account(args.get("party_type"), args.get("party"))
|
||||
if args.get("party_type")
|
||||
else ""
|
||||
)
|
||||
pr = frappe.new_doc("Payment Request")
|
||||
|
||||
if not args.get("payment_request_type"):
|
||||
@@ -681,22 +668,35 @@ def make_payment_request(**args):
|
||||
|
||||
def get_amount(ref_doc, payment_account=None):
|
||||
"""get amount based on doctype"""
|
||||
grand_total = 0
|
||||
|
||||
dt = ref_doc.doctype
|
||||
if dt in ["Sales Order", "Purchase Order"]:
|
||||
grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
|
||||
grand_total = (flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)) - ref_doc.advance_paid
|
||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if not ref_doc.get("is_pos"):
|
||||
if (
|
||||
dt == "Sales Invoice"
|
||||
and ref_doc.is_pos
|
||||
and ref_doc.payments
|
||||
and any(
|
||||
[
|
||||
payment.type == "Phone" and payment.account == payment_account
|
||||
for payment in ref_doc.payments
|
||||
]
|
||||
)
|
||||
):
|
||||
grand_total = sum(
|
||||
[
|
||||
payment.amount
|
||||
for payment in ref_doc.payments
|
||||
if payment.type == "Phone" and payment.account == payment_account
|
||||
]
|
||||
)
|
||||
else:
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
grand_total = flt(ref_doc.rounded_total or ref_doc.grand_total)
|
||||
grand_total = flt(ref_doc.outstanding_amount)
|
||||
else:
|
||||
grand_total = flt(
|
||||
flt(ref_doc.base_rounded_total or ref_doc.base_grand_total) / ref_doc.conversion_rate
|
||||
)
|
||||
elif dt == "Sales Invoice":
|
||||
for pay in ref_doc.payments:
|
||||
if pay.type == "Phone" and pay.account == payment_account:
|
||||
grand_total = pay.amount
|
||||
break
|
||||
grand_total = flt(flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate)
|
||||
elif dt == "POS Invoice":
|
||||
for pay in ref_doc.payments:
|
||||
if pay.type == "Phone" and pay.account == payment_account:
|
||||
@@ -705,10 +705,7 @@ def get_amount(ref_doc, payment_account=None):
|
||||
elif dt == "Fees":
|
||||
grand_total = ref_doc.outstanding_amount
|
||||
|
||||
if grand_total > 0:
|
||||
return flt(grand_total, get_currency_precision())
|
||||
else:
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
return flt(grand_total, get_currency_precision()) if grand_total > 0 else 0
|
||||
|
||||
|
||||
def get_irequest_status(payment_requests: None | list = None) -> list:
|
||||
@@ -751,7 +748,7 @@ def cancel_old_payment_requests(ref_dt, ref_dn):
|
||||
frappe.db.set_value("Integration Request", ireq.name, "status", "Cancelled")
|
||||
|
||||
|
||||
def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None = None) -> list:
|
||||
def get_existing_payment_request_amount(ref_doc, statuses: list | None = None) -> list:
|
||||
"""
|
||||
Return the total amount of Payment Requests against a reference document.
|
||||
"""
|
||||
@@ -759,9 +756,9 @@ def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None =
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(PR)
|
||||
.select(Sum(PR.grand_total))
|
||||
.where(PR.reference_doctype == ref_dt)
|
||||
.where(PR.reference_name == ref_dn)
|
||||
.select(Sum(PR.outstanding_amount))
|
||||
.where(PR.reference_doctype == ref_doc.doctype)
|
||||
.where(PR.reference_name == ref_doc.name)
|
||||
.where(PR.docstatus == 1)
|
||||
)
|
||||
|
||||
@@ -770,33 +767,12 @@ def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None =
|
||||
|
||||
response = query.run()
|
||||
|
||||
return response[0][0] if response[0] else 0
|
||||
os_amount_in_transaction_currency = flt(response[0][0] if response[0] else 0)
|
||||
|
||||
if ref_doc.currency != ref_doc.party_account_currency:
|
||||
os_amount_in_transaction_currency = flt(os_amount_in_transaction_currency / ref_doc.conversion_rate)
|
||||
|
||||
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(
|
||||
(PL.against_voucher_type == PER.reference_doctype)
|
||||
& (PL.against_voucher_no == PER.reference_name)
|
||||
& (PL.voucher_type == PER.parenttype)
|
||||
& (PL.voucher_no == PER.parent)
|
||||
)
|
||||
.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
|
||||
return os_amount_in_transaction_currency
|
||||
|
||||
|
||||
def get_gateway_details(args): # nosemgrep
|
||||
|
||||
@@ -465,6 +465,16 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
self.assertEqual(pr.outstanding_amount, 800)
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
|
||||
self.assertRaisesRegex(
|
||||
frappe.exceptions.ValidationError,
|
||||
re.compile(r"Payment Request is already created"),
|
||||
make_payment_request,
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
# complete payment
|
||||
pe = pr.create_payment_entry()
|
||||
|
||||
@@ -484,7 +494,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
# creating a more payment Request must not allowed
|
||||
self.assertRaisesRegex(
|
||||
frappe.exceptions.ValidationError,
|
||||
re.compile(r"Payment Request is already created"),
|
||||
re.compile(r"Payment Entry is already created"),
|
||||
make_payment_request,
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
@@ -516,6 +526,17 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
self.assertEqual(pr.party_account_currency, "INR")
|
||||
self.assertEqual(pr.status, "Initiated")
|
||||
|
||||
self.assertRaisesRegex(
|
||||
frappe.exceptions.ValidationError,
|
||||
re.compile(r"Payment Request is already created"),
|
||||
make_payment_request,
|
||||
dt="Purchase Invoice",
|
||||
dn=pi.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
# to make partial payment
|
||||
pe = pr.create_payment_entry(submit=False)
|
||||
pe.paid_amount = 2000
|
||||
@@ -544,7 +565,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
# creating a more payment Request must not allowed
|
||||
self.assertRaisesRegex(
|
||||
frappe.exceptions.ValidationError,
|
||||
re.compile(r"Payment Request is already created"),
|
||||
re.compile(r"Payment Entry is already created"),
|
||||
make_payment_request,
|
||||
dt="Purchase Invoice",
|
||||
dn=pi.name,
|
||||
@@ -748,6 +769,34 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
pi.load_from_db()
|
||||
self.assertEqual(pr_2.grand_total, pi.outstanding_amount)
|
||||
|
||||
def test_consider_journal_entry_and_return_invoice(self):
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
|
||||
si = create_sales_invoice(currency="INR", qty=5, rate=500)
|
||||
|
||||
je = make_journal_entry("_Test Cash - _TC", "Debtors - _TC", 500, save=False)
|
||||
je.accounts[1].party_type = "Customer"
|
||||
je.accounts[1].party = si.customer
|
||||
je.accounts[1].reference_type = "Sales Invoice"
|
||||
je.accounts[1].reference_name = si.name
|
||||
je.accounts[1].credit_in_account_currency = 500
|
||||
je.submit()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.paid_amount = 500
|
||||
pe.references[0].allocated_amount = 500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
cr_note = create_sales_invoice(qty=-1, rate=500, is_return=1, return_against=si.name, do_not_save=1)
|
||||
cr_note.update_outstanding_for_self = 0
|
||||
cr_note.save()
|
||||
cr_note.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)
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
"paid_amount",
|
||||
"discounted_amount",
|
||||
"column_break_3",
|
||||
"base_payment_amount"
|
||||
"base_payment_amount",
|
||||
"base_outstanding",
|
||||
"base_paid_amount"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -155,18 +157,34 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Payment Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "base_outstanding",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Outstanding (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "base_paid_amount",
|
||||
"fieldname": "base_paid_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Paid Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:11.356171",
|
||||
"modified": "2025-03-11 11:06:51.792982",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Schedule",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
|
||||
@@ -14,6 +14,8 @@ class PaymentSchedule(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
base_outstanding: DF.Currency
|
||||
base_paid_amount: DF.Currency
|
||||
base_payment_amount: DF.Currency
|
||||
description: DF.SmallText | None
|
||||
discount: DF.Float
|
||||
|
||||
34
erpnext/accounts/doctype/payment_term/test_records.json
Normal file
34
erpnext/accounts/doctype/payment_term/test_records.json
Normal file
@@ -0,0 +1,34 @@
|
||||
[
|
||||
{
|
||||
"doctype":"Payment Term",
|
||||
"due_date_based_on":"Day(s) after invoice date",
|
||||
"payment_term_name":"_Test N30",
|
||||
"description":"_Test Net 30 Days",
|
||||
"invoice_portion":50,
|
||||
"credit_days":30
|
||||
},
|
||||
{
|
||||
"doctype":"Payment Term",
|
||||
"due_date_based_on":"Day(s) after invoice date",
|
||||
"payment_term_name":"_Test COD",
|
||||
"description":"_Test Cash on Delivery",
|
||||
"invoice_portion":50,
|
||||
"credit_days":0
|
||||
},
|
||||
{
|
||||
"doctype":"Payment Term",
|
||||
"due_date_based_on":"Month(s) after the end of the invoice month",
|
||||
"payment_term_name":"_Test EONM",
|
||||
"description":"_Test End of Next Month",
|
||||
"invoice_portion":100,
|
||||
"credit_months":1
|
||||
},
|
||||
{
|
||||
"doctype":"Payment Term",
|
||||
"due_date_based_on":"Day(s) after invoice date",
|
||||
"payment_term_name":"_Test N30 1",
|
||||
"description":"_Test Net 30 Days",
|
||||
"invoice_portion":100,
|
||||
"credit_days":30
|
||||
}
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
[["Payment Term"]]
|
||||
due_date_based_on = "Day(s) after invoice date"
|
||||
payment_term_name = "_Test N30"
|
||||
description = "_Test Net 30 Days"
|
||||
invoice_portion = 50
|
||||
credit_days = 30
|
||||
|
||||
[["Payment Term"]]
|
||||
due_date_based_on = "Day(s) after invoice date"
|
||||
payment_term_name = "_Test COD"
|
||||
description = "_Test Cash on Delivery"
|
||||
invoice_portion = 50
|
||||
credit_days = 0
|
||||
|
||||
[["Payment Term"]]
|
||||
due_date_based_on = "Month(s) after the end of the invoice month"
|
||||
payment_term_name = "_Test EONM"
|
||||
description = "_Test End of Next Month"
|
||||
invoice_portion = 100
|
||||
credit_months = 1
|
||||
|
||||
[["Payment Term"]]
|
||||
due_date_based_on = "Day(s) after invoice date"
|
||||
payment_term_name = "_Test N30 1"
|
||||
description = "_Test Net 30 Days"
|
||||
invoice_portion = 100
|
||||
credit_days = 30
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
[
|
||||
{
|
||||
"doctype":"Payment Terms Template",
|
||||
"terms":[
|
||||
{
|
||||
"doctype":"Payment Terms Template Detail",
|
||||
"due_date_based_on":"Day(s) after invoice date",
|
||||
"idx":1,
|
||||
"description":"Cash on Delivery",
|
||||
"invoice_portion":50,
|
||||
"credit_days":0,
|
||||
"credit_months":0,
|
||||
"payment_term":"_Test COD"
|
||||
},
|
||||
{
|
||||
"doctype":"Payment Terms Template Detail",
|
||||
"due_date_based_on":"Day(s) after invoice date",
|
||||
"idx":2,
|
||||
"description":"Net 30 Days ",
|
||||
"invoice_portion":50,
|
||||
"credit_days":30,
|
||||
"credit_months":0,
|
||||
"payment_term":"_Test N30"
|
||||
}
|
||||
],
|
||||
"template_name":"_Test Payment Term Template"
|
||||
},
|
||||
{
|
||||
"doctype":"Payment Terms Template",
|
||||
"terms":[
|
||||
{
|
||||
"doctype":"Payment Terms Template Detail",
|
||||
"due_date_based_on":"Month(s) after the end of the invoice month",
|
||||
"idx":1,
|
||||
"description":"_Test End of Next Months",
|
||||
"invoice_portion":100,
|
||||
"credit_days":0,
|
||||
"credit_months":1,
|
||||
"payment_term":"_Test EONM"
|
||||
}
|
||||
],
|
||||
"template_name":"_Test Payment Term Template 1"
|
||||
},
|
||||
{
|
||||
"doctype":"Payment Terms Template",
|
||||
"terms":[
|
||||
{
|
||||
"doctype":"Payment Terms Template Detail",
|
||||
"due_date_based_on":"Day(s) after invoice date",
|
||||
"idx":1,
|
||||
"description":"_Test Net Within 30 days",
|
||||
"invoice_portion":100,
|
||||
"credit_days":30,
|
||||
"credit_months":0,
|
||||
"payment_term":"_Test N30 1"
|
||||
}
|
||||
],
|
||||
"template_name":"_Test Payment Term Template 3"
|
||||
}
|
||||
]
|
||||
@@ -1,49 +0,0 @@
|
||||
[["Payment Terms Template"]]
|
||||
template_name = "_Test Payment Term Template"
|
||||
[["Payment Terms Template".terms]]
|
||||
doctype = "Payment Terms Template Detail"
|
||||
due_date_based_on = "Day(s) after invoice date"
|
||||
idx = 1
|
||||
description = "Cash on Delivery"
|
||||
invoice_portion = 50
|
||||
credit_days = 0
|
||||
credit_months = 0
|
||||
payment_term = "_Test COD"
|
||||
|
||||
[["Payment Terms Template".terms]]
|
||||
doctype = "Payment Terms Template Detail"
|
||||
due_date_based_on = "Day(s) after invoice date"
|
||||
idx = 2
|
||||
description = "Net 30 Days "
|
||||
invoice_portion = 50
|
||||
credit_days = 30
|
||||
credit_months = 0
|
||||
payment_term = "_Test N30"
|
||||
|
||||
|
||||
[["Payment Terms Template"]]
|
||||
template_name = "_Test Payment Term Template 1"
|
||||
[["Payment Terms Template".terms]]
|
||||
doctype = "Payment Terms Template Detail"
|
||||
due_date_based_on = "Month(s) after the end of the invoice month"
|
||||
idx = 1
|
||||
description = "_Test End of Next Months"
|
||||
invoice_portion = 100
|
||||
credit_days = 0
|
||||
credit_months = 1
|
||||
payment_term = "_Test EONM"
|
||||
|
||||
|
||||
[["Payment Terms Template"]]
|
||||
template_name = "_Test Payment Term Template 3"
|
||||
[["Payment Terms Template".terms]]
|
||||
doctype = "Payment Terms Template Detail"
|
||||
due_date_based_on = "Day(s) after invoice date"
|
||||
idx = 1
|
||||
description = "_Test Net Within 30 days"
|
||||
invoice_portion = 100
|
||||
credit_days = 30
|
||||
credit_months = 0
|
||||
payment_term = "_Test N30 1"
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
self.cancel_gl_entries()
|
||||
|
||||
def make_gl_entries(self):
|
||||
if self.get_gle_count_in_selected_period() > 5000:
|
||||
if frappe.db.estimate_count("GL Entry") > 100_000:
|
||||
frappe.enqueue(
|
||||
process_gl_and_closing_entries,
|
||||
doc=self,
|
||||
@@ -154,16 +154,6 @@ class PeriodClosingVoucher(AccountsController):
|
||||
else:
|
||||
process_gl_and_closing_entries(self)
|
||||
|
||||
def get_gle_count_in_selected_period(self):
|
||||
return frappe.db.count(
|
||||
"GL Entry",
|
||||
{
|
||||
"posting_date": ["between", [self.period_start_date, self.period_end_date]],
|
||||
"company": self.company,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
)
|
||||
|
||||
def get_pcv_gl_entries(self):
|
||||
self.pl_accounts_reverse_gle = []
|
||||
self.closing_account_gle = []
|
||||
|
||||
@@ -26,6 +26,7 @@ class TestPeriodClosingVoucher(IntegrationTestCase):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center,
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv1.company = company
|
||||
@@ -38,6 +39,7 @@ class TestPeriodClosingVoucher(IntegrationTestCase):
|
||||
account1="Cost of Goods Sold - TPC",
|
||||
account2="Cash - TPC",
|
||||
cost_center=cost_center,
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv2.company = company
|
||||
@@ -155,6 +157,7 @@ class TestPeriodClosingVoucher(IntegrationTestCase):
|
||||
amount=400,
|
||||
cost_center=cost_center,
|
||||
posting_date="2021-03-15",
|
||||
company=company,
|
||||
)
|
||||
jv.company = company
|
||||
jv.finance_book = create_finance_book().name
|
||||
@@ -197,6 +200,7 @@ class TestPeriodClosingVoucher(IntegrationTestCase):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center,
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv1.company = company
|
||||
@@ -219,6 +223,7 @@ class TestPeriodClosingVoucher(IntegrationTestCase):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center1,
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv1.company = company
|
||||
@@ -231,6 +236,7 @@ class TestPeriodClosingVoucher(IntegrationTestCase):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center2,
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv2.company = company
|
||||
@@ -260,6 +266,7 @@ class TestPeriodClosingVoucher(IntegrationTestCase):
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center2,
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -124,6 +124,11 @@ class POSClosingEntry(StatusUpdater):
|
||||
|
||||
def on_submit(self):
|
||||
consolidate_pos_invoices(closing_entry=self)
|
||||
frappe.publish_realtime(
|
||||
f"poe_{self.pos_opening_entry}_closed",
|
||||
self,
|
||||
docname=f"POS Opening Entry/{self.pos_opening_entry}",
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
unconsolidate_pos_invoices(closing_entry=self)
|
||||
|
||||
@@ -196,6 +196,7 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
# run on validate method of selling controller
|
||||
super(SalesInvoice, self).validate()
|
||||
self.validate_pos_opening_entry()
|
||||
self.validate_auto_set_posting_time()
|
||||
self.validate_mode_of_payment()
|
||||
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||
@@ -327,6 +328,18 @@ class POSInvoice(SalesInvoice):
|
||||
_("Payment related to {0} is not completed").format(pay.mode_of_payment)
|
||||
)
|
||||
|
||||
def validate_pos_opening_entry(self):
|
||||
opening_entries = frappe.get_list(
|
||||
"POS Opening Entry", filters={"pos_profile": self.pos_profile, "status": "Open", "docstatus": 1}
|
||||
)
|
||||
if len(opening_entries) == 0:
|
||||
frappe.throw(
|
||||
title=_("POS Opening Entry Missing"),
|
||||
msg=_("No open POS Opening Entry found for POS Profile {0}.").format(
|
||||
frappe.bold(self.pos_profile)
|
||||
),
|
||||
)
|
||||
|
||||
def validate_stock_availablility(self):
|
||||
if self.is_return:
|
||||
return
|
||||
|
||||
@@ -28,6 +28,12 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=800, basic_rate=100)
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
|
||||
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
|
||||
|
||||
cls.test_user, cls.pos_profile = init_user_and_profile()
|
||||
create_opening_entry(cls.pos_profile, cls.test_user)
|
||||
|
||||
def tearDown(self):
|
||||
if frappe.session.user != "Administrator":
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -8,6 +8,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import map_child_doc, map_doc
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
|
||||
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
@@ -119,17 +120,18 @@ class POSInvoiceMergeLog(Document):
|
||||
returns = [d for d in pos_invoice_docs if d.get("is_return") == 1]
|
||||
sales = [d for d in pos_invoice_docs if d.get("is_return") == 0]
|
||||
|
||||
sales_invoice, credit_note = "", ""
|
||||
sales_invoice, credit_notes = "", {}
|
||||
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)
|
||||
distinguished_returns = self.distinguish_return_pos_invoices(returns, sales_invoice_doc)
|
||||
credit_notes = self.process_merging_into_credit_notes(distinguished_returns)
|
||||
|
||||
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
|
||||
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
|
||||
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_notes)
|
||||
|
||||
def on_cancel(self):
|
||||
pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
|
||||
@@ -159,34 +161,50 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
return sales_invoice
|
||||
|
||||
def process_merging_into_credit_note(self, data, sales_invoice_doc=None):
|
||||
credit_note = self.get_new_sales_invoice()
|
||||
credit_note.is_return = 1
|
||||
def process_merging_into_credit_notes(self, data):
|
||||
credit_notes = {}
|
||||
for key, value in data.items():
|
||||
if not value:
|
||||
continue
|
||||
|
||||
credit_note = self.merge_pos_invoice_into(credit_note, data)
|
||||
referenes = {}
|
||||
credit_note = self.get_new_sales_invoice()
|
||||
credit_note.is_return = 1
|
||||
|
||||
if sales_invoice_doc:
|
||||
credit_note.return_against = sales_invoice_doc.name
|
||||
credit_note = self.merge_pos_invoice_into(credit_note, value)
|
||||
credit_note.return_against = key
|
||||
|
||||
for d in sales_invoice_doc.items:
|
||||
referenes[d.item_code] = d.name
|
||||
credit_note.is_consolidated = 1
|
||||
credit_note.set_posting_time = 1
|
||||
credit_note.posting_date = getdate(self.posting_date)
|
||||
credit_note.posting_time = get_time(self.posting_time)
|
||||
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
||||
# credit_note.return_against = self.consolidated_invoice
|
||||
credit_note.save()
|
||||
credit_note.submit()
|
||||
|
||||
for d in credit_note.items:
|
||||
d.sales_invoice_item = referenes.get(d.item_code)
|
||||
self.consolidated_credit_note = credit_note.name
|
||||
credit_notes[credit_note.name] = [d.name for d in value]
|
||||
|
||||
credit_note.is_consolidated = 1
|
||||
credit_note.set_posting_time = 1
|
||||
credit_note.posting_date = getdate(self.posting_date)
|
||||
credit_note.posting_time = get_time(self.posting_time)
|
||||
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
||||
# credit_note.return_against = self.consolidated_invoice
|
||||
credit_note.save()
|
||||
credit_note.submit()
|
||||
return credit_notes
|
||||
|
||||
self.consolidated_credit_note = credit_note.name
|
||||
def distinguish_return_pos_invoices(self, data, sales_invoice_doc=None):
|
||||
return_invoices = {}
|
||||
|
||||
return credit_note.name
|
||||
return_invoices[sales_invoice_doc.name if sales_invoice_doc else None] = []
|
||||
|
||||
for doc in data:
|
||||
sales_invoices_of_return_against = frappe.db.get_value(
|
||||
"POS Invoice", doc.return_against, "consolidated_invoice"
|
||||
)
|
||||
if sales_invoices_of_return_against:
|
||||
if sales_invoices_of_return_against in return_invoices:
|
||||
return_invoices[sales_invoices_of_return_against].append(doc)
|
||||
else:
|
||||
return_invoices[sales_invoices_of_return_against] = [doc]
|
||||
else:
|
||||
return_invoices[sales_invoice_doc.name if sales_invoice_doc else None].append(doc)
|
||||
|
||||
return return_invoices
|
||||
|
||||
def merge_pos_invoice_into(self, invoice, data):
|
||||
items, payments, taxes = [], [], []
|
||||
@@ -212,33 +230,20 @@ class POSInvoiceMergeLog(Document):
|
||||
loyalty_amount_sum += doc.loyalty_amount
|
||||
|
||||
for item in doc.get("items"):
|
||||
found = False
|
||||
for i in items:
|
||||
if (
|
||||
i.item_code == item.item_code
|
||||
and not i.serial_and_batch_bundle
|
||||
and not i.serial_no
|
||||
and not i.batch_no
|
||||
and i.uom == item.uom
|
||||
and i.net_rate == item.net_rate
|
||||
and i.warehouse == item.warehouse
|
||||
):
|
||||
found = True
|
||||
i.qty = i.qty + item.qty
|
||||
i.amount = i.amount + item.net_amount
|
||||
i.net_amount = i.amount
|
||||
i.base_amount = i.base_amount + item.base_net_amount
|
||||
i.base_net_amount = i.base_amount
|
||||
|
||||
if not found:
|
||||
item.rate = item.net_rate
|
||||
item.amount = item.net_amount
|
||||
item.base_amount = item.base_net_amount
|
||||
item.price_list_rate = 0
|
||||
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
|
||||
if item.serial_and_batch_bundle:
|
||||
si_item.serial_and_batch_bundle = item.serial_and_batch_bundle
|
||||
items.append(si_item)
|
||||
item.rate = item.net_rate
|
||||
item.amount = item.net_amount
|
||||
item.base_amount = item.base_net_amount
|
||||
item.price_list_rate = 0
|
||||
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
|
||||
si_item.pos_invoice = doc.name
|
||||
si_item.pos_invoice_item = item.name
|
||||
if doc.is_return:
|
||||
si_item.sales_invoice_item = get_sales_invoice_item(
|
||||
doc.return_against, item.pos_invoice_item
|
||||
)
|
||||
if item.serial_and_batch_bundle:
|
||||
si_item.serial_and_batch_bundle = item.serial_and_batch_bundle
|
||||
items.append(si_item)
|
||||
|
||||
for tax in doc.get("taxes"):
|
||||
found = False
|
||||
@@ -328,16 +333,16 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
return sales_invoice
|
||||
|
||||
def update_pos_invoices(self, invoice_docs, sales_invoice="", credit_note=""):
|
||||
def update_pos_invoices(self, invoice_docs, sales_invoice="", credit_notes=None):
|
||||
for doc in invoice_docs:
|
||||
doc.load_from_db()
|
||||
doc.update(
|
||||
{
|
||||
"consolidated_invoice": None
|
||||
if self.docstatus == 2
|
||||
else (credit_note if doc.is_return else sales_invoice)
|
||||
}
|
||||
)
|
||||
inv = sales_invoice
|
||||
if doc.is_return:
|
||||
for key, value in credit_notes.items():
|
||||
if doc.name in value:
|
||||
inv = key
|
||||
break
|
||||
doc.update({"consolidated_invoice": None if self.docstatus == 2 else inv})
|
||||
doc.set_status(update=True)
|
||||
doc.save()
|
||||
|
||||
@@ -628,3 +633,26 @@ def get_error_message(message) -> str:
|
||||
return message["message"]
|
||||
except Exception:
|
||||
return str(message)
|
||||
|
||||
|
||||
def get_sales_invoice_item(return_against_pos_invoice, pos_invoice_item):
|
||||
try:
|
||||
SalesInvoice = DocType("Sales Invoice")
|
||||
SalesInvoiceItem = DocType("Sales Invoice Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(SalesInvoice)
|
||||
.from_(SalesInvoiceItem)
|
||||
.select(SalesInvoiceItem.name)
|
||||
.where(
|
||||
(SalesInvoice.name == SalesInvoiceItem.parent)
|
||||
& (SalesInvoice.is_return == 0)
|
||||
& (SalesInvoiceItem.pos_invoice == return_against_pos_invoice)
|
||||
& (SalesInvoiceItem.pos_invoice_item == pos_invoice_item)
|
||||
)
|
||||
)
|
||||
|
||||
result = query.run(as_dict=True)
|
||||
return result[0].name if result else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -70,3 +70,6 @@ class POSOpeningEntry(StatusUpdater):
|
||||
|
||||
def on_submit(self):
|
||||
self.set_status(update=True)
|
||||
|
||||
def on_cancel(self):
|
||||
self.set_status(update=True)
|
||||
|
||||
@@ -58,7 +58,8 @@
|
||||
"apply_discount_on",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break"
|
||||
"dimension_col_break",
|
||||
"project"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -406,6 +407,14 @@
|
||||
"fieldname": "disable_grand_total_to_default_mop",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable auto setting Grand Total to default Payment Mode"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"oldfieldname": "cost_center",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -433,7 +442,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2025-01-29 13:12:30.796630",
|
||||
"modified": "2025-04-09 11:35:13.779613",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
@@ -459,7 +468,8 @@
|
||||
"role": "Accounts User"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub, unscrub
|
||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_link_to_form, now
|
||||
|
||||
@@ -52,6 +53,7 @@ class POSProfile(Document):
|
||||
payments: DF.Table[POSPaymentMethod]
|
||||
print_format: DF.Link | None
|
||||
print_receipt_on_order_complete: DF.Check
|
||||
project: DF.Link | None
|
||||
select_print_heading: DF.Link | None
|
||||
selling_price_list: DF.Link | None
|
||||
tax_category: DF.Link | None
|
||||
@@ -206,17 +208,41 @@ class POSProfile(Document):
|
||||
def get_item_groups(pos_profile):
|
||||
item_groups = []
|
||||
pos_profile = frappe.get_cached_doc("POS Profile", pos_profile)
|
||||
permitted_item_groups = get_permitted_nodes("Item Group")
|
||||
|
||||
if pos_profile.get("item_groups"):
|
||||
# Get items based on the item groups defined in the POS profile
|
||||
for data in pos_profile.get("item_groups"):
|
||||
item_groups.extend(
|
||||
["%s" % frappe.db.escape(d.name) for d in get_child_nodes("Item Group", data.item_group)]
|
||||
[
|
||||
"%s" % frappe.db.escape(d.name)
|
||||
for d in get_child_nodes("Item Group", data.item_group)
|
||||
if not permitted_item_groups or d.name in permitted_item_groups
|
||||
]
|
||||
)
|
||||
|
||||
if not item_groups and permitted_item_groups:
|
||||
item_groups = ["%s" % frappe.db.escape(d) for d in permitted_item_groups]
|
||||
|
||||
return list(set(item_groups))
|
||||
|
||||
|
||||
def get_permitted_nodes(group_type):
|
||||
nodes = []
|
||||
permitted_nodes = get_permitted_documents(group_type)
|
||||
|
||||
if not permitted_nodes:
|
||||
return nodes
|
||||
|
||||
for node in permitted_nodes:
|
||||
if frappe.db.get_value(group_type, node, "is_group"):
|
||||
nodes.extend([d.name for d in get_child_nodes(group_type, node)])
|
||||
else:
|
||||
nodes.append(node)
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
def get_child_nodes(group_type, root):
|
||||
lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"])
|
||||
return frappe.db.sql(
|
||||
|
||||
@@ -454,8 +454,7 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
|
||||
if pricing_rule.coupon_code_based == 1:
|
||||
if not args.coupon_code:
|
||||
return item_details
|
||||
|
||||
continue
|
||||
coupon_code = frappe.db.get_value(
|
||||
doctype="Coupon Code", filters={"pricing_rule": pricing_rule.name}, fieldname="name"
|
||||
)
|
||||
|
||||
@@ -68,6 +68,14 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
|
||||
this.frm.trigger("supplier");
|
||||
}
|
||||
|
||||
this.frm.set_query("supplier", function () {
|
||||
return {
|
||||
filters: {
|
||||
is_transporter: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
refresh(doc) {
|
||||
|
||||
@@ -873,6 +873,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.make_payment_gl_entries(gl_entries)
|
||||
self.make_write_off_gl_entry(gl_entries)
|
||||
self.make_gle_for_rounding_adjustment(gl_entries)
|
||||
self.set_transaction_currency_and_rate_in_gl_map(gl_entries)
|
||||
return gl_entries
|
||||
|
||||
def check_asset_cwip_enabled(self):
|
||||
@@ -918,6 +919,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"credit_in_account_currency": base_grand_total
|
||||
if self.party_account_currency == self.company_currency
|
||||
else grand_total,
|
||||
"credit_in_transaction_currency": grand_total,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"project": self.project,
|
||||
@@ -953,7 +955,7 @@ class PurchaseInvoice(BuyingController):
|
||||
valuation_tax_accounts = [
|
||||
d.account_head
|
||||
for d in self.get("taxes")
|
||||
if d.category in ("Valuation", "Total and Valuation")
|
||||
if d.category in ("Valuation", "Valuation and Total")
|
||||
and flt(d.base_tax_amount_after_discount_amount)
|
||||
]
|
||||
|
||||
@@ -969,7 +971,6 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount):
|
||||
account_currency = get_account_currency(item.expense_account)
|
||||
if item.item_code:
|
||||
frappe.get_cached_value("Item", item.item_code, "asset_category")
|
||||
|
||||
@@ -978,6 +979,7 @@ class PurchaseInvoice(BuyingController):
|
||||
and self.auto_accounting_for_stock
|
||||
and (item.item_code in stock_items or item.is_fixed_asset)
|
||||
):
|
||||
account_currency = get_account_currency(item.expense_account)
|
||||
# warehouse account
|
||||
warehouse_debit_amount = self.make_stock_adjustment_entry(
|
||||
gl_entries, item, voucher_wise_stock_value, account_currency
|
||||
@@ -993,6 +995,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"project": item.project or self.project,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": warehouse_debit_amount,
|
||||
"debit_in_transaction_currency": item.net_amount,
|
||||
},
|
||||
warehouse_account[item.warehouse]["account_currency"],
|
||||
item=item,
|
||||
@@ -1013,6 +1016,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"project": item.project or self.project,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": -1 * flt(credit_amount, item.precision("base_net_amount")),
|
||||
"debit_in_transaction_currency": item.net_amount,
|
||||
},
|
||||
warehouse_account[item.from_warehouse]["account_currency"],
|
||||
item=item,
|
||||
@@ -1027,6 +1031,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"account": item.expense_account,
|
||||
"against": self.supplier,
|
||||
"debit": flt(item.base_net_amount, item.precision("base_net_amount")),
|
||||
"debit_in_transaction_currency": item.net_amount,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project,
|
||||
@@ -1044,6 +1049,10 @@ class PurchaseInvoice(BuyingController):
|
||||
"account": item.expense_account,
|
||||
"against": self.supplier,
|
||||
"debit": warehouse_debit_amount,
|
||||
"debit_in_transaction_currency": flt(
|
||||
warehouse_debit_amount / self.conversion_rate,
|
||||
item.precision("net_amount"),
|
||||
),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
@@ -1056,7 +1065,9 @@ class PurchaseInvoice(BuyingController):
|
||||
# Amount added through landed-cost-voucher
|
||||
if landed_cost_entries:
|
||||
if (item.item_code, item.name) in landed_cost_entries:
|
||||
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
|
||||
for account, base_amount in landed_cost_entries[
|
||||
(item.item_code, item.name)
|
||||
].items():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@@ -1064,8 +1075,9 @@ class PurchaseInvoice(BuyingController):
|
||||
"against": item.expense_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(amount["base_amount"]),
|
||||
"credit_in_account_currency": flt(amount["amount"]),
|
||||
"credit": flt(base_amount["base_amount"]),
|
||||
"credit_in_account_currency": flt(base_amount["amount"]),
|
||||
"credit_in_transaction_currency": item.net_amount,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
@@ -1088,6 +1100,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"project": item.project or self.project,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(item.rm_supp_cost),
|
||||
"credit_in_transaction_currency": item.net_amount,
|
||||
},
|
||||
warehouse_account[self.supplier_warehouse]["account_currency"],
|
||||
item=item,
|
||||
@@ -1101,7 +1114,8 @@ class PurchaseInvoice(BuyingController):
|
||||
else item.deferred_expense_account
|
||||
)
|
||||
|
||||
dummy, amount = self.get_amount_and_base_amount(item, None)
|
||||
account_currency = get_account_currency(expense_account)
|
||||
amount, base_amount = self.get_amount_and_base_amount(item, None)
|
||||
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
self.make_provisional_gl_entry(gl_entries, item)
|
||||
@@ -1112,7 +1126,8 @@ class PurchaseInvoice(BuyingController):
|
||||
{
|
||||
"account": expense_account,
|
||||
"against": self.supplier,
|
||||
"debit": amount,
|
||||
"debit": base_amount,
|
||||
"debit_in_transaction_currency": amount,
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
@@ -1186,6 +1201,10 @@ class PurchaseInvoice(BuyingController):
|
||||
"account": self.stock_received_but_not_billed,
|
||||
"against": self.supplier,
|
||||
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
|
||||
"debit_in_transaction_currency": flt(
|
||||
item.item_tax_amount / self.conversion_rate,
|
||||
item.precision("item_tax_amount"),
|
||||
),
|
||||
"remarks": self.remarks or _("Accounting Entry for Stock"),
|
||||
"cost_center": self.cost_center,
|
||||
"project": item.project or self.project,
|
||||
@@ -1305,6 +1324,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"account": cost_of_goods_sold_account,
|
||||
"against": item.expense_account,
|
||||
"debit": stock_adjustment_amt,
|
||||
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
|
||||
"remarks": self.get("remarks") or _("Stock Adjustment"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
@@ -1316,6 +1336,38 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
warehouse_debit_amount = stock_amount
|
||||
|
||||
elif self.is_return and self.update_stock and self.is_internal_supplier and warehouse_debit_amount:
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
stock_amount = (
|
||||
net_rate
|
||||
+ item.item_tax_amount
|
||||
+ flt(item.landed_cost_voucher_amount)
|
||||
+ flt(item.get("amount_difference_with_purchase_invoice"))
|
||||
)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
|
||||
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
|
||||
stock_adjustment_amt = stock_amount - warehouse_debit_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cost_of_goods_sold_account,
|
||||
"against": item.expense_account,
|
||||
"debit": stock_adjustment_amt,
|
||||
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
|
||||
"remarks": self.get("remarks") or _("Stock Adjustment"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
return warehouse_debit_amount
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries):
|
||||
@@ -1338,6 +1390,7 @@ class PurchaseInvoice(BuyingController):
|
||||
dr_or_cr + "_in_account_currency": base_amount
|
||||
if account_currency == self.company_currency
|
||||
else amount,
|
||||
dr_or_cr + "_in_transaction_currency": amount,
|
||||
"cost_center": tax.cost_center,
|
||||
},
|
||||
account_currency,
|
||||
@@ -1384,6 +1437,10 @@ class PurchaseInvoice(BuyingController):
|
||||
"cost_center": tax.cost_center,
|
||||
"against": self.supplier,
|
||||
"credit": applicable_amount,
|
||||
"credit_in_transaction_currency": flt(
|
||||
applicable_amount / self.conversion_rate,
|
||||
frappe.get_precision("Purchase Invoice Item", "item_tax_amount"),
|
||||
),
|
||||
"remarks": self.remarks or _("Accounting Entry for Stock"),
|
||||
},
|
||||
item=tax,
|
||||
@@ -1402,6 +1459,10 @@ class PurchaseInvoice(BuyingController):
|
||||
"cost_center": tax.cost_center,
|
||||
"against": self.supplier,
|
||||
"credit": valuation_tax[tax.name],
|
||||
"credit_in_transaction_currency": flt(
|
||||
valuation_tax[tax.name] / self.conversion_rate,
|
||||
frappe.get_precision("Purchase Invoice Item", "item_tax_amount"),
|
||||
),
|
||||
"remarks": self.remarks or _("Accounting Entry for Stock"),
|
||||
},
|
||||
item=tax,
|
||||
@@ -1417,6 +1478,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"account": self.unrealized_profit_loss_account,
|
||||
"against": self.supplier,
|
||||
"credit": flt(self.total_taxes_and_charges),
|
||||
"credit_in_transaction_currency": flt(self.total_taxes_and_charges),
|
||||
"credit_in_account_currency": flt(self.base_total_taxes_and_charges),
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
@@ -1466,6 +1528,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"debit_in_account_currency": self.base_paid_amount
|
||||
if self.party_account_currency == self.company_currency
|
||||
else self.paid_amount,
|
||||
"debit_in_transaction_currency": self.paid_amount,
|
||||
"against_voucher": self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
@@ -1487,6 +1550,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"credit_in_account_currency": self.base_paid_amount
|
||||
if bank_account_currency == self.company_currency
|
||||
else self.paid_amount,
|
||||
"credit_in_transaction_currency": self.paid_amount,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
bank_account_currency,
|
||||
@@ -1511,6 +1575,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"debit_in_account_currency": self.base_write_off_amount
|
||||
if self.party_account_currency == self.company_currency
|
||||
else self.write_off_amount,
|
||||
"debit_in_transaction_currency": self.write_off_amount,
|
||||
"against_voucher": self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
@@ -1531,6 +1596,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"credit_in_account_currency": self.base_write_off_amount
|
||||
if write_off_account_currency == self.company_currency
|
||||
else self.write_off_amount,
|
||||
"credit_in_transaction_currency": self.write_off_amount,
|
||||
"cost_center": self.cost_center or self.write_off_cost_center,
|
||||
},
|
||||
item=self,
|
||||
|
||||
@@ -2101,7 +2101,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
1,
|
||||
)
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 2500)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
frappe.db.set_value(
|
||||
@@ -2481,6 +2481,76 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
def test_adjust_incoming_rate_from_pi_with_multi_currency_and_partial_billing(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)
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
qty=10, rate=10, currency="USD", do_not_save=1, supplier="_Test Supplier USD"
|
||||
)
|
||||
pr.conversion_rate = 5300
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
incoming_rate = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"incoming_rate",
|
||||
)
|
||||
self.assertEqual(incoming_rate, 53000) # Asserting to confirm if the default calculation is correct
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.qty = 1
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
incoming_rate = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"incoming_rate",
|
||||
)
|
||||
# Test 1 : Incoming rate should not change as only the qty has changed and not the rate (this was not the case before)
|
||||
self.assertEqual(incoming_rate, 53000)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.qty = 1
|
||||
row.rate = 9
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
incoming_rate = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"incoming_rate",
|
||||
)
|
||||
# Test 2 : Rate in new PI is lower than PR, so incoming rate should also be lower
|
||||
self.assertEqual(incoming_rate, 50350)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.qty = 1
|
||||
row.rate = 12
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
incoming_rate = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"incoming_rate",
|
||||
)
|
||||
# Test 3 : Rate in new PI is higher than PR, so incoming rate should also be higher
|
||||
self.assertEqual(incoming_rate, 54766.667)
|
||||
|
||||
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_opening_invoice_rounding_adjustment_validation(self):
|
||||
pi = make_purchase_invoice(do_not_save=1)
|
||||
pi.items[0].rate = 99.98
|
||||
@@ -2586,6 +2656,122 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", original_value
|
||||
)
|
||||
|
||||
def test_trx_currency_debit_credit_for_high_precision(self):
|
||||
exc_rate = 0.737517516
|
||||
pi = make_purchase_invoice(
|
||||
currency="USD", conversion_rate=exc_rate, qty=1, rate=2000, do_not_save=True
|
||||
)
|
||||
pi.supplier = "_Test Supplier USD"
|
||||
pi.save().submit()
|
||||
|
||||
expected = (
|
||||
("_Test Account Cost for Goods Sold - _TC", 1475.04, 0.0, 2000.0, 0.0, "USD", exc_rate),
|
||||
("_Test Payable USD - _TC", 0.0, 1475.04, 0.0, 2000.0, "USD", exc_rate),
|
||||
)
|
||||
|
||||
actual = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pi.name},
|
||||
fields=[
|
||||
"account",
|
||||
"debit",
|
||||
"credit",
|
||||
"debit_in_transaction_currency",
|
||||
"credit_in_transaction_currency",
|
||||
"transaction_currency",
|
||||
"transaction_exchange_rate",
|
||||
],
|
||||
order_by="account",
|
||||
as_list=1,
|
||||
)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_prevents_fully_returned_invoice_with_zero_quantity(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import StockOverReturnError, make_return_doc
|
||||
|
||||
invoice = make_purchase_invoice(qty=10)
|
||||
|
||||
return_doc = make_return_doc(invoice.doctype, invoice.name)
|
||||
return_doc.items[0].qty = -10
|
||||
return_doc.save().submit()
|
||||
|
||||
return_doc = make_return_doc(invoice.doctype, invoice.name)
|
||||
return_doc.items[0].qty = 0
|
||||
|
||||
self.assertRaises(StockOverReturnError, return_doc.save)
|
||||
|
||||
def test_apply_discount_on_grand_total(self):
|
||||
"""
|
||||
To test if after applying discount on grand total,
|
||||
the grand total is calculated correctly without any rounding errors
|
||||
"""
|
||||
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
|
||||
invoice.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"rate": 21.39,
|
||||
},
|
||||
)
|
||||
invoice.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"rate": 15.5,
|
||||
},
|
||||
)
|
||||
|
||||
# the grand total here will be 255.71
|
||||
invoice.disable_rounded_total = 1
|
||||
# apply discount on grand total to adjust the grand total to 255
|
||||
invoice.discount_amount = 0.71
|
||||
invoice.save()
|
||||
|
||||
# check if grand total is 496 and not something like 254.99 due to rounding errors
|
||||
self.assertEqual(invoice.grand_total, 255)
|
||||
|
||||
def test_apply_discount_on_grand_total_with_previous_row_total_tax(self):
|
||||
"""
|
||||
To test if after applying discount on grand total,
|
||||
where the tax is calculated on previous row total, the grand total is calculated correctly
|
||||
"""
|
||||
|
||||
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
|
||||
invoice.extend(
|
||||
"taxes",
|
||||
[
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"tax_amount": 100,
|
||||
},
|
||||
{
|
||||
"charge_type": "On Previous Row Amount",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"row_id": 1,
|
||||
"rate": 10,
|
||||
},
|
||||
{
|
||||
"charge_type": "On Previous Row Total",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"row_id": 1,
|
||||
"rate": 10,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
# the total here will be 340, so applying 40 discount
|
||||
invoice.discount_amount = 40
|
||||
invoice.save()
|
||||
|
||||
self.assertEqual(invoice.grand_total, 300)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
209
erpnext/accounts/doctype/purchase_invoice/test_records.json
Normal file
209
erpnext/accounts/doctype/purchase_invoice/test_records.json
Normal file
@@ -0,0 +1,209 @@
|
||||
[
|
||||
{
|
||||
"bill_no": "NA",
|
||||
"buying_price_list": "_Test Price List",
|
||||
"company": "_Test Company",
|
||||
"conversion_rate": 1,
|
||||
"credit_to": "_Test Payable - _TC",
|
||||
"currency": "INR",
|
||||
"doctype": "Purchase Invoice",
|
||||
"items": [
|
||||
{
|
||||
"amount": 500,
|
||||
"base_amount": 500,
|
||||
"base_rate": 50,
|
||||
"conversion_factor": 1.0,
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"doctype": "Purchase Invoice Item",
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"item_code": "_Test Item Home Desktop 100",
|
||||
"item_name": "_Test Item Home Desktop 100",
|
||||
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
|
||||
"parentfield": "items",
|
||||
"qty": 10,
|
||||
"rate": 50,
|
||||
"uom": "_Test UOM",
|
||||
"warehouse": "_Test Warehouse - _TC"
|
||||
},
|
||||
{
|
||||
"amount": 750,
|
||||
"base_amount": 750,
|
||||
"base_rate": 150,
|
||||
"conversion_factor": 1.0,
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"doctype": "Purchase Invoice Item",
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"item_code": "_Test Item Home Desktop 200",
|
||||
"item_name": "_Test Item Home Desktop 200",
|
||||
"parentfield": "items",
|
||||
"qty": 5,
|
||||
"rate": 150,
|
||||
"uom": "_Test UOM",
|
||||
"warehouse": "_Test Warehouse - _TC"
|
||||
}
|
||||
],
|
||||
"grand_total": 0,
|
||||
"naming_series": "T-PINV-",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "_Test Account Shipping Charges - _TC",
|
||||
"add_deduct_tax": "Add",
|
||||
"category": "Valuation and Total",
|
||||
"charge_type": "Actual",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Shipping Charges",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"tax_amount": 100
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Customs Duty - _TC",
|
||||
"add_deduct_tax": "Add",
|
||||
"category": "Valuation",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Customs Duty",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 10
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Excise Duty - _TC",
|
||||
"add_deduct_tax": "Add",
|
||||
"category": "Total",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Excise Duty",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 12
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Education Cess - _TC",
|
||||
"add_deduct_tax": "Add",
|
||||
"category": "Total",
|
||||
"charge_type": "On Previous Row Amount",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Education Cess",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 2,
|
||||
"row_id": 3
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account S&H Education Cess - _TC",
|
||||
"add_deduct_tax": "Add",
|
||||
"category": "Total",
|
||||
"charge_type": "On Previous Row Amount",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "S&H Education Cess",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 1,
|
||||
"row_id": 3
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account CST - _TC",
|
||||
"add_deduct_tax": "Add",
|
||||
"category": "Total",
|
||||
"charge_type": "On Previous Row Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "CST",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 2,
|
||||
"row_id": 5
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"add_deduct_tax": "Add",
|
||||
"category": "Total",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 12.5
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Discount - _TC",
|
||||
"add_deduct_tax": "Deduct",
|
||||
"category": "Total",
|
||||
"charge_type": "On Previous Row Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Discount",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 10,
|
||||
"row_id": 7
|
||||
}
|
||||
],
|
||||
"supplier": "_Test Supplier",
|
||||
"supplier_name": "_Test Supplier"
|
||||
},
|
||||
|
||||
|
||||
|
||||
{
|
||||
"bill_no": "NA",
|
||||
"buying_price_list": "_Test Price List",
|
||||
"company": "_Test Company",
|
||||
"conversion_rate": 1.0,
|
||||
"credit_to": "_Test Payable - _TC",
|
||||
"currency": "INR",
|
||||
"doctype": "Purchase Invoice",
|
||||
"items": [
|
||||
{
|
||||
"conversion_factor": 1.0,
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"doctype": "Purchase Invoice Item",
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"item_code": "_Test Item",
|
||||
"item_name": "_Test Item",
|
||||
"parentfield": "items",
|
||||
"qty": 10.0,
|
||||
"rate": 50.0,
|
||||
"uom": "_Test UOM"
|
||||
}
|
||||
],
|
||||
"grand_total": 0,
|
||||
"naming_series": "T-PINV-",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "_Test Account Shipping Charges - _TC",
|
||||
"add_deduct_tax": "Add",
|
||||
"category": "Valuation and Total",
|
||||
"charge_type": "Actual",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Shipping Charges",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"tax_amount": 100.0
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"add_deduct_tax": "Add",
|
||||
"category": "Total",
|
||||
"charge_type": "Actual",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"tax_amount": 120.0
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Customs Duty - _TC",
|
||||
"add_deduct_tax": "Add",
|
||||
"category": "Valuation",
|
||||
"charge_type": "Actual",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Customs Duty",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"tax_amount": 150.0
|
||||
}
|
||||
],
|
||||
"supplier": "_Test Supplier",
|
||||
"supplier_name": "_Test Supplier"
|
||||
}
|
||||
]
|
||||
@@ -1,194 +0,0 @@
|
||||
[["Purchase Invoice"]]
|
||||
bill_no = "NA"
|
||||
buying_price_list = "_Test Price List"
|
||||
company = "_Test Company"
|
||||
conversion_rate = 1
|
||||
credit_to = "_Test Payable - _TC"
|
||||
currency = "INR"
|
||||
grand_total = 0
|
||||
naming_series = "T-PINV-"
|
||||
supplier = "_Test Supplier"
|
||||
supplier_name = "_Test Supplier"
|
||||
[["Purchase Invoice".items]]
|
||||
amount = 500
|
||||
base_amount = 500
|
||||
base_rate = 50
|
||||
conversion_factor = 1.0
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
doctype = "Purchase Invoice Item"
|
||||
expense_account = "_Test Account Cost for Goods Sold - _TC"
|
||||
item_code = "_Test Item Home Desktop 100"
|
||||
item_name = "_Test Item Home Desktop 100"
|
||||
item_tax_template = "_Test Account Excise Duty @ 10 - _TC"
|
||||
parentfield = "items"
|
||||
qty = 10
|
||||
rate = 50
|
||||
uom = "_Test UOM"
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
[["Purchase Invoice".items]]
|
||||
amount = 750
|
||||
base_amount = 750
|
||||
base_rate = 150
|
||||
conversion_factor = 1.0
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
doctype = "Purchase Invoice Item"
|
||||
expense_account = "_Test Account Cost for Goods Sold - _TC"
|
||||
item_code = "_Test Item Home Desktop 200"
|
||||
item_name = "_Test Item Home Desktop 200"
|
||||
parentfield = "items"
|
||||
qty = 5
|
||||
rate = 150
|
||||
uom = "_Test UOM"
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
[["Purchase Invoice".taxes]]
|
||||
account_head = "_Test Account Shipping Charges - _TC"
|
||||
add_deduct_tax = "Add"
|
||||
category = "Valuation and Total"
|
||||
charge_type = "Actual"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Shipping Charges"
|
||||
doctype = "Purchase Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
tax_amount = 100
|
||||
|
||||
[["Purchase Invoice".taxes]]
|
||||
account_head = "_Test Account Customs Duty - _TC"
|
||||
add_deduct_tax = "Add"
|
||||
category = "Valuation"
|
||||
charge_type = "On Net Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Customs Duty"
|
||||
doctype = "Purchase Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 10
|
||||
|
||||
[["Purchase Invoice".taxes]]
|
||||
account_head = "_Test Account Excise Duty - _TC"
|
||||
add_deduct_tax = "Add"
|
||||
category = "Total"
|
||||
charge_type = "On Net Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Excise Duty"
|
||||
doctype = "Purchase Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 12
|
||||
|
||||
[["Purchase Invoice".taxes]]
|
||||
account_head = "_Test Account Education Cess - _TC"
|
||||
add_deduct_tax = "Add"
|
||||
category = "Total"
|
||||
charge_type = "On Previous Row Amount"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Education Cess"
|
||||
doctype = "Purchase Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 2
|
||||
row_id = 3
|
||||
|
||||
[["Purchase Invoice".taxes]]
|
||||
account_head = "_Test Account S&H Education Cess - _TC"
|
||||
add_deduct_tax = "Add"
|
||||
category = "Total"
|
||||
charge_type = "On Previous Row Amount"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "S&H Education Cess"
|
||||
doctype = "Purchase Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 1
|
||||
row_id = 3
|
||||
|
||||
[["Purchase Invoice".taxes]]
|
||||
account_head = "_Test Account CST - _TC"
|
||||
add_deduct_tax = "Add"
|
||||
category = "Total"
|
||||
charge_type = "On Previous Row Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "CST"
|
||||
doctype = "Purchase Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 2
|
||||
row_id = 5
|
||||
|
||||
[["Purchase Invoice".taxes]]
|
||||
account_head = "_Test Account VAT - _TC"
|
||||
add_deduct_tax = "Add"
|
||||
category = "Total"
|
||||
charge_type = "On Net Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "VAT"
|
||||
doctype = "Purchase Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 12.5
|
||||
|
||||
[["Purchase Invoice".taxes]]
|
||||
account_head = "_Test Account Discount - _TC"
|
||||
add_deduct_tax = "Deduct"
|
||||
category = "Total"
|
||||
charge_type = "On Previous Row Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Discount"
|
||||
doctype = "Purchase Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 10
|
||||
row_id = 7
|
||||
|
||||
|
||||
[["Purchase Invoice"]]
|
||||
bill_no = "NA"
|
||||
buying_price_list = "_Test Price List"
|
||||
company = "_Test Company"
|
||||
conversion_rate = 1.0
|
||||
credit_to = "_Test Payable - _TC"
|
||||
currency = "INR"
|
||||
grand_total = 0
|
||||
naming_series = "T-PINV-"
|
||||
supplier = "_Test Supplier"
|
||||
supplier_name = "_Test Supplier"
|
||||
[["Purchase Invoice".items]]
|
||||
conversion_factor = 1.0
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
doctype = "Purchase Invoice Item"
|
||||
expense_account = "_Test Account Cost for Goods Sold - _TC"
|
||||
item_code = "_Test Item"
|
||||
item_name = "_Test Item"
|
||||
parentfield = "items"
|
||||
qty = 10.0
|
||||
rate = 50.0
|
||||
uom = "_Test UOM"
|
||||
|
||||
[["Purchase Invoice".taxes]]
|
||||
account_head = "_Test Account Shipping Charges - _TC"
|
||||
add_deduct_tax = "Add"
|
||||
category = "Valuation and Total"
|
||||
charge_type = "Actual"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Shipping Charges"
|
||||
doctype = "Purchase Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
tax_amount = 100.0
|
||||
|
||||
[["Purchase Invoice".taxes]]
|
||||
account_head = "_Test Account VAT - _TC"
|
||||
add_deduct_tax = "Add"
|
||||
category = "Total"
|
||||
charge_type = "Actual"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "VAT"
|
||||
doctype = "Purchase Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
tax_amount = 120.0
|
||||
|
||||
[["Purchase Invoice".taxes]]
|
||||
account_head = "_Test Account Customs Duty - _TC"
|
||||
add_deduct_tax = "Add"
|
||||
category = "Valuation"
|
||||
charge_type = "Actual"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Customs Duty"
|
||||
doctype = "Purchase Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
tax_amount = 150.0
|
||||
|
||||
|
||||
@@ -462,7 +462,8 @@
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"label": "Serial No"
|
||||
"label": "Serial No",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
@@ -979,16 +980,18 @@
|
||||
"options": "currency"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-10-28 15:06:19.246141",
|
||||
"modified": "2025-03-07 10:21:59.960021",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
|
||||
@@ -8,6 +8,8 @@ from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import comma_and
|
||||
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
|
||||
|
||||
class RepostAccountingLedger(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -97,6 +99,9 @@ class RepostAccountingLedger(Document):
|
||||
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
|
||||
if doc.doctype in ["Payment Entry", "Journal Entry"]:
|
||||
gle_map = doc.build_gl_map()
|
||||
elif doc.doctype == "Purchase Receipt":
|
||||
warehouse_account_map = get_warehouse_account_map(doc.company)
|
||||
gle_map = doc.get_gl_entries(warehouse_account_map)
|
||||
else:
|
||||
gle_map = doc.get_gl_entries()
|
||||
|
||||
@@ -177,6 +182,14 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.force_set_against_expense_account()
|
||||
doc.make_gl_entries()
|
||||
|
||||
elif doc.doctype == "Purchase Receipt":
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.docstatus = 2
|
||||
doc.make_gl_entries_on_cancel()
|
||||
|
||||
doc.docstatus = 1
|
||||
doc.make_gl_entries(from_repost=True)
|
||||
|
||||
elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.make_gl_entries(1)
|
||||
|
||||
@@ -12,6 +12,8 @@ from erpnext.accounts.doctype.payment_request.payment_request import make_paymen
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries, make_purchase_receipt
|
||||
|
||||
|
||||
class TestRepostAccountingLedger(AccountsTestMixin, IntegrationTestCase):
|
||||
@@ -209,9 +211,81 @@ class TestRepostAccountingLedger(AccountsTestMixin, IntegrationTestCase):
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
def test_06_repost_purchase_receipt(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
|
||||
provisional_account = create_account(
|
||||
account_name="Provision Account",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
company=self.company,
|
||||
)
|
||||
|
||||
another_provisional_account = create_account(
|
||||
account_name="Another Provision Account",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
company=self.company,
|
||||
)
|
||||
|
||||
company = frappe.get_doc("Company", self.company)
|
||||
company.enable_provisional_accounting_for_non_stock_items = 1
|
||||
company.default_provisional_account = provisional_account
|
||||
company.save()
|
||||
|
||||
test_cc = company.cost_center
|
||||
default_expense_account = company.default_expense_account
|
||||
|
||||
item = make_item(properties={"is_stock_item": 0})
|
||||
|
||||
pr = make_purchase_receipt(company=self.company, item_code=item.name, rate=1000.0, qty=1.0)
|
||||
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||
expected_pr_gles = [
|
||||
{"account": provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||
{"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
|
||||
]
|
||||
self.assertEqual(expected_pr_gles, pr_gl_entries)
|
||||
|
||||
# change the provisional account
|
||||
frappe.db.set_value(
|
||||
"Purchase Receipt Item",
|
||||
pr.items[0].name,
|
||||
"provisional_expense_account",
|
||||
another_provisional_account,
|
||||
)
|
||||
|
||||
repost_doc = frappe.new_doc("Repost Accounting Ledger")
|
||||
repost_doc.company = self.company
|
||||
repost_doc.delete_cancelled_entries = True
|
||||
repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name})
|
||||
repost_doc.save().submit()
|
||||
|
||||
pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||
expected_pr_gles_after_repost = [
|
||||
{"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": another_provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||
]
|
||||
self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost))
|
||||
self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost)
|
||||
|
||||
# teardown
|
||||
repost_doc.cancel()
|
||||
repost_doc.delete()
|
||||
|
||||
pr.reload()
|
||||
pr.cancel()
|
||||
|
||||
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||
company.default_provisional_account = None
|
||||
company.save()
|
||||
|
||||
|
||||
def update_repost_settings():
|
||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
allowed_types = [
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
"Purchase Receipt",
|
||||
]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
|
||||
@@ -920,9 +920,25 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
}
|
||||
|
||||
const timesheets = await frm.events.get_timesheet_data(frm, kwargs);
|
||||
|
||||
if (kwargs.item_code) {
|
||||
frm.events.add_timesheet_item(frm, kwargs.item_code, timesheets);
|
||||
}
|
||||
|
||||
return frm.events.set_timesheet_data(frm, timesheets);
|
||||
},
|
||||
|
||||
add_timesheet_item: function (frm, item_code, timesheets) {
|
||||
const row = frm.add_child("items");
|
||||
frappe.model.set_value(row.doctype, row.name, "item_code", item_code);
|
||||
frappe.model.set_value(
|
||||
row.doctype,
|
||||
row.name,
|
||||
"qty",
|
||||
timesheets.reduce((a, b) => a + (b["billing_hours"] || 0.0), 0.0)
|
||||
);
|
||||
},
|
||||
|
||||
async get_timesheet_data(frm, kwargs) {
|
||||
return frappe
|
||||
.call({
|
||||
@@ -1020,6 +1036,22 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Item Code"),
|
||||
fieldname: "item_code",
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
get_query: () => {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters: {
|
||||
is_sales_item: 1,
|
||||
customer: frm.doc.customer,
|
||||
has_variants: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
fieldname: "col_break_1",
|
||||
@@ -1044,6 +1076,7 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
from_time: data.from_time,
|
||||
to_time: data.to_time,
|
||||
project: data.project,
|
||||
item_code: data.item_code,
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
|
||||
@@ -2201,6 +2201,7 @@
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 181,
|
||||
"is_submittable": 1,
|
||||
@@ -2211,7 +2212,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-02-06 15:59:54.636202",
|
||||
"modified": "2025-03-17 19:32:31.809658",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
@@ -2257,6 +2258,7 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "posting_date, due_date, customer, base_grand_total, outstanding_amount",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
@@ -2266,4 +2268,4 @@
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,8 +273,8 @@ class SalesInvoice(SellingController):
|
||||
self.indicator_title = _("Paid")
|
||||
|
||||
def validate(self):
|
||||
super().validate()
|
||||
self.validate_auto_set_posting_time()
|
||||
super().validate()
|
||||
|
||||
if not (self.is_pos or self.is_debit_note):
|
||||
self.so_dn_required()
|
||||
@@ -679,7 +679,13 @@ class SalesInvoice(SellingController):
|
||||
"Account", self.debit_to, "account_currency", cache=True
|
||||
)
|
||||
if not self.due_date and self.customer:
|
||||
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
|
||||
self.due_date = get_due_date(
|
||||
self.posting_date,
|
||||
"Customer",
|
||||
self.customer,
|
||||
self.company,
|
||||
template_name=self.payment_terms_template,
|
||||
)
|
||||
|
||||
super().set_missing_values(for_validate)
|
||||
|
||||
@@ -1238,6 +1244,7 @@ class SalesInvoice(SellingController):
|
||||
self.make_write_off_gl_entry(gl_entries)
|
||||
self.make_gle_for_rounding_adjustment(gl_entries)
|
||||
|
||||
self.set_transaction_currency_and_rate_in_gl_map(gl_entries)
|
||||
return gl_entries
|
||||
|
||||
def make_customer_gl_entry(self, gl_entries):
|
||||
@@ -1271,6 +1278,7 @@ class SalesInvoice(SellingController):
|
||||
"debit_in_account_currency": base_grand_total
|
||||
if self.party_account_currency == self.company_currency
|
||||
else grand_total,
|
||||
"debit_in_transaction_currency": grand_total,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
@@ -1302,6 +1310,9 @@ class SalesInvoice(SellingController):
|
||||
if account_currency == self.company_currency
|
||||
else flt(amount, tax.precision("tax_amount_after_discount_amount"))
|
||||
),
|
||||
"credit_in_transaction_currency": flt(
|
||||
amount, tax.precision("tax_amount_after_discount_amount")
|
||||
),
|
||||
"cost_center": tax.cost_center,
|
||||
},
|
||||
account_currency,
|
||||
@@ -1319,6 +1330,7 @@ class SalesInvoice(SellingController):
|
||||
"against": self.customer,
|
||||
"debit": flt(self.total_taxes_and_charges),
|
||||
"debit_in_account_currency": flt(self.base_total_taxes_and_charges),
|
||||
"debit_in_transaction_currency": flt(self.total_taxes_and_charges),
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
account_currency,
|
||||
@@ -1417,6 +1429,7 @@ class SalesInvoice(SellingController):
|
||||
if account_currency == self.company_currency
|
||||
else flt(amount, item.precision("net_amount"))
|
||||
),
|
||||
"credit_in_transaction_currency": flt(amount, item.precision("net_amount")),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
@@ -1468,6 +1481,7 @@ class SalesInvoice(SellingController):
|
||||
+ cstr(self.loyalty_redemption_account)
|
||||
+ " for the Loyalty Program",
|
||||
"credit": self.loyalty_amount,
|
||||
"credit_in_transaction_currency": self.loyalty_amount,
|
||||
"against_voucher": self.return_against if cint(self.is_return) else self.name,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
@@ -1482,6 +1496,7 @@ class SalesInvoice(SellingController):
|
||||
"cost_center": self.cost_center or self.loyalty_redemption_cost_center,
|
||||
"against": self.customer,
|
||||
"debit": self.loyalty_amount,
|
||||
"debit_in_transaction_currency": self.loyalty_amount,
|
||||
"remark": "Loyalty Points redeemed by the customer",
|
||||
},
|
||||
item=self,
|
||||
@@ -1515,6 +1530,7 @@ class SalesInvoice(SellingController):
|
||||
"credit_in_account_currency": payment_mode.base_amount
|
||||
if self.party_account_currency == self.company_currency
|
||||
else payment_mode.amount,
|
||||
"credit_in_transaction_currency": payment_mode.amount,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
@@ -1534,6 +1550,7 @@ class SalesInvoice(SellingController):
|
||||
"debit_in_account_currency": payment_mode.base_amount
|
||||
if payment_mode_account_currency == self.company_currency
|
||||
else payment_mode.amount,
|
||||
"debit_in_transaction_currency": payment_mode.amount,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
payment_mode_account_currency,
|
||||
@@ -1562,6 +1579,7 @@ class SalesInvoice(SellingController):
|
||||
"debit_in_account_currency": flt(self.base_change_amount)
|
||||
if self.party_account_currency == self.company_currency
|
||||
else flt(self.change_amount),
|
||||
"debit_in_transaction_currency": flt(self.change_amount),
|
||||
"against_voucher": self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
@@ -1577,6 +1595,7 @@ class SalesInvoice(SellingController):
|
||||
"account": self.account_for_change_amount,
|
||||
"against": self.customer,
|
||||
"credit": self.base_change_amount,
|
||||
"credit_in_transaction_currency": self.change_amount,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
@@ -1606,6 +1625,9 @@ class SalesInvoice(SellingController):
|
||||
if self.party_account_currency == self.company_currency
|
||||
else flt(self.write_off_amount, self.precision("write_off_amount"))
|
||||
),
|
||||
"credit_in_transaction_currency": flt(
|
||||
self.write_off_amount, self.precision("write_off_amount")
|
||||
),
|
||||
"against_voucher": self.return_against if cint(self.is_return) else self.name,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
@@ -1626,6 +1648,9 @@ class SalesInvoice(SellingController):
|
||||
if write_off_account_currency == self.company_currency
|
||||
else flt(self.write_off_amount, self.precision("write_off_amount"))
|
||||
),
|
||||
"debit_in_transaction_currency": flt(
|
||||
self.write_off_amount, self.precision("write_off_amount")
|
||||
),
|
||||
"cost_center": self.cost_center or self.write_off_cost_center or default_cost_center,
|
||||
},
|
||||
write_off_account_currency,
|
||||
@@ -1670,6 +1695,9 @@ class SalesInvoice(SellingController):
|
||||
"credit_in_account_currency": flt(
|
||||
self.rounding_adjustment, self.precision("rounding_adjustment")
|
||||
),
|
||||
"credit_in_transaction_currency": flt(
|
||||
self.rounding_adjustment, self.precision("rounding_adjustment")
|
||||
),
|
||||
"credit": flt(
|
||||
self.base_rounding_adjustment, self.precision("base_rounding_adjustment")
|
||||
),
|
||||
@@ -1934,13 +1962,16 @@ def is_overdue(doc, total):
|
||||
"base_payment_amount" if doc.party_account_currency != doc.currency else "payment_amount"
|
||||
)
|
||||
|
||||
payable_amount = sum(
|
||||
payment.get(payment_amount_field)
|
||||
for payment in doc.payment_schedule
|
||||
if getdate(payment.due_date) < today
|
||||
payable_amount = flt(
|
||||
sum(
|
||||
payment.get(payment_amount_field)
|
||||
for payment in doc.payment_schedule
|
||||
if getdate(payment.due_date) < today
|
||||
),
|
||||
doc.precision("outstanding_amount"),
|
||||
)
|
||||
|
||||
return (total - outstanding_amount) < payable_amount
|
||||
return flt(total - outstanding_amount, doc.precision("outstanding_amount")) < payable_amount
|
||||
|
||||
|
||||
def get_discounting_status(sales_invoice):
|
||||
|
||||
401
erpnext/accounts/doctype/sales_invoice/test_records.json
Normal file
401
erpnext/accounts/doctype/sales_invoice/test_records.json
Normal file
@@ -0,0 +1,401 @@
|
||||
[
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"conversion_rate": 1.0,
|
||||
"currency": "INR",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"customer": "_Test Customer",
|
||||
"customer_name": "_Test Customer",
|
||||
"debit_to": "Debtors - _TC",
|
||||
"doctype": "Sales Invoice",
|
||||
"items": [
|
||||
{
|
||||
"amount": 500.0,
|
||||
"base_amount": 500.0,
|
||||
"base_rate": 500.0,
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "138-CMS Shoe",
|
||||
"doctype": "Sales Invoice Item",
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"item_code": "138-CMS Shoe",
|
||||
"item_name": "138-CMS Shoe",
|
||||
"parentfield": "items",
|
||||
"qty": 1.0,
|
||||
"rate": 500.0,
|
||||
"uom": "_Test UOM",
|
||||
"conversion_factor": 1,
|
||||
"stock_uom": "_Test UOM"
|
||||
}
|
||||
],
|
||||
"base_grand_total": 561.8,
|
||||
"grand_total": 561.8,
|
||||
"is_pos": 0,
|
||||
"naming_series": "T-SINV-",
|
||||
"base_net_total": 500.0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"rate": 6
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"description": "Service Tax",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"rate": 6.36
|
||||
}
|
||||
],
|
||||
"plc_conversion_rate": 1.0,
|
||||
"price_list_currency": "INR",
|
||||
"sales_team": [
|
||||
{
|
||||
"allocated_percentage": 65.5,
|
||||
"doctype": "Sales Team",
|
||||
"parentfield": "sales_team",
|
||||
"sales_person": "_Test Sales Person 1"
|
||||
},
|
||||
{
|
||||
"allocated_percentage": 34.5,
|
||||
"doctype": "Sales Team",
|
||||
"parentfield": "sales_team",
|
||||
"sales_person": "_Test Sales Person 2"
|
||||
}
|
||||
],
|
||||
"selling_price_list": "_Test Price List",
|
||||
"territory": "_Test Territory"
|
||||
},
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"conversion_rate": 1.0,
|
||||
"currency": "INR",
|
||||
"customer": "_Test Customer",
|
||||
"customer_name": "_Test Customer",
|
||||
"debit_to": "Debtors - _TC",
|
||||
"doctype": "Sales Invoice",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"items": [
|
||||
{
|
||||
"amount": 500.0,
|
||||
"base_amount": 500.0,
|
||||
"base_rate": 500.0,
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "_Test Item",
|
||||
"doctype": "Sales Invoice Item",
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"income_account": "Sales - _TC",
|
||||
"item_code": "_Test Item",
|
||||
"item_name": "_Test Item",
|
||||
"parentfield": "items",
|
||||
"price_list_rate": 500.0,
|
||||
"qty": 1.0,
|
||||
"uom": "_Test UOM",
|
||||
"conversion_factor": 1,
|
||||
"stock_uom": "_Test UOM"
|
||||
}
|
||||
],
|
||||
"base_grand_total": 630.0,
|
||||
"grand_total": 630.0,
|
||||
"is_pos": 0,
|
||||
"naming_series": "T-SINV-",
|
||||
"base_net_total": 500.0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"rate": 16
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"description": "Service Tax",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"rate": 10
|
||||
}
|
||||
],
|
||||
"plc_conversion_rate": 1.0,
|
||||
"price_list_currency": "INR",
|
||||
"selling_price_list": "_Test Price List",
|
||||
"territory": "_Test Territory"
|
||||
},
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"conversion_rate": 1.0,
|
||||
"currency": "INR",
|
||||
"customer": "_Test Customer",
|
||||
"customer_name": "_Test Customer",
|
||||
"debit_to": "Debtors - _TC",
|
||||
"doctype": "Sales Invoice",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"items": [
|
||||
{
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"doctype": "Sales Invoice Item",
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"item_code": "_Test Item Home Desktop 100",
|
||||
"item_name": "_Test Item Home Desktop 100",
|
||||
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
|
||||
"parentfield": "items",
|
||||
"price_list_rate": 50,
|
||||
"qty": 10,
|
||||
"rate": 50,
|
||||
"uom": "_Test UOM 1",
|
||||
"conversion_factor": 1,
|
||||
"stock_uom": "_Test UOM 1"
|
||||
},
|
||||
{
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"doctype": "Sales Invoice Item",
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"item_code": "_Test Item Home Desktop 200",
|
||||
"item_name": "_Test Item Home Desktop 200",
|
||||
"parentfield": "items",
|
||||
"price_list_rate": 150,
|
||||
"qty": 5,
|
||||
"uom": "_Test UOM",
|
||||
"conversion_factor": 1,
|
||||
"rate": 150,
|
||||
"stock_uom": "_Test UOM"
|
||||
}
|
||||
],
|
||||
"grand_total": 0,
|
||||
"is_pos": 0,
|
||||
"naming_series": "T-SINV-",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "_Test Account Shipping Charges - _TC",
|
||||
"charge_type": "Actual",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Shipping Charges",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"tax_amount": 100
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Customs Duty - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Customs Duty",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 10
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Excise Duty - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Excise Duty",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 12
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Education Cess - _TC",
|
||||
"charge_type": "On Previous Row Amount",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Education Cess",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 2,
|
||||
"row_id": 3
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account S&H Education Cess - _TC",
|
||||
"charge_type": "On Previous Row Amount",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "S&H Education Cess",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 1,
|
||||
"row_id": 3
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account CST - _TC",
|
||||
"charge_type": "On Previous Row Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "CST",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 2,
|
||||
"row_id": 5
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 12.5
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Discount - _TC",
|
||||
"charge_type": "On Previous Row Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Discount",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": -10,
|
||||
"row_id": 7
|
||||
}
|
||||
],
|
||||
"plc_conversion_rate": 1.0,
|
||||
"price_list_currency": "INR",
|
||||
"selling_price_list": "_Test Price List",
|
||||
"territory": "_Test Territory"
|
||||
},
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"conversion_rate": 1.0,
|
||||
"currency": "INR",
|
||||
"customer": "_Test Customer",
|
||||
"customer_name": "_Test Customer",
|
||||
"debit_to": "Debtors - _TC",
|
||||
"doctype": "Sales Invoice",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"items": [
|
||||
{
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"doctype": "Sales Invoice Item",
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"item_code": "_Test Item Home Desktop 100",
|
||||
"item_name": "_Test Item Home Desktop 100",
|
||||
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
|
||||
"parentfield": "items",
|
||||
"price_list_rate": 62.5,
|
||||
"qty": 10,
|
||||
"uom": "_Test UOM 1",
|
||||
"conversion_factor": 1,
|
||||
"stock_uom": "_Test UOM 1"
|
||||
},
|
||||
{
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"doctype": "Sales Invoice Item",
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"item_code": "_Test Item Home Desktop 200",
|
||||
"item_name": "_Test Item Home Desktop 200",
|
||||
"parentfield": "items",
|
||||
"price_list_rate": 190.66,
|
||||
"qty": 5,
|
||||
"uom": "_Test UOM",
|
||||
"conversion_factor": 1,
|
||||
"stock_uom": "_Test UOM"
|
||||
}
|
||||
],
|
||||
"grand_total": 0,
|
||||
"is_pos": 0,
|
||||
"naming_series": "T-SINV-",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "_Test Account Excise Duty - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Excise Duty",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"idx": 1,
|
||||
"included_in_print_rate": 1,
|
||||
"parentfield": "taxes",
|
||||
"rate": 12
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Education Cess - _TC",
|
||||
"charge_type": "On Previous Row Amount",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Education Cess",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"idx": 2,
|
||||
"included_in_print_rate": 1,
|
||||
"parentfield": "taxes",
|
||||
"rate": 2,
|
||||
"row_id": 1
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account S&H Education Cess - _TC",
|
||||
"charge_type": "On Previous Row Amount",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "S&H Education Cess",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"idx": 3,
|
||||
"included_in_print_rate": 1,
|
||||
"parentfield": "taxes",
|
||||
"rate": 1,
|
||||
"row_id": 1
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account CST - _TC",
|
||||
"charge_type": "On Previous Row Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "CST",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"idx": 4,
|
||||
"included_in_print_rate": 1,
|
||||
"parentfield": "taxes",
|
||||
"rate": 2,
|
||||
"row_id": 3
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"idx": 5,
|
||||
"included_in_print_rate": 1,
|
||||
"parentfield": "taxes",
|
||||
"rate": 12.5
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Customs Duty - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Customs Duty",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"idx": 6,
|
||||
"parentfield": "taxes",
|
||||
"rate": 10
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Shipping Charges - _TC",
|
||||
"charge_type": "Actual",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Shipping Charges",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"idx": 7,
|
||||
"parentfield": "taxes",
|
||||
"tax_amount": 100
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Discount - _TC",
|
||||
"charge_type": "On Previous Row Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Discount",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"idx": 8,
|
||||
"parentfield": "taxes",
|
||||
"rate": -10,
|
||||
"row_id": 7
|
||||
}
|
||||
],
|
||||
"plc_conversion_rate": 1.0,
|
||||
"price_list_currency": "INR",
|
||||
"selling_price_list": "_Test Price List",
|
||||
"territory": "_Test Territory"
|
||||
}
|
||||
]
|
||||
@@ -1,377 +0,0 @@
|
||||
[["Sales Invoice"]]
|
||||
company = "_Test Company"
|
||||
conversion_rate = 1.0
|
||||
currency = "INR"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
customer = "_Test Customer"
|
||||
customer_name = "_Test Customer"
|
||||
debit_to = "Debtors - _TC"
|
||||
base_grand_total = 561.8
|
||||
grand_total = 561.8
|
||||
is_pos = 0
|
||||
naming_series = "T-SINV-"
|
||||
base_net_total = 500.0
|
||||
plc_conversion_rate = 1.0
|
||||
price_list_currency = "INR"
|
||||
selling_price_list = "_Test Price List"
|
||||
territory = "_Test Territory"
|
||||
[["Sales Invoice".items]]
|
||||
amount = 500.0
|
||||
base_amount = 500.0
|
||||
base_rate = 500.0
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "138-CMS Shoe"
|
||||
doctype = "Sales Invoice Item"
|
||||
income_account = "Sales - _TC"
|
||||
expense_account = "_Test Account Cost for Goods Sold - _TC"
|
||||
item_code = "138-CMS Shoe"
|
||||
item_name = "138-CMS Shoe"
|
||||
parentfield = "items"
|
||||
qty = 1.0
|
||||
rate = 500.0
|
||||
uom = "_Test UOM"
|
||||
conversion_factor = 1
|
||||
stock_uom = "_Test UOM"
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account VAT - _TC"
|
||||
charge_type = "On Net Total"
|
||||
description = "VAT"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
rate = 6
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account Service Tax - _TC"
|
||||
charge_type = "On Net Total"
|
||||
description = "Service Tax"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
rate = 6.36
|
||||
|
||||
[["Sales Invoice".sales_team]]
|
||||
allocated_percentage = 65.5
|
||||
doctype = "Sales Team"
|
||||
parentfield = "sales_team"
|
||||
sales_person = "_Test Sales Person 1"
|
||||
|
||||
[["Sales Invoice".sales_team]]
|
||||
allocated_percentage = 34.5
|
||||
doctype = "Sales Team"
|
||||
parentfield = "sales_team"
|
||||
sales_person = "_Test Sales Person 2"
|
||||
|
||||
|
||||
[["Sales Invoice"]]
|
||||
company = "_Test Company"
|
||||
conversion_rate = 1.0
|
||||
currency = "INR"
|
||||
customer = "_Test Customer"
|
||||
customer_name = "_Test Customer"
|
||||
debit_to = "Debtors - _TC"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
base_grand_total = 630.0
|
||||
grand_total = 630.0
|
||||
is_pos = 0
|
||||
naming_series = "T-SINV-"
|
||||
base_net_total = 500.0
|
||||
plc_conversion_rate = 1.0
|
||||
price_list_currency = "INR"
|
||||
selling_price_list = "_Test Price List"
|
||||
territory = "_Test Territory"
|
||||
[["Sales Invoice".items]]
|
||||
amount = 500.0
|
||||
base_amount = 500.0
|
||||
base_rate = 500.0
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "_Test Item"
|
||||
doctype = "Sales Invoice Item"
|
||||
expense_account = "_Test Account Cost for Goods Sold - _TC"
|
||||
income_account = "Sales - _TC"
|
||||
item_code = "_Test Item"
|
||||
item_name = "_Test Item"
|
||||
parentfield = "items"
|
||||
price_list_rate = 500.0
|
||||
qty = 1.0
|
||||
uom = "_Test UOM"
|
||||
conversion_factor = 1
|
||||
stock_uom = "_Test UOM"
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account VAT - _TC"
|
||||
charge_type = "On Net Total"
|
||||
description = "VAT"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
rate = 16
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account Service Tax - _TC"
|
||||
charge_type = "On Net Total"
|
||||
description = "Service Tax"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
rate = 10
|
||||
|
||||
|
||||
[["Sales Invoice"]]
|
||||
company = "_Test Company"
|
||||
conversion_rate = 1.0
|
||||
currency = "INR"
|
||||
customer = "_Test Customer"
|
||||
customer_name = "_Test Customer"
|
||||
debit_to = "Debtors - _TC"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
grand_total = 0
|
||||
is_pos = 0
|
||||
naming_series = "T-SINV-"
|
||||
plc_conversion_rate = 1.0
|
||||
price_list_currency = "INR"
|
||||
selling_price_list = "_Test Price List"
|
||||
territory = "_Test Territory"
|
||||
[["Sales Invoice".items]]
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
doctype = "Sales Invoice Item"
|
||||
income_account = "Sales - _TC"
|
||||
expense_account = "_Test Account Cost for Goods Sold - _TC"
|
||||
item_code = "_Test Item Home Desktop 100"
|
||||
item_name = "_Test Item Home Desktop 100"
|
||||
item_tax_template = "_Test Account Excise Duty @ 10 - _TC"
|
||||
parentfield = "items"
|
||||
price_list_rate = 50
|
||||
qty = 10
|
||||
rate = 50
|
||||
uom = "_Test UOM 1"
|
||||
conversion_factor = 1
|
||||
stock_uom = "_Test UOM 1"
|
||||
|
||||
[["Sales Invoice".items]]
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
doctype = "Sales Invoice Item"
|
||||
income_account = "Sales - _TC"
|
||||
expense_account = "_Test Account Cost for Goods Sold - _TC"
|
||||
item_code = "_Test Item Home Desktop 200"
|
||||
item_name = "_Test Item Home Desktop 200"
|
||||
parentfield = "items"
|
||||
price_list_rate = 150
|
||||
qty = 5
|
||||
uom = "_Test UOM"
|
||||
conversion_factor = 1
|
||||
rate = 150
|
||||
stock_uom = "_Test UOM"
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account Shipping Charges - _TC"
|
||||
charge_type = "Actual"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Shipping Charges"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
tax_amount = 100
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account Customs Duty - _TC"
|
||||
charge_type = "On Net Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Customs Duty"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 10
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account Excise Duty - _TC"
|
||||
charge_type = "On Net Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Excise Duty"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 12
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account Education Cess - _TC"
|
||||
charge_type = "On Previous Row Amount"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Education Cess"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 2
|
||||
row_id = 3
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account S&H Education Cess - _TC"
|
||||
charge_type = "On Previous Row Amount"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "S&H Education Cess"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 1
|
||||
row_id = 3
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account CST - _TC"
|
||||
charge_type = "On Previous Row Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "CST"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 2
|
||||
row_id = 5
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account VAT - _TC"
|
||||
charge_type = "On Net Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "VAT"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 12.5
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account Discount - _TC"
|
||||
charge_type = "On Previous Row Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Discount"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = -10
|
||||
row_id = 7
|
||||
|
||||
|
||||
[["Sales Invoice"]]
|
||||
company = "_Test Company"
|
||||
conversion_rate = 1.0
|
||||
currency = "INR"
|
||||
customer = "_Test Customer"
|
||||
customer_name = "_Test Customer"
|
||||
debit_to = "Debtors - _TC"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
grand_total = 0
|
||||
is_pos = 0
|
||||
naming_series = "T-SINV-"
|
||||
plc_conversion_rate = 1.0
|
||||
price_list_currency = "INR"
|
||||
selling_price_list = "_Test Price List"
|
||||
territory = "_Test Territory"
|
||||
[["Sales Invoice".items]]
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
doctype = "Sales Invoice Item"
|
||||
income_account = "Sales - _TC"
|
||||
expense_account = "_Test Account Cost for Goods Sold - _TC"
|
||||
item_code = "_Test Item Home Desktop 100"
|
||||
item_name = "_Test Item Home Desktop 100"
|
||||
item_tax_template = "_Test Account Excise Duty @ 10 - _TC"
|
||||
parentfield = "items"
|
||||
price_list_rate = 62.5
|
||||
qty = 10
|
||||
uom = "_Test UOM 1"
|
||||
conversion_factor = 1
|
||||
stock_uom = "_Test UOM 1"
|
||||
|
||||
[["Sales Invoice".items]]
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
doctype = "Sales Invoice Item"
|
||||
income_account = "Sales - _TC"
|
||||
expense_account = "_Test Account Cost for Goods Sold - _TC"
|
||||
item_code = "_Test Item Home Desktop 200"
|
||||
item_name = "_Test Item Home Desktop 200"
|
||||
parentfield = "items"
|
||||
price_list_rate = 190.66
|
||||
qty = 5
|
||||
uom = "_Test UOM"
|
||||
conversion_factor = 1
|
||||
stock_uom = "_Test UOM"
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account Excise Duty - _TC"
|
||||
charge_type = "On Net Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Excise Duty"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
idx = 1
|
||||
included_in_print_rate = 1
|
||||
parentfield = "taxes"
|
||||
rate = 12
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account Education Cess - _TC"
|
||||
charge_type = "On Previous Row Amount"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Education Cess"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
idx = 2
|
||||
included_in_print_rate = 1
|
||||
parentfield = "taxes"
|
||||
rate = 2
|
||||
row_id = 1
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account S&H Education Cess - _TC"
|
||||
charge_type = "On Previous Row Amount"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "S&H Education Cess"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
idx = 3
|
||||
included_in_print_rate = 1
|
||||
parentfield = "taxes"
|
||||
rate = 1
|
||||
row_id = 1
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account CST - _TC"
|
||||
charge_type = "On Previous Row Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "CST"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
idx = 4
|
||||
included_in_print_rate = 1
|
||||
parentfield = "taxes"
|
||||
rate = 2
|
||||
row_id = 3
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account VAT - _TC"
|
||||
charge_type = "On Net Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "VAT"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
idx = 5
|
||||
included_in_print_rate = 1
|
||||
parentfield = "taxes"
|
||||
rate = 12.5
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account Customs Duty - _TC"
|
||||
charge_type = "On Net Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Customs Duty"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
idx = 6
|
||||
parentfield = "taxes"
|
||||
rate = 10
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account Shipping Charges - _TC"
|
||||
charge_type = "Actual"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Shipping Charges"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
idx = 7
|
||||
parentfield = "taxes"
|
||||
tax_amount = 100
|
||||
|
||||
[["Sales Invoice".taxes]]
|
||||
account_head = "_Test Account Discount - _TC"
|
||||
charge_type = "On Previous Row Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Discount"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
idx = 8
|
||||
parentfield = "taxes"
|
||||
rate = -10
|
||||
row_id = 7
|
||||
|
||||
|
||||
@@ -1827,17 +1827,6 @@ class TestSalesInvoice(IntegrationTestCase):
|
||||
for field in expected_gle:
|
||||
self.assertEqual(expected_gle[field], gle[field])
|
||||
|
||||
def test_invoice_exchange_rate(self):
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=1,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
def test_invalid_currency(self):
|
||||
# Customer currency = USD
|
||||
|
||||
@@ -4305,6 +4294,31 @@ class TestSalesInvoice(IntegrationTestCase):
|
||||
doc = frappe.get_doc("Project", project.name)
|
||||
self.assertEqual(doc.total_billed_amount, si.grand_total)
|
||||
|
||||
def test_total_billed_amount_with_different_projects(self):
|
||||
# This test case is for checking the scenario where project is set at document level and for **some** child items only, not all
|
||||
from copy import copy
|
||||
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
|
||||
project = frappe.new_doc("Project")
|
||||
project.company = "_Test Company"
|
||||
project.project_name = "Test Total Billed Amount"
|
||||
project.save()
|
||||
|
||||
si.project = project.name
|
||||
si.items.append(copy(si.items[0]))
|
||||
si.items.append(copy(si.items[0]))
|
||||
si.items[0].project = project.name
|
||||
si.items[1].project = project.name
|
||||
# Not setting project on last item
|
||||
si.items[1].insert()
|
||||
si.items[2].insert()
|
||||
si.submit()
|
||||
|
||||
project.reload()
|
||||
self.assertIsNone(si.items[2].project)
|
||||
self.assertEqual(project.total_billed_amount, 300)
|
||||
|
||||
def test_pos_returns_with_party_account_currency(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
@@ -4329,6 +4343,49 @@ class TestSalesInvoice(IntegrationTestCase):
|
||||
pos_return = make_sales_return(pos.name)
|
||||
self.assertEqual(abs(pos_return.payments[0].amount), pos.payments[0].amount)
|
||||
|
||||
def test_create_return_invoice_for_self_update(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
invoice = create_sales_invoice()
|
||||
|
||||
payment_entry = get_payment_entry(dt=invoice.doctype, dn=invoice.name)
|
||||
payment_entry.reference_no = "test001"
|
||||
payment_entry.reference_date = getdate()
|
||||
|
||||
payment_entry.save()
|
||||
payment_entry.submit()
|
||||
|
||||
r_invoice = make_return_doc(invoice.doctype, invoice.name)
|
||||
|
||||
r_invoice.update_outstanding_for_self = 0
|
||||
r_invoice.save()
|
||||
|
||||
self.assertEqual(r_invoice.update_outstanding_for_self, 1)
|
||||
|
||||
r_invoice.submit()
|
||||
|
||||
self.assertNotEqual(r_invoice.outstanding_amount, 0)
|
||||
|
||||
invoice.reload()
|
||||
|
||||
self.assertEqual(invoice.outstanding_amount, 0)
|
||||
|
||||
def test_prevents_fully_returned_invoice_with_zero_quantity(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import StockOverReturnError, make_return_doc
|
||||
|
||||
invoice = create_sales_invoice(qty=10)
|
||||
|
||||
return_doc = make_return_doc(invoice.doctype, invoice.name)
|
||||
return_doc.items[0].qty = -10
|
||||
return_doc.save().submit()
|
||||
|
||||
return_doc = make_return_doc(invoice.doctype, invoice.name)
|
||||
return_doc.items[0].qty = 0
|
||||
|
||||
self.assertRaises(StockOverReturnError, return_doc.save)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -106,6 +106,9 @@
|
||||
"delivery_note",
|
||||
"dn_detail",
|
||||
"delivered_qty",
|
||||
"column_break_vwhb",
|
||||
"pos_invoice",
|
||||
"pos_invoice_item",
|
||||
"internal_transfer_section",
|
||||
"purchase_order",
|
||||
"column_break_92",
|
||||
@@ -631,6 +634,7 @@
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"label": "Serial No",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "serial_no",
|
||||
"oldfieldtype": "Small Text"
|
||||
},
|
||||
@@ -952,18 +956,42 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_invoice_item",
|
||||
"fieldtype": "Data",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "POS Invoice Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_vwhb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_invoice",
|
||||
"fieldtype": "Link",
|
||||
"label": "POS Invoice",
|
||||
"no_copy": 1,
|
||||
"options": "POS Invoice",
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-25 16:27:33.287341",
|
||||
"modified": "2025-03-07 10:25:30.275246",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
|
||||
@@ -71,6 +71,8 @@ class SalesInvoiceItem(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
pos_invoice: DF.Link | None
|
||||
pos_invoice_item: DF.Data | None
|
||||
price_list_rate: DF.Currency
|
||||
pricing_rules: DF.SmallText | None
|
||||
project: DF.Link | None
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
[
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"doctype": "Sales Taxes and Charges Template",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"cost_center": "Main - _TC",
|
||||
"parentfield": "taxes",
|
||||
"rate": 6
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"description": "Service Tax",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"cost_center": "Main - _TC",
|
||||
"parentfield": "taxes",
|
||||
"rate": 6.36
|
||||
}
|
||||
],
|
||||
"title": "_Test Sales Taxes and Charges Template"
|
||||
},
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"doctype": "Sales Taxes and Charges Template",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "_Test Account Shipping Charges - _TC",
|
||||
"charge_type": "Actual",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Shipping Charges",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"tax_amount": 100
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Customs Duty - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Customs Duty",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 10
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Excise Duty - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Excise Duty",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 12
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Education Cess - _TC",
|
||||
"charge_type": "On Previous Row Amount",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Education Cess",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 2,
|
||||
"row_id": 3
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account S&H Education Cess - _TC",
|
||||
"charge_type": "On Previous Row Amount",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "S&H Education Cess",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 1,
|
||||
"row_id": 3
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account CST - _TC",
|
||||
"charge_type": "On Previous Row Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "CST",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 2,
|
||||
"row_id": 5
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": 12.5
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Discount - _TC",
|
||||
"charge_type": "On Previous Row Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Discount",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"parentfield": "taxes",
|
||||
"rate": -10,
|
||||
"row_id": 7
|
||||
}
|
||||
],
|
||||
"title": "_Test India Tax Master"
|
||||
},
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"doctype": "Sales Taxes and Charges Template",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"cost_center": "Main - _TC",
|
||||
"parentfield": "taxes",
|
||||
"rate": 12
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"description": "Service Tax",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"cost_center": "Main - _TC",
|
||||
"parentfield": "taxes",
|
||||
"rate": 4
|
||||
}
|
||||
],
|
||||
"title": "_Test Sales Taxes and Charges Template - Rest of the World"
|
||||
},
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"doctype": "Sales Taxes and Charges Template",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"cost_center": "Main - _TC",
|
||||
"parentfield": "taxes",
|
||||
"rate": 12
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"description": "Service Tax",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"cost_center": "Main - _TC",
|
||||
"parentfield": "taxes",
|
||||
"rate": 4
|
||||
}
|
||||
],
|
||||
"title": "_Test Sales Taxes and Charges Template 1"
|
||||
},
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"doctype": "Sales Taxes and Charges Template",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"cost_center": "Main - _TC",
|
||||
"parentfield": "taxes",
|
||||
"rate": 12
|
||||
},
|
||||
{
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"description": "Service Tax",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"cost_center": "Main - _TC",
|
||||
"parentfield": "taxes",
|
||||
"rate": 4
|
||||
}
|
||||
],
|
||||
"title": "_Test Sales Taxes and Charges Template 2"
|
||||
},
|
||||
{
|
||||
"doctype" : "Sales Taxes and Charges Template",
|
||||
"title": "_Test Tax 1",
|
||||
"company": "_Test Company",
|
||||
"taxes":[{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Sales Expenses - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"description": "Test Shopping cart taxes with Tax Rule",
|
||||
"tax_amount": 1000
|
||||
}]
|
||||
},
|
||||
{
|
||||
"doctype" : "Sales Taxes and Charges Template",
|
||||
"title": "_Test Tax 2",
|
||||
"company": "_Test Company",
|
||||
"taxes":[{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Sales Expenses - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"description": "Test Shopping cart taxes with Tax Rule",
|
||||
"tax_amount": 200
|
||||
}]
|
||||
}
|
||||
]
|
||||
@@ -1,190 +0,0 @@
|
||||
[["Sales Taxes and Charges Template"]]
|
||||
company = "_Test Company"
|
||||
title = "_Test Sales Taxes and Charges Template"
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
account_head = "_Test Account VAT - _TC"
|
||||
charge_type = "On Net Total"
|
||||
description = "VAT"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
cost_center = "Main - _TC"
|
||||
parentfield = "taxes"
|
||||
rate = 6
|
||||
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
account_head = "_Test Account Service Tax - _TC"
|
||||
charge_type = "On Net Total"
|
||||
description = "Service Tax"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
cost_center = "Main - _TC"
|
||||
parentfield = "taxes"
|
||||
rate = 6.36
|
||||
|
||||
|
||||
[["Sales Taxes and Charges Template"]]
|
||||
company = "_Test Company"
|
||||
title = "_Test India Tax Master"
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
account_head = "_Test Account Shipping Charges - _TC"
|
||||
charge_type = "Actual"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Shipping Charges"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
tax_amount = 100
|
||||
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
account_head = "_Test Account Customs Duty - _TC"
|
||||
charge_type = "On Net Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Customs Duty"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 10
|
||||
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
account_head = "_Test Account Excise Duty - _TC"
|
||||
charge_type = "On Net Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Excise Duty"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 12
|
||||
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
account_head = "_Test Account Education Cess - _TC"
|
||||
charge_type = "On Previous Row Amount"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Education Cess"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 2
|
||||
row_id = 3
|
||||
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
account_head = "_Test Account S&H Education Cess - _TC"
|
||||
charge_type = "On Previous Row Amount"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "S&H Education Cess"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 1
|
||||
row_id = 3
|
||||
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
account_head = "_Test Account CST - _TC"
|
||||
charge_type = "On Previous Row Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "CST"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 2
|
||||
row_id = 5
|
||||
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
account_head = "_Test Account VAT - _TC"
|
||||
charge_type = "On Net Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "VAT"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = 12.5
|
||||
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
account_head = "_Test Account Discount - _TC"
|
||||
charge_type = "On Previous Row Total"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
description = "Discount"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
parentfield = "taxes"
|
||||
rate = -10
|
||||
row_id = 7
|
||||
|
||||
|
||||
[["Sales Taxes and Charges Template"]]
|
||||
company = "_Test Company"
|
||||
title = "_Test Sales Taxes and Charges Template - Rest of the World"
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
account_head = "_Test Account VAT - _TC"
|
||||
charge_type = "On Net Total"
|
||||
description = "VAT"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
cost_center = "Main - _TC"
|
||||
parentfield = "taxes"
|
||||
rate = 12
|
||||
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
account_head = "_Test Account Service Tax - _TC"
|
||||
charge_type = "On Net Total"
|
||||
description = "Service Tax"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
cost_center = "Main - _TC"
|
||||
parentfield = "taxes"
|
||||
rate = 4
|
||||
|
||||
|
||||
[["Sales Taxes and Charges Template"]]
|
||||
company = "_Test Company"
|
||||
title = "_Test Sales Taxes and Charges Template 1"
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
account_head = "_Test Account VAT - _TC"
|
||||
charge_type = "On Net Total"
|
||||
description = "VAT"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
cost_center = "Main - _TC"
|
||||
parentfield = "taxes"
|
||||
rate = 12
|
||||
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
account_head = "_Test Account Service Tax - _TC"
|
||||
charge_type = "On Net Total"
|
||||
description = "Service Tax"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
cost_center = "Main - _TC"
|
||||
parentfield = "taxes"
|
||||
rate = 4
|
||||
|
||||
|
||||
[["Sales Taxes and Charges Template"]]
|
||||
company = "_Test Company"
|
||||
title = "_Test Sales Taxes and Charges Template 2"
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
account_head = "_Test Account VAT - _TC"
|
||||
charge_type = "On Net Total"
|
||||
description = "VAT"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
cost_center = "Main - _TC"
|
||||
parentfield = "taxes"
|
||||
rate = 12
|
||||
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
account_head = "_Test Account Service Tax - _TC"
|
||||
charge_type = "On Net Total"
|
||||
description = "Service Tax"
|
||||
doctype = "Sales Taxes and Charges"
|
||||
cost_center = "Main - _TC"
|
||||
parentfield = "taxes"
|
||||
rate = 4
|
||||
|
||||
|
||||
[["Sales Taxes and Charges Template"]]
|
||||
title = "_Test Tax 1"
|
||||
company = "_Test Company"
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
charge_type = "Actual"
|
||||
account_head = "Sales Expenses - _TC"
|
||||
cost_center = "Main - _TC"
|
||||
description = "Test Shopping cart taxes with Tax Rule"
|
||||
tax_amount = 1000
|
||||
|
||||
|
||||
[["Sales Taxes and Charges Template"]]
|
||||
title = "_Test Tax 2"
|
||||
company = "_Test Company"
|
||||
[["Sales Taxes and Charges Template".taxes]]
|
||||
charge_type = "Actual"
|
||||
account_head = "Sales Expenses - _TC"
|
||||
cost_center = "Main - _TC"
|
||||
description = "Test Shopping cart taxes with Tax Rule"
|
||||
tax_amount = 200
|
||||
|
||||
|
||||
10
erpnext/accounts/doctype/share_type/test_records.json
Normal file
10
erpnext/accounts/doctype/share_type/test_records.json
Normal file
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"doctype": "Share Type",
|
||||
"title": "Class A"
|
||||
},
|
||||
{
|
||||
"doctype": "Share Type",
|
||||
"title": "Class B"
|
||||
}
|
||||
]
|
||||
@@ -1,6 +0,0 @@
|
||||
[["Share Type"]]
|
||||
title = "Class A"
|
||||
|
||||
[["Share Type"]]
|
||||
title = "Class B"
|
||||
|
||||
20
erpnext/accounts/doctype/shareholder/test_records.json
Normal file
20
erpnext/accounts/doctype/shareholder/test_records.json
Normal file
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"doctype": "Shareholder",
|
||||
"naming_series": "SH-",
|
||||
"title": "Iron Man",
|
||||
"company": "_Test Company"
|
||||
},
|
||||
{
|
||||
"doctype": "Shareholder",
|
||||
"naming_series": "SH-",
|
||||
"title": "Thor",
|
||||
"company": "_Test Company"
|
||||
},
|
||||
{
|
||||
"doctype": "Shareholder",
|
||||
"naming_series": "SH-",
|
||||
"title": "Hulk",
|
||||
"company": "_Test Company"
|
||||
}
|
||||
]
|
||||
@@ -1,15 +0,0 @@
|
||||
[[Shareholder]]
|
||||
naming_series = "SH-"
|
||||
title = "Iron Man"
|
||||
company = "_Test Company"
|
||||
|
||||
[[Shareholder]]
|
||||
naming_series = "SH-"
|
||||
title = "Thor"
|
||||
company = "_Test Company"
|
||||
|
||||
[[Shareholder]]
|
||||
naming_series = "SH-"
|
||||
title = "Hulk"
|
||||
company = "_Test Company"
|
||||
|
||||
108
erpnext/accounts/doctype/shipping_rule/test_records.json
Normal file
108
erpnext/accounts/doctype/shipping_rule/test_records.json
Normal file
@@ -0,0 +1,108 @@
|
||||
[
|
||||
{
|
||||
"account": "_Test Account Shipping Charges - _TC",
|
||||
"calculate_based_on": "Net Total",
|
||||
"company": "_Test Company",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"doctype": "Shipping Rule",
|
||||
"label": "_Test Shipping Rule",
|
||||
"name": "_Test Shipping Rule",
|
||||
"shipping_rule_type": "Selling",
|
||||
"conditions": [
|
||||
{
|
||||
"doctype": "Shipping Rule Condition",
|
||||
"from_value": 0,
|
||||
"parentfield": "conditions",
|
||||
"shipping_amount": 50.0,
|
||||
"to_value": 100
|
||||
},
|
||||
{
|
||||
"doctype": "Shipping Rule Condition",
|
||||
"from_value": 101,
|
||||
"parentfield": "conditions",
|
||||
"shipping_amount": 100.0,
|
||||
"to_value": 200
|
||||
},
|
||||
{
|
||||
"doctype": "Shipping Rule Condition",
|
||||
"from_value": 201,
|
||||
"parentfield": "conditions",
|
||||
"shipping_amount": 200.0
|
||||
}
|
||||
],
|
||||
"countries": [
|
||||
{"country": "India"}
|
||||
],
|
||||
"worldwide_shipping": 1
|
||||
},
|
||||
{
|
||||
"account": "_Test Account Shipping Charges - _TC",
|
||||
"calculate_based_on": "Net Total",
|
||||
"company": "_Test Company",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"doctype": "Shipping Rule",
|
||||
"label": "_Test Shipping Rule - India",
|
||||
"name": "_Test Shipping Rule - India",
|
||||
"conditions": [
|
||||
{
|
||||
"doctype": "Shipping Rule Condition",
|
||||
"from_value": 0,
|
||||
"parentfield": "conditions",
|
||||
"shipping_amount": 50.0,
|
||||
"to_value": 100
|
||||
},
|
||||
{
|
||||
"doctype": "Shipping Rule Condition",
|
||||
"from_value": 101,
|
||||
"parentfield": "conditions",
|
||||
"shipping_amount": 100.0,
|
||||
"to_value": 200
|
||||
},
|
||||
{
|
||||
"doctype": "Shipping Rule Condition",
|
||||
"from_value": 201,
|
||||
"parentfield": "conditions",
|
||||
"shipping_amount": 0.0
|
||||
}
|
||||
],
|
||||
"countries": [
|
||||
{"country": "India"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"account": "_Test Account Shipping Charges - _TC",
|
||||
"calculate_based_on": "Net Total",
|
||||
"company": "_Test Company",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"doctype": "Shipping Rule",
|
||||
"label": "_Test Shipping Rule - Rest of the World",
|
||||
"name": "_Test Shipping Rule - Rest of the World",
|
||||
"shipping_rule_type": "Buying",
|
||||
"conditions": [
|
||||
{
|
||||
"doctype": "Shipping Rule Condition",
|
||||
"from_value": 0,
|
||||
"parentfield": "conditions",
|
||||
"shipping_amount": 500.0,
|
||||
"to_value": 1000
|
||||
},
|
||||
{
|
||||
"doctype": "Shipping Rule Condition",
|
||||
"from_value": 1001,
|
||||
"parentfield": "conditions",
|
||||
"shipping_amount": 1000.0,
|
||||
"to_value": 2000
|
||||
},
|
||||
{
|
||||
"doctype": "Shipping Rule Condition",
|
||||
"from_value": 2001,
|
||||
"parentfield": "conditions",
|
||||
"shipping_amount": 1500.0
|
||||
}
|
||||
],
|
||||
"worldwide_shipping": 1,
|
||||
"countries": [
|
||||
{"country": "Germany"}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,97 +0,0 @@
|
||||
[["Shipping Rule"]]
|
||||
account = "_Test Account Shipping Charges - _TC"
|
||||
calculate_based_on = "Net Total"
|
||||
company = "_Test Company"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
label = "_Test Shipping Rule"
|
||||
name = "_Test Shipping Rule"
|
||||
shipping_rule_type = "Selling"
|
||||
worldwide_shipping = 1
|
||||
[["Shipping Rule".conditions]]
|
||||
doctype = "Shipping Rule Condition"
|
||||
from_value = 0
|
||||
parentfield = "conditions"
|
||||
shipping_amount = 50.0
|
||||
to_value = 100
|
||||
|
||||
[["Shipping Rule".conditions]]
|
||||
doctype = "Shipping Rule Condition"
|
||||
from_value = 101
|
||||
parentfield = "conditions"
|
||||
shipping_amount = 100.0
|
||||
to_value = 200
|
||||
|
||||
[["Shipping Rule".conditions]]
|
||||
doctype = "Shipping Rule Condition"
|
||||
from_value = 201
|
||||
parentfield = "conditions"
|
||||
shipping_amount = 200.0
|
||||
|
||||
[["Shipping Rule".countries]]
|
||||
country = "India"
|
||||
|
||||
|
||||
[["Shipping Rule"]]
|
||||
account = "_Test Account Shipping Charges - _TC"
|
||||
calculate_based_on = "Net Total"
|
||||
company = "_Test Company"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
label = "_Test Shipping Rule - India"
|
||||
name = "_Test Shipping Rule - India"
|
||||
[["Shipping Rule".conditions]]
|
||||
doctype = "Shipping Rule Condition"
|
||||
from_value = 0
|
||||
parentfield = "conditions"
|
||||
shipping_amount = 50.0
|
||||
to_value = 100
|
||||
|
||||
[["Shipping Rule".conditions]]
|
||||
doctype = "Shipping Rule Condition"
|
||||
from_value = 101
|
||||
parentfield = "conditions"
|
||||
shipping_amount = 100.0
|
||||
to_value = 200
|
||||
|
||||
[["Shipping Rule".conditions]]
|
||||
doctype = "Shipping Rule Condition"
|
||||
from_value = 201
|
||||
parentfield = "conditions"
|
||||
shipping_amount = 0.0
|
||||
|
||||
[["Shipping Rule".countries]]
|
||||
country = "India"
|
||||
|
||||
|
||||
[["Shipping Rule"]]
|
||||
account = "_Test Account Shipping Charges - _TC"
|
||||
calculate_based_on = "Net Total"
|
||||
company = "_Test Company"
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
label = "_Test Shipping Rule - Rest of the World"
|
||||
name = "_Test Shipping Rule - Rest of the World"
|
||||
shipping_rule_type = "Buying"
|
||||
worldwide_shipping = 1
|
||||
[["Shipping Rule".conditions]]
|
||||
doctype = "Shipping Rule Condition"
|
||||
from_value = 0
|
||||
parentfield = "conditions"
|
||||
shipping_amount = 500.0
|
||||
to_value = 1000
|
||||
|
||||
[["Shipping Rule".conditions]]
|
||||
doctype = "Shipping Rule Condition"
|
||||
from_value = 1001
|
||||
parentfield = "conditions"
|
||||
shipping_amount = 1000.0
|
||||
to_value = 2000
|
||||
|
||||
[["Shipping Rule".conditions]]
|
||||
doctype = "Shipping Rule Condition"
|
||||
from_value = 2001
|
||||
parentfield = "conditions"
|
||||
shipping_amount = 1500.0
|
||||
|
||||
[["Shipping Rule".countries]]
|
||||
country = "Germany"
|
||||
|
||||
|
||||
14
erpnext/accounts/doctype/tax_category/test_records.json
Normal file
14
erpnext/accounts/doctype/tax_category/test_records.json
Normal file
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"doctype": "Tax Category",
|
||||
"title": "_Test Tax Category 1"
|
||||
},
|
||||
{
|
||||
"doctype": "Tax Category",
|
||||
"title": "_Test Tax Category 2"
|
||||
},
|
||||
{
|
||||
"doctype": "Tax Category",
|
||||
"title": "_Test Tax Category 3"
|
||||
}
|
||||
]
|
||||
@@ -1,9 +0,0 @@
|
||||
[["Tax Category"]]
|
||||
title = "_Test Tax Category 1"
|
||||
|
||||
[["Tax Category"]]
|
||||
title = "_Test Tax Category 2"
|
||||
|
||||
[["Tax Category"]]
|
||||
title = "_Test Tax Category 3"
|
||||
|
||||
28
erpnext/accounts/doctype/tax_rule/test_records.json
Normal file
28
erpnext/accounts/doctype/tax_rule/test_records.json
Normal file
@@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"doctype": "Tax Rule",
|
||||
"tax_type" : "Sales",
|
||||
"sales_tax_template": "_Test Tax 1 - _TC",
|
||||
"use_for_shopping_cart": 1,
|
||||
"billing_city": "_Test City",
|
||||
"billing_state": "Test State",
|
||||
"billing_country": "India",
|
||||
"shipping_city": "_Test City",
|
||||
"shipping_country": "India",
|
||||
"priority": 1,
|
||||
"company": "_Test Company"
|
||||
},
|
||||
{
|
||||
"doctype": "Tax Rule",
|
||||
"tax_type" : "Sales",
|
||||
"sales_tax_template": "_Test Tax 2 - _TC",
|
||||
"use_for_shopping_cart": 0,
|
||||
"billing_city": "_Test City",
|
||||
"billing_country": "India",
|
||||
"shipping_city": "_Test City",
|
||||
"shipping_state": "Test State",
|
||||
"shipping_country": "India",
|
||||
"priority": 2,
|
||||
"company": "_Test Company"
|
||||
}
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
[["Tax Rule"]]
|
||||
tax_type = "Sales"
|
||||
sales_tax_template = "_Test Tax 1 - _TC"
|
||||
use_for_shopping_cart = 1
|
||||
billing_city = "_Test City"
|
||||
billing_state = "Test State"
|
||||
billing_country = "India"
|
||||
shipping_city = "_Test City"
|
||||
shipping_country = "India"
|
||||
priority = 1
|
||||
company = "_Test Company"
|
||||
|
||||
[["Tax Rule"]]
|
||||
tax_type = "Sales"
|
||||
sales_tax_template = "_Test Tax 2 - _TC"
|
||||
use_for_shopping_cart = 0
|
||||
billing_city = "_Test City"
|
||||
billing_country = "India"
|
||||
shipping_city = "_Test City"
|
||||
shipping_state = "Test State"
|
||||
shipping_country = "India"
|
||||
priority = 2
|
||||
company = "_Test Company"
|
||||
|
||||
@@ -13,17 +13,15 @@
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType"
|
||||
"label": "Voucher Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Name",
|
||||
"options": "voucher_type"
|
||||
"label": "Voucher Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "taxable_amount",
|
||||
@@ -36,7 +34,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:52.307012",
|
||||
"modified": "2025-02-05 16:39:14.863698",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withheld Vouchers",
|
||||
|
||||
@@ -18,8 +18,8 @@ class TaxWithheldVouchers(Document):
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
taxable_amount: DF.Currency
|
||||
voucher_name: DF.DynamicLink | None
|
||||
voucher_type: DF.Link | None
|
||||
voucher_name: DF.Data | None
|
||||
voucher_type: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
@@ -10,6 +10,7 @@ frappe.ui.form.on("Tax Withholding Category", {
|
||||
filters: {
|
||||
company: child.company,
|
||||
root_type: ["in", ["Asset", "Liability"]],
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,27 +36,38 @@ class TaxWithholdingCategory(Document):
|
||||
|
||||
def validate(self):
|
||||
self.validate_dates()
|
||||
self.validate_accounts()
|
||||
self.validate_companies_and_accounts()
|
||||
self.validate_thresholds()
|
||||
|
||||
def validate_dates(self):
|
||||
last_date = None
|
||||
for d in self.get("rates"):
|
||||
last_to_date = None
|
||||
rates = sorted(self.get("rates"), key=lambda d: getdate(d.from_date))
|
||||
|
||||
for d in rates:
|
||||
if getdate(d.from_date) >= getdate(d.to_date):
|
||||
frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx))
|
||||
|
||||
# validate overlapping of dates
|
||||
if last_date and getdate(d.to_date) < getdate(last_date):
|
||||
if last_to_date and getdate(d.from_date) < getdate(last_to_date):
|
||||
frappe.throw(_("Row #{0}: Dates overlapping with other row").format(d.idx))
|
||||
|
||||
def validate_accounts(self):
|
||||
existing_accounts = []
|
||||
last_to_date = d.to_date
|
||||
|
||||
def validate_companies_and_accounts(self):
|
||||
existing_accounts = set()
|
||||
companies = set()
|
||||
for d in self.get("accounts"):
|
||||
# validate duplicate company
|
||||
if d.get("company") in companies:
|
||||
frappe.throw(_("Company {0} added multiple times").format(frappe.bold(d.get("company"))))
|
||||
companies.add(d.get("company"))
|
||||
|
||||
# validate duplicate account
|
||||
if d.get("account") in existing_accounts:
|
||||
frappe.throw(_("Account {0} added multiple times").format(frappe.bold(d.get("account"))))
|
||||
|
||||
validate_account_head(d.idx, d.get("account"), d.get("company"))
|
||||
existing_accounts.append(d.get("account"))
|
||||
existing_accounts.add(d.get("account"))
|
||||
|
||||
def validate_thresholds(self):
|
||||
for d in self.get("rates"):
|
||||
@@ -436,6 +447,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
tax_details.get("tax_withholding_category"),
|
||||
company,
|
||||
),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for d in journal_entries_details:
|
||||
|
||||
@@ -529,7 +529,7 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
payment = get_payment_entry(order.doctype, order.name)
|
||||
payment.apply_tax_withholding_amount = 1
|
||||
payment.tax_withholding_category = "Cumulative Threshold TDS"
|
||||
payment.submit()
|
||||
payment.save().submit()
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 4000)
|
||||
|
||||
def test_multi_category_single_supplier(self):
|
||||
|
||||
@@ -82,6 +82,10 @@ def make_acc_dimensions_offsetting_entry(gl_map):
|
||||
"credit_in_account_currency": credit,
|
||||
"remarks": _("Offsetting for Accounting Dimension") + f" - {dimension.name}",
|
||||
"against_voucher": None,
|
||||
"account_currency": dimension.account_currency,
|
||||
# Party Type and Party are restricted to Receivable and Payable accounts
|
||||
"party_type": None,
|
||||
"party": None,
|
||||
}
|
||||
)
|
||||
offsetting_entry["against_voucher_type"] = None
|
||||
@@ -109,6 +113,9 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map, company):
|
||||
accounting_dimensions_to_offset = []
|
||||
for acc_dimension in acc_dimensions:
|
||||
values = set([entry.get(acc_dimension.fieldname) for entry in gl_map])
|
||||
acc_dimension.account_currency = frappe.get_cached_value(
|
||||
"Account", acc_dimension.offsetting_account, "account_currency"
|
||||
)
|
||||
if len(values) > 1:
|
||||
accounting_dimensions_to_offset.append(acc_dimension)
|
||||
|
||||
@@ -431,7 +438,7 @@ def process_debit_credit_difference(gl_map):
|
||||
voucher_no = gl_map[0].voucher_no
|
||||
allowance = get_debit_credit_allowance(voucher_type, precision)
|
||||
|
||||
debit_credit_diff = get_debit_credit_difference(gl_map, precision)
|
||||
debit_credit_diff, trx_cur_debit_credit_diff = get_debit_credit_difference(gl_map, precision)
|
||||
|
||||
if abs(debit_credit_diff) > allowance:
|
||||
if not (
|
||||
@@ -442,9 +449,9 @@ def process_debit_credit_difference(gl_map):
|
||||
raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
|
||||
|
||||
elif abs(debit_credit_diff) >= (1.0 / (10**precision)):
|
||||
make_round_off_gle(gl_map, debit_credit_diff, precision)
|
||||
make_round_off_gle(gl_map, debit_credit_diff, trx_cur_debit_credit_diff, precision)
|
||||
|
||||
debit_credit_diff = get_debit_credit_difference(gl_map, precision)
|
||||
debit_credit_diff, trx_cur_debit_credit_diff = get_debit_credit_difference(gl_map, precision)
|
||||
if abs(debit_credit_diff) > allowance:
|
||||
if not (
|
||||
voucher_type == "Journal Entry"
|
||||
@@ -456,14 +463,23 @@ def process_debit_credit_difference(gl_map):
|
||||
|
||||
def get_debit_credit_difference(gl_map, precision):
|
||||
debit_credit_diff = 0.0
|
||||
trx_cur_debit_credit_diff = 0
|
||||
|
||||
for entry in gl_map:
|
||||
entry.debit = flt(entry.debit, precision)
|
||||
entry.credit = flt(entry.credit, precision)
|
||||
debit_credit_diff += entry.debit - entry.credit
|
||||
|
||||
debit_credit_diff = flt(debit_credit_diff, precision)
|
||||
entry.debit_in_transaction_currency = flt(entry.debit_in_transaction_currency, precision)
|
||||
entry.credit_in_transaction_currency = flt(entry.credit_in_transaction_currency, precision)
|
||||
trx_cur_debit_credit_diff += (
|
||||
entry.debit_in_transaction_currency - entry.credit_in_transaction_currency
|
||||
)
|
||||
|
||||
return debit_credit_diff
|
||||
debit_credit_diff = flt(debit_credit_diff, precision)
|
||||
trx_cur_debit_credit_diff = flt(trx_cur_debit_credit_diff, precision)
|
||||
|
||||
return debit_credit_diff, trx_cur_debit_credit_diff
|
||||
|
||||
|
||||
def get_debit_credit_allowance(voucher_type, precision):
|
||||
@@ -490,7 +506,7 @@ def has_opening_entries(gl_map: list) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
def make_round_off_gle(gl_map, debit_credit_diff, trx_cur_debit_credit_diff, precision):
|
||||
round_off_account, round_off_cost_center, round_off_for_opening = get_round_off_account_and_cost_center(
|
||||
gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
|
||||
)
|
||||
@@ -535,6 +551,12 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
"credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0,
|
||||
"debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
|
||||
"credit": debit_credit_diff if debit_credit_diff > 0 else 0,
|
||||
"debit_in_transaction_currency": abs(trx_cur_debit_credit_diff)
|
||||
if trx_cur_debit_credit_diff < 0
|
||||
else 0,
|
||||
"credit_in_transaction_currency": trx_cur_debit_credit_diff
|
||||
if trx_cur_debit_credit_diff > 0
|
||||
else 0,
|
||||
"cost_center": round_off_cost_center,
|
||||
"party_type": None,
|
||||
"party": None,
|
||||
|
||||
@@ -280,32 +280,50 @@ def get_regional_address_details(party_details, doctype, company):
|
||||
|
||||
|
||||
def complete_contact_details(party_details):
|
||||
if not party_details.contact_person:
|
||||
party_details.update(
|
||||
{
|
||||
"contact_person": None,
|
||||
"contact_display": None,
|
||||
"contact_email": None,
|
||||
"contact_mobile": None,
|
||||
"contact_phone": None,
|
||||
"contact_designation": None,
|
||||
"contact_department": None,
|
||||
}
|
||||
contact_details = frappe._dict()
|
||||
|
||||
if party_details.party_type == "Employee":
|
||||
contact_details = frappe.db.get_value(
|
||||
"Employee",
|
||||
party_details.party,
|
||||
[
|
||||
"employee_name as contact_display",
|
||||
"prefered_email as contact_email",
|
||||
"cell_number as contact_mobile",
|
||||
"designation as contact_designation",
|
||||
"department as contact_department",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
contact_details.update({"contact_person": None, "contact_phone": None})
|
||||
elif party_details.contact_person:
|
||||
contact_details = frappe.db.get_value(
|
||||
"Contact",
|
||||
party_details.contact_person,
|
||||
[
|
||||
"name as contact_person",
|
||||
"full_name as contact_display",
|
||||
"email_id as contact_email",
|
||||
"mobile_no as contact_mobile",
|
||||
"phone as contact_phone",
|
||||
"designation as contact_designation",
|
||||
"department as contact_department",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
else:
|
||||
fields = [
|
||||
"name as contact_person",
|
||||
"full_name as contact_display",
|
||||
"email_id as contact_email",
|
||||
"mobile_no as contact_mobile",
|
||||
"phone as contact_phone",
|
||||
"designation as contact_designation",
|
||||
"department as contact_department",
|
||||
]
|
||||
contact_details = {
|
||||
"contact_person": None,
|
||||
"contact_display": None,
|
||||
"contact_email": None,
|
||||
"contact_mobile": None,
|
||||
"contact_phone": None,
|
||||
"contact_designation": None,
|
||||
"contact_department": None,
|
||||
}
|
||||
|
||||
contact_details = frappe.db.get_value("Contact", party_details.contact_person, fields, as_dict=True)
|
||||
|
||||
party_details.update(contact_details)
|
||||
party_details.update(contact_details)
|
||||
|
||||
|
||||
def set_contact_details(party_details, party, party_type):
|
||||
@@ -577,12 +595,13 @@ def validate_party_accounts(doc):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_due_date(posting_date, party_type, party, company=None, bill_date=None):
|
||||
def get_due_date(posting_date, party_type, party, company=None, bill_date=None, template_name=None):
|
||||
"""Get due date from `Payment Terms Template`"""
|
||||
due_date = None
|
||||
if (bill_date or posting_date) and party:
|
||||
due_date = bill_date or posting_date
|
||||
template_name = get_payment_terms_template(party, party_type, company)
|
||||
if not template_name:
|
||||
template_name = get_payment_terms_template(party, party_type, company)
|
||||
|
||||
if template_name:
|
||||
due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d")
|
||||
@@ -779,9 +798,9 @@ def validate_account_party_type(self):
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
if account_type and (account_type not in ["Receivable", "Payable", "Equity"]):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Party Type and Party can only be set for Receivable / Payable account<br><br>" "{0}"
|
||||
).format(self.account)
|
||||
_("Party Type and Party can only be set for Receivable / Payable account<br><br>{0}").format(
|
||||
self.account
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None, print_heading_template=None) -%}
|
||||
{% if letter_head and not no_letterhead %}
|
||||
<div class="letter-head">{{ letter_head }}</div>
|
||||
{% endif %}
|
||||
{% if print_heading_template %}
|
||||
{{ frappe.render_template(print_heading_template, {"doc":doc}) }}
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{%- if doc.meta.is_submittable and doc.docstatus==2-%}
|
||||
<div class="text-center" document-status="cancelled">
|
||||
<h4 style="margin: 0px;">{{ _("CANCELLED") }}</h4>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{%- endmacro -%}
|
||||
{% for page in layout %}
|
||||
<div class="page-break">
|
||||
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
|
||||
{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings) }}
|
||||
</div>
|
||||
<style>
|
||||
.taxes-section .order-taxes.mt-5{
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
.taxes-section .order-taxes .border-btm.pb-5{
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
.print-format label{
|
||||
color: #74808b;
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
{% if print_settings.repeat_header_footer %}
|
||||
<div id="footer-html" class="visible-pdf">
|
||||
{% if not no_letterhead and footer %}
|
||||
<div class="letter-head-footer">
|
||||
{{ footer }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="text-center small page-number visible-pdf">
|
||||
{{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row section-break" style="margin-bottom: 10px;">
|
||||
<div class="col-xs-6 p-0">
|
||||
<div class="col-xs-12 value text-uppercase"><b>{{ doc.customer }}</b></div>
|
||||
<div class="col-xs-12">
|
||||
{{ doc.address_display }}
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
{{ _("Contact: ")+doc.contact_display if doc.contact_display else '' }}
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
{{ _("Mobile: ")+doc.contact_mobile if doc.contact_mobile else '' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-3"></div>
|
||||
<div class="col-xs-3" style="padding-left: 5px;">
|
||||
<div>
|
||||
<div><label>{{ _("Invoice ID") }}</label></div>
|
||||
<div>{{ doc.name }}</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div><label>{{ _("Invoice Date") }}</label></div>
|
||||
<div>{{ frappe.utils.format_date(doc.posting_date) }}</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div><label>{{ _("Due Date") }}</label></div>
|
||||
<div>{{ frappe.utils.format_date(doc.due_date) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-break">
|
||||
<table class="table table-bordered table-condensed mb-0" style="width: 100%; border-collapse: collapse; font-size: 12px;">
|
||||
<colgroup>
|
||||
<col style="width: 5%">
|
||||
<col style="width: 45%">
|
||||
<col style="width: 10%">
|
||||
<col style="width: 20%">
|
||||
<col style="width: 20%">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-uppercase" style="text-align:center">{{ _("Sr") }}</th>
|
||||
<th class="text-uppercase" style="text-align:center">{{ _("Details") }}</th>
|
||||
<th class="text-uppercase" style="text-align:center">{{ _("Qty") }}</th>
|
||||
<th class="text-uppercase" style="text-align:right">{{ _("Rate") }}</th>
|
||||
<th class="text-uppercase" style="text-align:right">{{ _("Amount") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for item in doc.items %}
|
||||
<tr>
|
||||
<td style="text-align:center">{{ loop.index }}</td>
|
||||
<td>
|
||||
<b>{{ item.item_code }}: {{ item.item_name }}</b>
|
||||
{% if (item.description != item.item_name) %}
|
||||
<br>{{ item.description }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align: center;">
|
||||
{{ item.get_formatted("qty", 0) }}
|
||||
{{ item.get_formatted("uom", 0) }}
|
||||
</td>
|
||||
<td style="text-align: right;">{{ item.get_formatted("net_rate", doc) }}</td>
|
||||
<td style="text-align: right;">{{ item.get_formatted("net_amount", doc) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<!-- total -->
|
||||
<div class="row">
|
||||
|
||||
<div class="col-xs-6">
|
||||
<div>
|
||||
<label>{{ _("Amount in Words") }}</label>
|
||||
{{ doc.in_words }}
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<label>{{ _("Payment Status") }}</label>
|
||||
{{ doc.status }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<div class="row section-break">
|
||||
<div class="col-xs-7"><div>{{ _("Sub Total") }}</div></div>
|
||||
<div class="col-xs-5" style="text-align: right;">{{ doc.get_formatted("net_total", doc) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
{% for d in doc.taxes %}
|
||||
{% if d.tax_amount %}
|
||||
<div class="row">
|
||||
<div class="col-xs-8"><div>{{ _(d.description) }}</div></div>
|
||||
<div class="col-xs-4" style="text-align: right;">{{ d.get_formatted("tax_amount") }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-7"><div>{{ _("Total") }}</div></div>
|
||||
<div class="col-xs-5" style="text-align: right;">{{ doc.get_formatted("grand_total", doc) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="row important data-field">
|
||||
<div class="col-xs-12"><label>{{ _("Terms and Conditions") }}: </label></div>
|
||||
<div class="col-xs-12">{{ doc.terms if doc.terms else '' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"absolute_value": 0,
|
||||
"align_labels_right": 0,
|
||||
"creation": "2025-01-22 16:23:51.012200",
|
||||
"css": "",
|
||||
"custom_format": 0,
|
||||
"default_print_language": "en",
|
||||
"disabled": 0,
|
||||
"doc_type": "Sales Invoice",
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"font": "",
|
||||
"font_size": 14,
|
||||
"idx": 0,
|
||||
"line_breaks": 0,
|
||||
"margin_bottom": 0.0,
|
||||
"margin_left": 0.0,
|
||||
"margin_right": 0.0,
|
||||
"margin_top": 0.0,
|
||||
"modified": "2025-01-22 16:23:51.012200",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Print",
|
||||
"owner": "Administrator",
|
||||
"page_number": "Hide",
|
||||
"print_format_builder": 0,
|
||||
"print_format_builder_beta": 0,
|
||||
"print_format_type": "Jinja",
|
||||
"raw_printing": 0,
|
||||
"show_section_headings": 0,
|
||||
"standard": "Yes"
|
||||
}
|
||||
@@ -164,7 +164,7 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
},
|
||||
};
|
||||
|
||||
erpnext.utils.add_dimensions("Accounts Payable", 9);
|
||||
erpnext.utils.add_dimensions("Accounts Payable", 10);
|
||||
|
||||
function get_party_type_options() {
|
||||
let options = [];
|
||||
|
||||
@@ -282,4 +282,4 @@
|
||||
{% } %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-right text-muted">{{ __("Printed On ") }}{%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
|
||||
<p class="text-right text-muted">{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}</p>
|
||||
|
||||
@@ -517,10 +517,10 @@ class ReceivablePayableReport:
|
||||
select
|
||||
si.name, si.party_account_currency, si.currency, si.conversion_rate,
|
||||
si.total_advance, ps.due_date, ps.payment_term, ps.payment_amount, ps.base_payment_amount,
|
||||
ps.description, ps.paid_amount, ps.discounted_amount
|
||||
ps.description, ps.paid_amount, ps.base_paid_amount, ps.discounted_amount
|
||||
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
|
||||
where
|
||||
si.name = ps.parent and
|
||||
si.name = ps.parent and ps.parenttype = '{row.voucher_type}' and
|
||||
si.name = %s and
|
||||
si.is_return = 0
|
||||
order by ps.paid_amount desc, due_date
|
||||
@@ -540,20 +540,24 @@ class ReceivablePayableReport:
|
||||
# Deduct that from paid amount pre allocation
|
||||
row.paid -= flt(payment_terms_details[0].total_advance)
|
||||
|
||||
company_currency = frappe.get_value("Company", self.filters.get("company"), "default_currency")
|
||||
|
||||
# If single payment terms, no need to split the row
|
||||
if len(payment_terms_details) == 1 and payment_terms_details[0].payment_term:
|
||||
self.append_payment_term(row, payment_terms_details[0], original_row)
|
||||
self.append_payment_term(row, payment_terms_details[0], original_row, company_currency)
|
||||
return
|
||||
|
||||
for d in payment_terms_details:
|
||||
term = frappe._dict(original_row)
|
||||
self.append_payment_term(row, d, term)
|
||||
self.append_payment_term(row, d, term, company_currency)
|
||||
|
||||
def append_payment_term(self, row, d, term):
|
||||
if d.currency == d.party_account_currency:
|
||||
def append_payment_term(self, row, d, term, company_currency):
|
||||
invoiced = d.base_payment_amount
|
||||
paid_amount = d.base_paid_amount
|
||||
|
||||
if company_currency == d.party_account_currency or self.filters.get("in_party_currency"):
|
||||
invoiced = d.payment_amount
|
||||
else:
|
||||
invoiced = d.base_payment_amount
|
||||
paid_amount = d.paid_amount
|
||||
|
||||
row.payment_terms.append(
|
||||
term.update(
|
||||
@@ -562,15 +566,15 @@ class ReceivablePayableReport:
|
||||
"invoiced": invoiced,
|
||||
"invoice_grand_total": row.invoiced,
|
||||
"payment_term": d.description or d.payment_term,
|
||||
"paid": d.paid_amount + d.discounted_amount,
|
||||
"paid": paid_amount + d.discounted_amount,
|
||||
"credit_note": 0.0,
|
||||
"outstanding": invoiced - d.paid_amount - d.discounted_amount,
|
||||
"outstanding": invoiced - paid_amount - d.discounted_amount,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if d.paid_amount:
|
||||
row["paid"] -= d.paid_amount + d.discounted_amount
|
||||
if paid_amount:
|
||||
row["paid"] -= paid_amount + d.discounted_amount
|
||||
|
||||
def allocate_closing_to_term(self, row, term, key):
|
||||
if row[key]:
|
||||
@@ -729,11 +733,13 @@ class ReceivablePayableReport:
|
||||
"company": self.filters.company,
|
||||
"update_outstanding_for_self": 0,
|
||||
}
|
||||
|
||||
or_filters = {}
|
||||
for party_type in self.party_type:
|
||||
if party_type := self.filters.party_type:
|
||||
party_field = scrub(party_type)
|
||||
if self.filters.get(party_field):
|
||||
or_filters.update({party_field: self.filters.get(party_field)})
|
||||
if parties := self.filters.get("party"):
|
||||
or_filters.update({party_field: ["in", parties]})
|
||||
|
||||
self.return_entries = frappe._dict(
|
||||
frappe.get_all(
|
||||
doctype, filters=filters, or_filters=or_filters, fields=["name", "return_against"], as_list=1
|
||||
|
||||
@@ -21,7 +21,7 @@ class TestAccountsReceivable(AccountsTestMixin, IntegrationTestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
|
||||
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False, **args):
|
||||
frappe.set_user("Administrator")
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
@@ -34,6 +34,7 @@ class TestAccountsReceivable(AccountsTestMixin, IntegrationTestCase):
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_save=1,
|
||||
**args,
|
||||
)
|
||||
if not no_payment_schedule:
|
||||
si.append(
|
||||
@@ -108,7 +109,7 @@ class TestAccountsReceivable(AccountsTestMixin, IntegrationTestCase):
|
||||
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
|
||||
pos_inv.cancel()
|
||||
|
||||
def test_accounts_receivable(self):
|
||||
def test_accounts_receivable_with_payment(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
@@ -145,11 +146,15 @@ class TestAccountsReceivable(AccountsTestMixin, IntegrationTestCase):
|
||||
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||
cr_note.update_outstanding_for_self = False
|
||||
cr_note.save().submit()
|
||||
|
||||
# as the invoice partially paid and returning the full amount so the outstanding amount should be True
|
||||
self.assertEqual(cr_note.update_outstanding_for_self, True)
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
|
||||
expected_data_after_credit_note = [0, 0, 100, 0, -100, self.debit_to]
|
||||
|
||||
row = report[1][0]
|
||||
row = report[1][-1]
|
||||
self.assertEqual(
|
||||
expected_data_after_credit_note,
|
||||
[
|
||||
@@ -162,6 +167,99 @@ class TestAccountsReceivable(AccountsTestMixin, IntegrationTestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_accounts_receivable_without_payment(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
"show_remarks": True,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice()
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||
cr_note.update_outstanding_for_self = False
|
||||
cr_note.save().submit()
|
||||
|
||||
self.assertEqual(cr_note.update_outstanding_for_self, False)
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
row = report[1]
|
||||
self.assertTrue(len(row) == 0)
|
||||
|
||||
def test_accounts_receivable_with_partial_payment(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
"show_remarks": True,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice(qty=2)
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [[200, 60, "No Remarks"], [200, 100, "No Remarks"], [200, 40, "No Remarks"]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||
self.create_payment_entry(si.name)
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [[200, 60, 40, 20], [200, 100, 0, 100], [200, 40, 0, 40]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(
|
||||
expected_data_after_payment[i - 1],
|
||||
[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
|
||||
)
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||
cr_note.update_outstanding_for_self = False
|
||||
cr_note.save().submit()
|
||||
|
||||
self.assertFalse(cr_note.update_outstanding_for_self)
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_credit_note = [
|
||||
[200, 100, 0, 80, 20, self.debit_to],
|
||||
[200, 40, 0, 0, 40, self.debit_to],
|
||||
]
|
||||
|
||||
for i in range(2):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(
|
||||
expected_data_after_credit_note[i - 1],
|
||||
[
|
||||
row.invoice_grand_total,
|
||||
row.invoiced,
|
||||
row.paid,
|
||||
row.credit_note,
|
||||
row.outstanding,
|
||||
row.party_account,
|
||||
],
|
||||
)
|
||||
|
||||
def test_cr_note_flag_to_update_self(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
|
||||
@@ -50,6 +50,7 @@ def get_group_by_asset_category_data(filters):
|
||||
flt(row.accumulated_depreciation_as_on_from_date)
|
||||
+ flt(row.depreciation_amount_during_the_period)
|
||||
- flt(row.depreciation_eliminated_during_the_period)
|
||||
- flt(row.depreciation_eliminated_via_reversal)
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
||||
@@ -144,6 +145,130 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
)
|
||||
|
||||
|
||||
def get_assets_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition = f" and a.asset_category = '{filters.get('asset_category')}'"
|
||||
finance_book_filter = ""
|
||||
if filters.get("finance_book"):
|
||||
finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s"
|
||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT results.asset_category,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.asset_category,
|
||||
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
|
||||
gle.credit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
|
||||
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_amount_during_the_period
|
||||
from `tabGL Entry` gle
|
||||
join `tabAsset` a on
|
||||
gle.against_voucher = a.name
|
||||
join `tabAsset Category Account` aca on
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{condition} {finance_book_filter}
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then
|
||||
0
|
||||
else
|
||||
a.opening_accumulated_depreciation
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
0 as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
|
||||
a.opening_accumulated_depreciation
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
0 as depreciation_amount_during_the_period
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
group by a.asset_category) as results
|
||||
group by results.asset_category
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"finance_book": filters.get("finance_book", ""),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_group_by_asset_data(filters):
|
||||
data = []
|
||||
|
||||
asset_details = get_asset_details_for_grouped_by_category(filters)
|
||||
assets = get_assets_for_grouped_by_asset(filters)
|
||||
|
||||
for asset_detail in asset_details:
|
||||
row = frappe._dict()
|
||||
row.update(asset_detail)
|
||||
|
||||
row.value_as_on_to_date = (
|
||||
flt(row.value_as_on_from_date)
|
||||
+ flt(row.value_of_new_purchase)
|
||||
- flt(row.value_of_sold_asset)
|
||||
- flt(row.value_of_scrapped_asset)
|
||||
- flt(row.value_of_capitalized_asset)
|
||||
)
|
||||
|
||||
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
|
||||
|
||||
row.accumulated_depreciation_as_on_to_date = (
|
||||
flt(row.accumulated_depreciation_as_on_from_date)
|
||||
+ flt(row.depreciation_amount_during_the_period)
|
||||
- flt(row.depreciation_eliminated_during_the_period)
|
||||
- flt(row.depreciation_eliminated_via_reversal)
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
||||
row.accumulated_depreciation_as_on_from_date
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
|
||||
row.accumulated_depreciation_as_on_to_date
|
||||
)
|
||||
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_asset_details_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
@@ -223,123 +348,6 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
)
|
||||
|
||||
|
||||
def get_group_by_asset_data(filters):
|
||||
data = []
|
||||
|
||||
asset_details = get_asset_details_for_grouped_by_category(filters)
|
||||
assets = get_assets_for_grouped_by_asset(filters)
|
||||
|
||||
for asset_detail in asset_details:
|
||||
row = frappe._dict()
|
||||
row.update(asset_detail)
|
||||
|
||||
row.value_as_on_to_date = (
|
||||
flt(row.value_as_on_from_date)
|
||||
+ flt(row.value_of_new_purchase)
|
||||
- flt(row.value_of_sold_asset)
|
||||
- flt(row.value_of_scrapped_asset)
|
||||
- flt(row.value_of_capitalized_asset)
|
||||
)
|
||||
|
||||
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
|
||||
|
||||
row.accumulated_depreciation_as_on_to_date = (
|
||||
flt(row.accumulated_depreciation_as_on_from_date)
|
||||
+ flt(row.depreciation_amount_during_the_period)
|
||||
- flt(row.depreciation_eliminated_during_the_period)
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
||||
row.accumulated_depreciation_as_on_from_date
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
|
||||
row.accumulated_depreciation_as_on_to_date
|
||||
)
|
||||
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_assets_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition = f" and a.asset_category = '{filters.get('asset_category')}'"
|
||||
finance_book_filter = ""
|
||||
if filters.get("finance_book"):
|
||||
finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s"
|
||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT results.asset_category,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.asset_category,
|
||||
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
|
||||
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_amount_during_the_period
|
||||
from `tabGL Entry` gle
|
||||
join `tabAsset` a on
|
||||
gle.against_voucher = a.name
|
||||
join `tabAsset Category Account` aca on
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.debit != 0
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{condition} {finance_book_filter}
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
|
||||
0
|
||||
else
|
||||
a.opening_accumulated_depreciation
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
|
||||
a.opening_accumulated_depreciation
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
0 as depreciation_amount_during_the_period
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
group by a.asset_category) as results
|
||||
group by results.asset_category
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"finance_book": filters.get("finance_book", ""),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_assets_for_grouped_by_asset(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
@@ -354,6 +362,7 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
f"""
|
||||
SELECT results.name as asset,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.name as name,
|
||||
@@ -362,6 +371,11 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
|
||||
gle.credit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||
gle.debit
|
||||
@@ -385,18 +399,18 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.debit != 0
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{finance_book_filter} {condition}
|
||||
group by a.name
|
||||
union
|
||||
SELECT a.name as name,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then
|
||||
0
|
||||
else
|
||||
a.opening_accumulated_depreciation
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
0 as depreciation_as_on_from_date_credit,
|
||||
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
|
||||
a.opening_accumulated_depreciation
|
||||
else
|
||||
@@ -503,6 +517,12 @@ def get_columns(filters):
|
||||
"fieldtype": "Currency",
|
||||
"width": 270,
|
||||
},
|
||||
{
|
||||
"label": _("Depreciation eliminated via reversal"),
|
||||
"fieldname": "depreciation_eliminated_via_reversal",
|
||||
"fieldtype": "Currency",
|
||||
"width": 270,
|
||||
},
|
||||
{
|
||||
"label": _("Net Asset value as on") + " " + formatdate(filters.day_before_from_date),
|
||||
"fieldname": "net_asset_value_as_on_from_date",
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
<div style="margin-bottom: 7px;">
|
||||
{%= frappe.boot.letter_heads[frappe.defaults.get_default("letter_head")] %}
|
||||
</div>
|
||||
<h2 class="text-center">{%= __("Bank Reconciliation Statement") %}</h2>
|
||||
<h4 class="text-center">{%= filters.account && (filters.account + ", "+filters.report_date) || "" %} {%= filters.company %}</h4>
|
||||
<hr>
|
||||
@@ -46,4 +43,4 @@
|
||||
{% } %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-right text-muted">Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
|
||||
<p class="text-right text-muted">{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}</p>
|
||||
|
||||
@@ -27,6 +27,7 @@ def get_report_filters(report_filters):
|
||||
["Purchase Invoice", "docstatus", "=", 1],
|
||||
["Purchase Invoice", "per_received", "<", 100],
|
||||
["Purchase Invoice", "update_stock", "=", 0],
|
||||
["Purchase Invoice", "is_opening", "!=", "Yes"],
|
||||
]
|
||||
|
||||
if report_filters.get("purchase_invoice"):
|
||||
|
||||
@@ -263,6 +263,7 @@ def get_actual_details(name, filters):
|
||||
and ba.account=gl.account
|
||||
and b.{budget_against} = gl.{budget_against}
|
||||
and gl.fiscal_year between %s and %s
|
||||
and gl.is_cancelled = 0
|
||||
and b.{budget_against} = %s
|
||||
and exists(
|
||||
select
|
||||
|
||||
@@ -9,6 +9,7 @@ frappe.query_reports["Customer Ledger Summary"] = {
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
|
||||
@@ -4,14 +4,19 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, scrub
|
||||
from frappe.query_builder import Criterion, Tuple
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import getdate, nowdate
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
|
||||
|
||||
TREE_DOCTYPES = frozenset(
|
||||
["Customer Group", "Terrirtory", "Supplier Group", "Sales Partner", "Sales Person", "Cost Center"]
|
||||
)
|
||||
|
||||
|
||||
class PartyLedgerSummaryReport:
|
||||
@@ -20,59 +25,110 @@ class PartyLedgerSummaryReport:
|
||||
self.filters.from_date = getdate(self.filters.from_date or nowdate())
|
||||
self.filters.to_date = getdate(self.filters.to_date or nowdate())
|
||||
|
||||
if not self.filters.get("company"):
|
||||
self.filters["company"] = frappe.db.get_single_value("Global Defaults", "default_company")
|
||||
|
||||
def run(self, args):
|
||||
if self.filters.from_date > self.filters.to_date:
|
||||
frappe.throw(_("From Date must be before To Date"))
|
||||
|
||||
self.filters.party_type = args.get("party_type")
|
||||
self.party_naming_by = frappe.db.get_single_value(args.get("naming_by")[0], args.get("naming_by")[1])
|
||||
|
||||
self.validate_filters()
|
||||
self.get_party_details()
|
||||
|
||||
if not self.parties:
|
||||
return [], []
|
||||
|
||||
self.get_gl_entries()
|
||||
self.get_additional_columns()
|
||||
self.get_return_invoices()
|
||||
self.get_party_adjustment_amounts()
|
||||
|
||||
self.party_naming_by = frappe.db.get_single_value(args.get("naming_by")[0], args.get("naming_by")[1])
|
||||
columns = self.get_columns()
|
||||
data = self.get_data()
|
||||
|
||||
return columns, data
|
||||
|
||||
def get_additional_columns(self):
|
||||
def validate_filters(self):
|
||||
if not self.filters.get("company"):
|
||||
frappe.throw(_("{0} is mandatory").format(_("Company")))
|
||||
|
||||
if self.filters.from_date > self.filters.to_date:
|
||||
frappe.throw(_("From Date must be before To Date"))
|
||||
|
||||
self.update_hierarchical_filters()
|
||||
|
||||
def update_hierarchical_filters(self):
|
||||
for doctype in TREE_DOCTYPES:
|
||||
key = scrub(doctype)
|
||||
if self.filters.get(key):
|
||||
self.filters[key] = get_children(doctype, self.filters[key])
|
||||
|
||||
def get_party_details(self):
|
||||
"""
|
||||
Additional Columns for 'User Permission' based access control
|
||||
"""
|
||||
self.parties = []
|
||||
self.party_details = frappe._dict()
|
||||
party_type = self.filters.party_type
|
||||
|
||||
if self.filters.party_type == "Customer":
|
||||
self.territories = frappe._dict({})
|
||||
self.customer_group = frappe._dict({})
|
||||
doctype = qb.DocType(party_type)
|
||||
conditions = self.get_party_conditions(doctype)
|
||||
query = (
|
||||
qb.from_(doctype)
|
||||
.select(doctype.name.as_("party"), f"{scrub(party_type)}_name")
|
||||
.where(Criterion.all(conditions))
|
||||
)
|
||||
|
||||
customer = qb.DocType("Customer")
|
||||
result = (
|
||||
frappe.qb.from_(customer)
|
||||
.select(
|
||||
customer.name, customer.territory, customer.customer_group, customer.default_sales_partner
|
||||
)
|
||||
.where(customer.disabled == 0)
|
||||
.run(as_dict=True)
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions(party_type)
|
||||
|
||||
if match_conditions:
|
||||
query += "and" + match_conditions
|
||||
|
||||
party_details = frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
for row in party_details:
|
||||
self.parties.append(row.party)
|
||||
self.party_details[row.party] = row
|
||||
|
||||
def get_party_conditions(self, doctype):
|
||||
conditions = []
|
||||
group_field = "customer_group" if self.filters.party_type == "Customer" else "supplier_group"
|
||||
|
||||
if self.filters.party:
|
||||
conditions.append(doctype.name == self.filters.party)
|
||||
|
||||
if self.filters.territory:
|
||||
conditions.append(doctype.territory.isin(self.filters.territory))
|
||||
|
||||
if self.filters.get(group_field):
|
||||
conditions.append(doctype.get(group_field).isin(self.filters.get(group_field)))
|
||||
|
||||
if self.filters.payment_terms_template:
|
||||
conditions.append(doctype.payment_terms == self.filters.payment_terms_template)
|
||||
|
||||
if self.filters.sales_partner:
|
||||
conditions.append(doctype.default_sales_partner.isin(self.filters.sales_partner))
|
||||
|
||||
if self.filters.sales_person:
|
||||
sales_team = qb.DocType("Sales Team")
|
||||
sales_invoice = qb.DocType("Sales Invoice")
|
||||
|
||||
customers = (
|
||||
qb.from_(sales_team)
|
||||
.select(sales_team.parent)
|
||||
.where(sales_team.sales_person.isin(self.filters.sales_person))
|
||||
.where(sales_team.parenttype == "Customer")
|
||||
) + (
|
||||
qb.from_(sales_team)
|
||||
.join(sales_invoice)
|
||||
.on(sales_team.parent == sales_invoice.name)
|
||||
.select(sales_invoice.customer)
|
||||
.where(sales_team.sales_person.isin(self.filters.sales_person))
|
||||
.where(sales_team.parenttype == "Sales Invoice")
|
||||
)
|
||||
|
||||
for x in result:
|
||||
self.territories[x.name] = x.territory
|
||||
self.customer_group[x.name] = x.customer_group
|
||||
else:
|
||||
self.supplier_group = frappe._dict({})
|
||||
supplier = qb.DocType("Supplier")
|
||||
result = (
|
||||
frappe.qb.from_(supplier)
|
||||
.select(supplier.name, supplier.supplier_group)
|
||||
.where(supplier.disabled == 0)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
conditions.append(doctype.name.isin(customers))
|
||||
|
||||
for x in result:
|
||||
self.supplier_group[x.name] = x.supplier_group
|
||||
return conditions
|
||||
|
||||
def get_columns(self):
|
||||
columns = [
|
||||
@@ -195,12 +251,13 @@ class PartyLedgerSummaryReport:
|
||||
|
||||
self.party_data = frappe._dict({})
|
||||
for gle in self.gl_entries:
|
||||
party_details = self.party_details.get(gle.party)
|
||||
self.party_data.setdefault(
|
||||
gle.party,
|
||||
frappe._dict(
|
||||
{
|
||||
"party": gle.party,
|
||||
"party_name": gle.party_name,
|
||||
**party_details,
|
||||
"party_name": gle.party,
|
||||
"opening_balance": 0,
|
||||
"invoiced_amount": 0,
|
||||
"paid_amount": 0,
|
||||
@@ -211,12 +268,6 @@ class PartyLedgerSummaryReport:
|
||||
),
|
||||
)
|
||||
|
||||
if self.filters.party_type == "Customer":
|
||||
self.party_data[gle.party].update({"territory": self.territories.get(gle.party)})
|
||||
self.party_data[gle.party].update({"customer_group": self.customer_group.get(gle.party)})
|
||||
else:
|
||||
self.party_data[gle.party].update({"supplier_group": self.supplier_group.get(gle.party)})
|
||||
|
||||
amount = gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr)
|
||||
self.party_data[gle.party].closing_balance += amount
|
||||
|
||||
@@ -261,8 +312,6 @@ class PartyLedgerSummaryReport:
|
||||
gle.party,
|
||||
gle.voucher_type,
|
||||
gle.voucher_no,
|
||||
gle.against_voucher_type,
|
||||
gle.against_voucher,
|
||||
gle.debit,
|
||||
gle.credit,
|
||||
gle.is_opening,
|
||||
@@ -273,26 +322,12 @@ class PartyLedgerSummaryReport:
|
||||
& (gle.party_type == self.filters.party_type)
|
||||
& (IfNull(gle.party, "") != "")
|
||||
& (gle.posting_date <= self.filters.to_date)
|
||||
& (gle.party.isin(self.parties))
|
||||
)
|
||||
.orderby(gle.posting_date)
|
||||
)
|
||||
|
||||
if self.filters.party_type == "Customer":
|
||||
customer = qb.DocType("Customer")
|
||||
query = (
|
||||
query.select(customer.customer_name.as_("party_name"))
|
||||
.left_join(customer)
|
||||
.on(customer.name == gle.party)
|
||||
)
|
||||
elif self.filters.party_type == "Supplier":
|
||||
supplier = qb.DocType("Supplier")
|
||||
query = (
|
||||
query.select(supplier.supplier_name.as_("party_name"))
|
||||
.left_join(supplier)
|
||||
.on(supplier.name == gle.party)
|
||||
)
|
||||
|
||||
query = self.prepare_conditions(query)
|
||||
|
||||
self.gl_entries = query.run(as_dict=True)
|
||||
|
||||
def prepare_conditions(self, query):
|
||||
@@ -303,70 +338,7 @@ class PartyLedgerSummaryReport:
|
||||
if self.filters.finance_book:
|
||||
query = query.where(IfNull(gle.finance_book, "") == self.filters.finance_book)
|
||||
|
||||
if self.filters.party:
|
||||
query = query.where(gle.party == self.filters.party)
|
||||
|
||||
if self.filters.party_type == "Customer":
|
||||
customer = qb.DocType("Customer")
|
||||
if self.filters.customer_group:
|
||||
query = query.where(
|
||||
(gle.party).isin(
|
||||
qb.from_(customer)
|
||||
.select(customer.name)
|
||||
.where(customer.customer_group == self.filters.customer_group)
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.territory:
|
||||
query = query.where(
|
||||
(gle.party).isin(
|
||||
qb.from_(customer)
|
||||
.select(customer.name)
|
||||
.where(customer.territory == self.filters.territory)
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.payment_terms_template:
|
||||
query = query.where(
|
||||
(gle.party).isin(
|
||||
qb.from_(customer)
|
||||
.select(customer.name)
|
||||
.where(customer.payment_terms == self.filters.payment_terms_template)
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.sales_partner:
|
||||
query = query.where(
|
||||
(gle.party).isin(
|
||||
qb.from_(customer)
|
||||
.select(customer.name)
|
||||
.where(customer.default_sales_partner == self.filters.sales_partner)
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.sales_person:
|
||||
sales_team = qb.DocType("Sales Team")
|
||||
query = query.where(
|
||||
(gle.party).isin(
|
||||
qb.from_(sales_team)
|
||||
.select(sales_team.parent)
|
||||
.where(sales_team.sales_person == self.filters.sales_person)
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.party_type == "Supplier":
|
||||
if self.filters.supplier_group:
|
||||
supplier = qb.DocType("Supplier")
|
||||
query = query.where(
|
||||
(gle.party).isin(
|
||||
qb.from_(supplier)
|
||||
.select(supplier.name)
|
||||
.where(supplier.supplier_group == self.filters.supplier_group)
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.cost_center:
|
||||
self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center)
|
||||
query = query.where((gle.cost_center).isin(self.filters.cost_center))
|
||||
|
||||
if self.filters.project:
|
||||
@@ -393,45 +365,44 @@ class PartyLedgerSummaryReport:
|
||||
|
||||
def get_return_invoices(self):
|
||||
doctype = "Sales Invoice" if self.filters.party_type == "Customer" else "Purchase Invoice"
|
||||
self.return_invoices = [
|
||||
d.name
|
||||
for d in frappe.get_all(
|
||||
doctype,
|
||||
filters={
|
||||
"is_return": 1,
|
||||
"docstatus": 1,
|
||||
"posting_date": ["between", [self.filters.from_date, self.filters.to_date]],
|
||||
},
|
||||
)
|
||||
]
|
||||
filters = (
|
||||
{
|
||||
"is_return": 1,
|
||||
"docstatus": 1,
|
||||
"posting_date": ["between", [self.filters.from_date, self.filters.to_date]],
|
||||
f"{scrub(self.filters.party_type)}": ["in", self.parties],
|
||||
},
|
||||
)
|
||||
|
||||
self.return_invoices = frappe.get_all(doctype, filters=filters, pluck="name")
|
||||
|
||||
def get_party_adjustment_amounts(self):
|
||||
account_type = "Expense Account" if self.filters.party_type == "Customer" else "Income Account"
|
||||
self.income_or_expense_accounts = frappe.db.get_all(
|
||||
"Account", filters={"account_type": account_type, "company": self.filters.company}, pluck="name"
|
||||
)
|
||||
|
||||
invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
|
||||
reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit"
|
||||
round_off_account = frappe.get_cached_value("Company", self.filters.company, "round_off_account")
|
||||
|
||||
if not self.income_or_expense_accounts:
|
||||
# prevent empty 'in' condition
|
||||
self.income_or_expense_accounts.append("")
|
||||
else:
|
||||
# escape '%' in account name
|
||||
# ignoring frappe.db.escape as it replaces single quotes with double quotes
|
||||
self.income_or_expense_accounts = [x.replace("%", "%%") for x in self.income_or_expense_accounts]
|
||||
current_period_vouchers = set()
|
||||
adjustment_voucher_entries = {}
|
||||
|
||||
self.party_adjustment_details = {}
|
||||
self.party_adjustment_accounts = set()
|
||||
|
||||
for gle in self.gl_entries:
|
||||
if (
|
||||
gle.is_opening != "Yes"
|
||||
and gle.posting_date >= self.filters.from_date
|
||||
and gle.posting_date <= self.filters.to_date
|
||||
):
|
||||
current_period_vouchers.add((gle.voucher_type, gle.voucher_no))
|
||||
adjustment_voucher_entries.setdefault((gle.voucher_type, gle.voucher_no), []).append(gle)
|
||||
|
||||
if not current_period_vouchers:
|
||||
return
|
||||
|
||||
gl = qb.DocType("GL Entry")
|
||||
accounts_query = self.get_base_accounts_query()
|
||||
accounts_query_voucher_no = accounts_query.select(gl.voucher_no)
|
||||
accounts_query_voucher_type = accounts_query.select(gl.voucher_type)
|
||||
|
||||
subquery = self.get_base_subquery()
|
||||
subquery_voucher_no = subquery.select(gl.voucher_no)
|
||||
subquery_voucher_type = subquery.select(gl.voucher_type)
|
||||
|
||||
gl_entries = (
|
||||
query = (
|
||||
qb.from_(gl)
|
||||
.select(
|
||||
gl.posting_date, gl.account, gl.party, gl.voucher_type, gl.voucher_no, gl.debit, gl.credit
|
||||
@@ -439,18 +410,16 @@ class PartyLedgerSummaryReport:
|
||||
.where(
|
||||
(gl.docstatus < 2)
|
||||
& (gl.is_cancelled == 0)
|
||||
& (gl.voucher_no.isin(accounts_query_voucher_no))
|
||||
& (gl.voucher_type.isin(accounts_query_voucher_type))
|
||||
& (gl.voucher_no.isin(subquery_voucher_no))
|
||||
& (gl.voucher_type.isin(subquery_voucher_type))
|
||||
& (gl.posting_date.gte(self.filters.from_date))
|
||||
& (gl.posting_date.lte(self.filters.to_date))
|
||||
& (Tuple((gl.voucher_type, gl.voucher_no)).isin(current_period_vouchers))
|
||||
& (IfNull(gl.party, "") == "")
|
||||
)
|
||||
).run(as_dict=True)
|
||||
)
|
||||
query = self.prepare_conditions(query)
|
||||
gl_entries = query.run(as_dict=True)
|
||||
|
||||
self.party_adjustment_details = {}
|
||||
self.party_adjustment_accounts = set()
|
||||
adjustment_voucher_entries = {}
|
||||
for gle in gl_entries:
|
||||
adjustment_voucher_entries.setdefault((gle.voucher_type, gle.voucher_no), [])
|
||||
adjustment_voucher_entries[(gle.voucher_type, gle.voucher_no)].append(gle)
|
||||
|
||||
for voucher_gl_entries in adjustment_voucher_entries.values():
|
||||
@@ -486,25 +455,11 @@ class PartyLedgerSummaryReport:
|
||||
self.party_adjustment_details[party].setdefault(account, 0)
|
||||
self.party_adjustment_details[party][account] += amount
|
||||
|
||||
def get_base_accounts_query(self):
|
||||
gl = qb.DocType("GL Entry")
|
||||
query = qb.from_(gl).where(
|
||||
(gl.account.isin(self.income_or_expense_accounts))
|
||||
& (gl.posting_date.gte(self.filters.from_date))
|
||||
& (gl.posting_date.lte(self.filters.to_date))
|
||||
)
|
||||
return query
|
||||
|
||||
def get_base_subquery(self):
|
||||
gl = qb.DocType("GL Entry")
|
||||
query = qb.from_(gl).where(
|
||||
(gl.docstatus < 2)
|
||||
& (gl.party_type == self.filters.party_type)
|
||||
& (IfNull(gl.party, "") != "")
|
||||
& (gl.posting_date.between(self.filters.from_date, self.filters.to_date))
|
||||
)
|
||||
query = self.prepare_conditions(query)
|
||||
return query
|
||||
def get_children(doctype, value):
|
||||
children = get_descendants_of(doctype, value)
|
||||
|
||||
return [value, *children]
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -512,4 +467,5 @@ def execute(filters=None):
|
||||
"party_type": "Customer",
|
||||
"naming_by": ["Selling Settings", "cust_master_name"],
|
||||
}
|
||||
|
||||
return PartyLedgerSummaryReport(filters).run(args)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user