mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-07 15:12:51 +00:00
Merge pull request #46940 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -62,7 +62,7 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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,8 +374,6 @@ def auto_reconcile_vouchers(
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
):
|
||||
frappe.flags.auto_reconcile_vouchers = True
|
||||
|
||||
bank_transactions = get_bank_transactions(bank_account)
|
||||
|
||||
if len(bank_transactions) > 10:
|
||||
@@ -403,6 +402,8 @@ def auto_reconcile_vouchers(
|
||||
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(
|
||||
@@ -517,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,
|
||||
@@ -796,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"),
|
||||
@@ -827,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):
|
||||
|
||||
@@ -220,6 +220,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
|
||||
|
||||
@@ -249,16 +249,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"))
|
||||
@@ -276,15 +278,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):
|
||||
"""
|
||||
@@ -322,91 +325,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:
|
||||
@@ -479,47 +480,48 @@ 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:
|
||||
d.db_set(field, value)
|
||||
if field == "exchange_rate" or not d.get(field) or force:
|
||||
d.db_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)
|
||||
@@ -529,12 +531,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(
|
||||
@@ -545,9 +543,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(
|
||||
@@ -578,63 +575,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 = {}
|
||||
@@ -700,37 +695,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):
|
||||
@@ -977,14 +974,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")
|
||||
@@ -1648,25 +1645,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_in_transaction_currency": d.amount / self.transaction_exchange_rate,
|
||||
"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":
|
||||
|
||||
@@ -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
|
||||
@@ -120,13 +119,13 @@ 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(
|
||||
@@ -544,6 +543,8 @@ def make_payment_request(**args):
|
||||
gateway_account = get_gateway_details(args) or frappe._dict()
|
||||
|
||||
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
|
||||
if not grand_total:
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
if args.loyalty_points and args.dt == "Sales Order":
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
|
||||
|
||||
@@ -554,19 +555,8 @@ def make_payment_request(**args):
|
||||
frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False)
|
||||
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": args.dt, "reference_name": args.dn, "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 +568,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 +577,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 +588,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"):
|
||||
@@ -675,22 +666,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:
|
||||
@@ -699,10 +703,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:
|
||||
@@ -745,7 +746,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.
|
||||
"""
|
||||
@@ -753,9 +754,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)
|
||||
)
|
||||
|
||||
@@ -764,43 +765,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):
|
||||
PLE = frappe.qb.DocType("Payment Ledger Entry")
|
||||
PER = frappe.qb.DocType("Payment Entry Reference")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(PLE)
|
||||
.left_join(PER)
|
||||
.on(
|
||||
(PLE.against_voucher_type == PER.reference_doctype)
|
||||
& (PLE.against_voucher_no == PER.reference_name)
|
||||
& (PLE.voucher_type == PER.parenttype)
|
||||
& (PLE.voucher_no == PER.parent)
|
||||
)
|
||||
.select(
|
||||
Abs(Sum(PLE.amount)).as_("total_amount"),
|
||||
Abs(Sum(frappe.qb.terms.Case().when(PER.payment_request.isnotnull(), PLE.amount).else_(0))).as_(
|
||||
"request_paid_amount"
|
||||
),
|
||||
)
|
||||
.where(
|
||||
(PLE.voucher_type.isin([doctype, "Journal Entry", "Payment Entry"]))
|
||||
& (PLE.against_voucher_type == doctype)
|
||||
& (PLE.against_voucher_no == name)
|
||||
& (PLE.delinked == 0)
|
||||
& (PLE.docstatus == 1)
|
||||
& (PLE.amount < 0)
|
||||
)
|
||||
)
|
||||
|
||||
result = query.run()
|
||||
ledger_amount = flt(result[0][0]) if result else 0
|
||||
request_paid_amount = flt(result[0][1]) if result else 0
|
||||
|
||||
return ledger_amount - request_paid_amount
|
||||
return os_amount_in_transaction_currency
|
||||
|
||||
|
||||
def get_gateway_details(args): # nosemgrep
|
||||
|
||||
@@ -313,6 +313,16 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
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()
|
||||
|
||||
@@ -331,7 +341,7 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
# 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,
|
||||
@@ -361,6 +371,17 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
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
|
||||
@@ -389,7 +410,7 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
# 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,
|
||||
|
||||
@@ -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")
|
||||
@@ -320,6 +321,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
|
||||
|
||||
@@ -26,6 +26,12 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2094,7 +2094,7 @@ class TestPurchaseInvoice(FrappeTestCase, 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(
|
||||
@@ -2683,6 +2683,78 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
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(
|
||||
|
||||
@@ -267,8 +267,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()
|
||||
|
||||
@@ -4284,6 +4284,35 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
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
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -21,7 +21,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
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, FrappeTestCase):
|
||||
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, FrappeTestCase):
|
||||
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, FrappeTestCase):
|
||||
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, FrappeTestCase):
|
||||
],
|
||||
)
|
||||
|
||||
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,
|
||||
|
||||
@@ -52,11 +52,6 @@ frappe.query_reports["General Ledger"] = {
|
||||
frappe.query_report.set_filter_value("group_by", "Group by Voucher (Consolidated)");
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "against_voucher_no",
|
||||
label: __("Against Voucher No"),
|
||||
fieldtype: "Data",
|
||||
},
|
||||
{
|
||||
fieldtype: "Break",
|
||||
},
|
||||
@@ -66,7 +61,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
fieldtype: "Autocomplete",
|
||||
options: Object.keys(frappe.boot.party_account_types),
|
||||
on_change: function () {
|
||||
frappe.query_report.set_filter_value("party", "");
|
||||
frappe.query_report.set_filter_value("party", []);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -224,9 +224,6 @@ def get_conditions(filters):
|
||||
if filters.get("voucher_no"):
|
||||
conditions.append("voucher_no=%(voucher_no)s")
|
||||
|
||||
if filters.get("against_voucher_no"):
|
||||
conditions.append("against_voucher=%(against_voucher_no)s")
|
||||
|
||||
if filters.get("ignore_err"):
|
||||
err_journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
@@ -490,9 +487,6 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
|
||||
data[key][rev_dr_or_cr] = 0
|
||||
data[key][rev_dr_or_cr + "_in_account_currency"] = 0
|
||||
|
||||
if data[key].against_voucher and gle.against_voucher:
|
||||
data[key].against_voucher += ", " + gle.against_voucher
|
||||
|
||||
from_date, to_date = getdate(filters.from_date), getdate(filters.to_date)
|
||||
show_opening_entries = filters.get("show_opening_entries")
|
||||
|
||||
@@ -695,14 +689,6 @@ def get_columns(filters):
|
||||
|
||||
columns.extend(
|
||||
[
|
||||
{"label": _("Against Voucher Type"), "fieldname": "against_voucher_type", "width": 100},
|
||||
{
|
||||
"label": _("Against Voucher"),
|
||||
"fieldname": "against_voucher",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "against_voucher_type",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Supplier Invoice No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 100},
|
||||
]
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import erpnext
|
||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
||||
add_sub_total_row,
|
||||
add_total_row,
|
||||
apply_group_by_conditions,
|
||||
apply_order_by_conditions,
|
||||
get_grand_total,
|
||||
get_group_by_and_display_fields,
|
||||
get_tax_accounts,
|
||||
@@ -305,12 +305,6 @@ def apply_conditions(query, pi, pii, filters):
|
||||
if filters.get("item_group"):
|
||||
query = query.where(pii.item_group == filters.get("item_group"))
|
||||
|
||||
if not filters.get("group_by"):
|
||||
query = query.orderby(pi.posting_date, order=Order.desc)
|
||||
query = query.orderby(pii.item_group, order=Order.desc)
|
||||
else:
|
||||
query = apply_group_by_conditions(query, pi, pii, filters)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
@@ -372,7 +366,17 @@ def get_items(filters, additional_table_columns):
|
||||
|
||||
query = apply_conditions(query, pi, pii, filters)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Sales Invoice")
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query = apply_order_by_conditions(query, pi, pii, filters)
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_aii_accounts():
|
||||
|
||||
@@ -384,27 +384,24 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||
| (si.unrealized_profit_loss_account == filters.get("income_account"))
|
||||
)
|
||||
|
||||
if not filters.get("group_by"):
|
||||
query = query.orderby(si.posting_date, order=Order.desc)
|
||||
query = query.orderby(sii.item_group, order=Order.desc)
|
||||
else:
|
||||
query = apply_group_by_conditions(query, si, sii, filters)
|
||||
|
||||
for key, value in (additional_conditions or {}).items():
|
||||
query = query.where(si[key] == value)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def apply_group_by_conditions(query, si, ii, filters):
|
||||
if filters.get("group_by") == "Invoice":
|
||||
query = query.orderby(ii.parent, order=Order.desc)
|
||||
def apply_order_by_conditions(query, si, ii, filters):
|
||||
if not filters.get("group_by"):
|
||||
query += f" order by {si.posting_date} desc, {ii.item_group} desc"
|
||||
elif filters.get("group_by") == "Invoice":
|
||||
query += f" order by {ii.parent} desc"
|
||||
elif filters.get("group_by") == "Item":
|
||||
query = query.orderby(ii.item_code)
|
||||
query += f" order by {ii.item_code}"
|
||||
elif filters.get("group_by") == "Item Group":
|
||||
query = query.orderby(ii.item_group)
|
||||
query += f" order by {ii.item_group}"
|
||||
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
|
||||
query = query.orderby(si[frappe.scrub(filters.get("group_by"))])
|
||||
filter_field = frappe.scrub(filters.get("group_by"))
|
||||
query += f" order by {filter_field} desc"
|
||||
|
||||
return query
|
||||
|
||||
@@ -479,7 +476,17 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
|
||||
query = apply_conditions(query, si, sii, filters, additional_conditions)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Sales Invoice")
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query = apply_order_by_conditions(query, si, sii, filters)
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_delivery_notes_against_sales_order(item_list):
|
||||
|
||||
@@ -397,7 +397,6 @@ def get_invoices(filters, additional_query_columns):
|
||||
pi.mode_of_payment,
|
||||
)
|
||||
.where(pi.docstatus == 1)
|
||||
.orderby(pi.posting_date, pi.name, order=Order.desc)
|
||||
)
|
||||
|
||||
if additional_query_columns:
|
||||
@@ -421,8 +420,17 @@ def get_invoices(filters, additional_query_columns):
|
||||
)
|
||||
query = query.where(pi.credit_to.isin(party_account))
|
||||
|
||||
invoices = query.run(as_dict=True)
|
||||
return invoices
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Purchase Invoice")
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query += " order by posting_date desc, name desc"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(filters, query, doctype):
|
||||
|
||||
@@ -439,7 +439,6 @@ def get_invoices(filters, additional_query_columns):
|
||||
si.company,
|
||||
)
|
||||
.where(si.docstatus == 1)
|
||||
.orderby(si.posting_date, si.name, order=Order.desc)
|
||||
)
|
||||
|
||||
if additional_query_columns:
|
||||
@@ -457,8 +456,17 @@ def get_invoices(filters, additional_query_columns):
|
||||
filters, query, doctype="Sales Invoice", child_doctype="Sales Invoice Item"
|
||||
)
|
||||
|
||||
invoices = query.run(as_dict=True)
|
||||
return invoices
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Sales Invoice")
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query += " order by posting_date desc, name desc"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(filters, query, doctype):
|
||||
|
||||
@@ -85,6 +85,7 @@ class AccountsTestMixin:
|
||||
"attribute_name": "bank",
|
||||
"account_name": "HDFC",
|
||||
"parent_account": "Bank Accounts - " + abbr,
|
||||
"account_type": "Bank",
|
||||
}
|
||||
),
|
||||
frappe._dict(
|
||||
|
||||
@@ -1854,14 +1854,17 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
|
||||
):
|
||||
outstanding = voucher_outstanding[0]
|
||||
ref_doc = frappe.get_doc(voucher_type, voucher_no)
|
||||
outstanding_amount = flt(
|
||||
outstanding["outstanding_in_account_currency"], ref_doc.precision("outstanding_amount")
|
||||
)
|
||||
|
||||
# Didn't use db_set for optimisation purpose
|
||||
ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] or 0.0
|
||||
ref_doc.outstanding_amount = outstanding_amount
|
||||
frappe.db.set_value(
|
||||
voucher_type,
|
||||
voucher_no,
|
||||
"outstanding_amount",
|
||||
outstanding["outstanding_in_account_currency"] or 0.0,
|
||||
outstanding_amount,
|
||||
)
|
||||
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
@@ -621,7 +621,7 @@
|
||||
"doc_view": "List",
|
||||
"label": "Learn Accounting",
|
||||
"type": "URL",
|
||||
"url": "https://frappe.school/courses/erpnext-accounting?utm_source=in_app"
|
||||
"url": "https://school.frappe.io/lms/courses/erpnext-accounting?utm_source=in_app"
|
||||
},
|
||||
{
|
||||
"label": "Chart of Accounts",
|
||||
|
||||
@@ -22,15 +22,27 @@ frappe.listview_settings["Purchase Order"] = {
|
||||
return [
|
||||
__("To Receive and Bill"),
|
||||
"orange",
|
||||
"per_received,<,100|per_billed,<,100|status,!=,Closed",
|
||||
"per_received,<,100|per_billed,<,100|status,!=,Closed|docstatus,=,1",
|
||||
];
|
||||
} else {
|
||||
return [__("To Receive"), "orange", "per_received,<,100|per_billed,=,100|status,!=,Closed"];
|
||||
return [
|
||||
__("To Receive"),
|
||||
"orange",
|
||||
"per_received,<,100|per_billed,=,100|status,!=,Closed|docstatus,=,1",
|
||||
];
|
||||
}
|
||||
} else if (flt(doc.per_received) >= 100 && flt(doc.per_billed) < 100 && doc.status !== "Closed") {
|
||||
return [__("To Bill"), "orange", "per_received,=,100|per_billed,<,100|status,!=,Closed"];
|
||||
return [
|
||||
__("To Bill"),
|
||||
"orange",
|
||||
"per_received,=,100|per_billed,<,100|status,!=,Closed|docstatus,=,1",
|
||||
];
|
||||
} else if (flt(doc.per_received) >= 100 && flt(doc.per_billed) == 100 && doc.status !== "Closed") {
|
||||
return [__("Completed"), "green", "per_received,=,100|per_billed,=,100|status,!=,Closed"];
|
||||
return [
|
||||
__("Completed"),
|
||||
"green",
|
||||
"per_received,=,100|per_billed,=,100|status,!=,Closed|docstatus,=,1",
|
||||
];
|
||||
}
|
||||
},
|
||||
onload: function (listview) {
|
||||
|
||||
@@ -537,7 +537,7 @@
|
||||
"doc_view": "List",
|
||||
"label": "Learn Procurement",
|
||||
"type": "URL",
|
||||
"url": "https://frappe.school/courses/procurement?utm_source=in_app"
|
||||
"url": "https://school.frappe.io/lms/courses/procurement?utm_source=in_app"
|
||||
},
|
||||
{
|
||||
"color": "Yellow",
|
||||
|
||||
@@ -165,6 +165,48 @@ class AccountsController(TransactionBase):
|
||||
raise_exception=1,
|
||||
)
|
||||
|
||||
def validate_against_voucher_outstanding(self):
|
||||
from frappe.model.meta import get_meta
|
||||
|
||||
if not get_meta(self.doctype).has_field("outstanding_amount"):
|
||||
return
|
||||
|
||||
if self.get("is_return") and self.return_against and not self.get("is_pos"):
|
||||
against_voucher_outstanding = frappe.get_value(
|
||||
self.doctype, self.return_against, "outstanding_amount"
|
||||
)
|
||||
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
|
||||
|
||||
msg = ""
|
||||
if self.get("update_outstanding_for_self"):
|
||||
msg = (
|
||||
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, "
|
||||
"uncheck '{2}' checkbox. <br><br>Or"
|
||||
).format(
|
||||
frappe.bold(document_type),
|
||||
get_link_to_form(self.doctype, self.get("return_against")),
|
||||
frappe.bold(_("Update Outstanding for Self")),
|
||||
)
|
||||
|
||||
elif not self.update_outstanding_for_self and (
|
||||
abs(flt(self.rounded_total) or flt(self.grand_total)) > flt(against_voucher_outstanding)
|
||||
):
|
||||
self.update_outstanding_for_self = 1
|
||||
msg = (
|
||||
"The outstanding amount {} in {} is lesser than {}. Updating the outstanding to this invoice. <br><br>And"
|
||||
).format(
|
||||
against_voucher_outstanding,
|
||||
get_link_to_form(self.doctype, self.get("return_against")),
|
||||
flt(abs(self.outstanding_amount)),
|
||||
)
|
||||
|
||||
if msg:
|
||||
msg += " you can use {} tool to reconcile against {} later.".format(
|
||||
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
|
||||
get_link_to_form(self.doctype, self.get("return_against")),
|
||||
)
|
||||
frappe.msgprint(_(msg))
|
||||
|
||||
def validate(self):
|
||||
if not self.get("is_return") and not self.get("is_debit_note"):
|
||||
self.validate_qty_is_not_zero()
|
||||
@@ -193,6 +235,7 @@ class AccountsController(TransactionBase):
|
||||
self.disable_tax_included_prices_for_internal_transfer()
|
||||
self.set_incoming_rate()
|
||||
self.init_internal_values()
|
||||
self.validate_against_voucher_outstanding()
|
||||
|
||||
# Need to set taxes based on taxes_and_charges template
|
||||
# before calculating taxes and totals
|
||||
@@ -228,20 +271,6 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
)
|
||||
|
||||
if self.get("is_return") and self.get("return_against") and not self.get("is_pos"):
|
||||
if self.get("update_outstanding_for_self"):
|
||||
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck '{2}' checkbox. <br><br> Or you can use {3} tool to reconcile against {1} later."
|
||||
).format(
|
||||
frappe.bold(document_type),
|
||||
get_link_to_form(self.doctype, self.get("return_against")),
|
||||
frappe.bold(_("Update Outstanding for Self")),
|
||||
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
|
||||
)
|
||||
)
|
||||
|
||||
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
||||
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
|
||||
self.set_advances()
|
||||
@@ -2328,7 +2357,9 @@ class AccountsController(TransactionBase):
|
||||
and automatically_fetch_payment_terms
|
||||
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
||||
):
|
||||
self.fetch_payment_terms_from_order(po_or_so, doctype)
|
||||
self.fetch_payment_terms_from_order(
|
||||
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||
)
|
||||
if self.get("payment_terms_template"):
|
||||
self.ignore_default_payment_terms_template = 1
|
||||
elif self.get("payment_terms_template"):
|
||||
@@ -2372,7 +2403,9 @@ class AccountsController(TransactionBase):
|
||||
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
|
||||
)
|
||||
else:
|
||||
self.fetch_payment_terms_from_order(po_or_so, doctype)
|
||||
self.fetch_payment_terms_from_order(
|
||||
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||
)
|
||||
self.ignore_default_payment_terms_template = 1
|
||||
|
||||
def get_order_details(self):
|
||||
@@ -2410,7 +2443,9 @@ class AccountsController(TransactionBase):
|
||||
def linked_order_has_payment_schedule(self, po_or_so):
|
||||
return frappe.get_all("Payment Schedule", filters={"parent": po_or_so})
|
||||
|
||||
def fetch_payment_terms_from_order(self, po_or_so, po_or_so_doctype):
|
||||
def fetch_payment_terms_from_order(
|
||||
self, po_or_so, po_or_so_doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||
):
|
||||
"""
|
||||
Fetch Payment Terms from Purchase/Sales Order on creating a new Purchase/Sales Invoice.
|
||||
"""
|
||||
@@ -2426,12 +2461,25 @@ class AccountsController(TransactionBase):
|
||||
"invoice_portion": schedule.invoice_portion,
|
||||
"mode_of_payment": schedule.mode_of_payment,
|
||||
"description": schedule.description,
|
||||
"payment_amount": schedule.payment_amount,
|
||||
"base_payment_amount": schedule.base_payment_amount,
|
||||
"outstanding": schedule.outstanding,
|
||||
"paid_amount": schedule.paid_amount,
|
||||
}
|
||||
|
||||
if automatically_fetch_payment_terms:
|
||||
payment_schedule["payment_amount"] = flt(
|
||||
grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
||||
schedule.precision("payment_amount"),
|
||||
)
|
||||
payment_schedule["base_payment_amount"] = flt(
|
||||
base_grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
||||
schedule.precision("base_payment_amount"),
|
||||
)
|
||||
payment_schedule["outstanding"] = payment_schedule["payment_amount"]
|
||||
else:
|
||||
payment_schedule["base_payment_amount"] = flt(
|
||||
schedule.base_payment_amount * self.get("conversion_rate"),
|
||||
schedule.precision("base_payment_amount"),
|
||||
)
|
||||
|
||||
if schedule.discount_type == "Percentage":
|
||||
payment_schedule["discount_type"] = schedule.discount_type
|
||||
payment_schedule["discount"] = schedule.discount
|
||||
|
||||
@@ -898,3 +898,32 @@ def get_filtered_child_rows(doctype, txt, searchfield, start, page_len, filters)
|
||||
)
|
||||
|
||||
return query.run(as_dict=False)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
if frappe.db.get_single_value("Stock Settings", "allow_uom_with_conversion_rate_defined_in_item"):
|
||||
query_filters = {"parent": filters.get("item_code")}
|
||||
|
||||
if txt:
|
||||
query_filters["uom"] = ["like", f"%{txt}%"]
|
||||
|
||||
return frappe.get_all(
|
||||
"UOM Conversion Detail",
|
||||
filters=query_filters,
|
||||
fields=["uom", "conversion_factor"],
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
order_by="idx",
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
return frappe.get_all(
|
||||
"UOM",
|
||||
filters={"name": ["like", f"%{txt}%"]},
|
||||
fields=["name"],
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
@@ -774,7 +774,7 @@ class StockController(AccountsController):
|
||||
if row.get("batch_no"):
|
||||
update_values["batch_no"] = None
|
||||
|
||||
if row.serial_and_batch_bundle:
|
||||
if row.get("serial_and_batch_bundle"):
|
||||
update_values["serial_and_batch_bundle"] = None
|
||||
frappe.db.set_value(
|
||||
"Serial and Batch Bundle", row.serial_and_batch_bundle, {"is_cancelled": 1}
|
||||
@@ -1631,6 +1631,8 @@ def is_reposting_pending():
|
||||
|
||||
|
||||
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
if allow_force_reposting and frappe.db.get_single_value(
|
||||
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
|
||||
):
|
||||
@@ -1652,14 +1654,15 @@ def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
|
||||
|
||||
or_conditions = get_conditions_to_validate_future_sle(sl_entries)
|
||||
|
||||
args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"])
|
||||
|
||||
data = frappe.db.sql(
|
||||
"""
|
||||
select item_code, warehouse, count(name) as total_row
|
||||
from `tabStock Ledger Entry` force index (item_warehouse)
|
||||
from `tabStock Ledger Entry`
|
||||
where
|
||||
({})
|
||||
and timestamp(posting_date, posting_time)
|
||||
>= timestamp(%(posting_date)s, %(posting_time)s)
|
||||
and posting_datetime >= %(posting_datetime)s
|
||||
and voucher_no != %(voucher_no)s
|
||||
and is_cancelled = 0
|
||||
GROUP BY
|
||||
|
||||
@@ -743,7 +743,9 @@ class SubcontractingController(StockController):
|
||||
):
|
||||
continue
|
||||
|
||||
if self.doctype == self.subcontract_data.order_doctype or self.backflush_based_on == "BOM":
|
||||
if self.doctype == self.subcontract_data.order_doctype or (
|
||||
self.backflush_based_on == "BOM" or self.is_return
|
||||
):
|
||||
for bom_item in self.__get_materials_from_bom(
|
||||
row.item_code, row.bom, row.get("include_exploded_items")
|
||||
):
|
||||
|
||||
@@ -377,9 +377,7 @@ class calculate_taxes_and_totals:
|
||||
self._calculate()
|
||||
|
||||
def calculate_taxes(self):
|
||||
rounding_adjustment_computed = self.doc.get("is_consolidated") and self.doc.get("rounding_adjustment")
|
||||
if not rounding_adjustment_computed:
|
||||
self.doc.rounding_adjustment = 0
|
||||
self.grand_total_diff = 0
|
||||
|
||||
# maintain actual tax rate based on idx
|
||||
actual_tax_dict = dict(
|
||||
@@ -446,9 +444,8 @@ class calculate_taxes_and_totals:
|
||||
and self.discount_amount_applied
|
||||
and self.doc.discount_amount
|
||||
and self.doc.apply_discount_on == "Grand Total"
|
||||
and not rounding_adjustment_computed
|
||||
):
|
||||
self.doc.rounding_adjustment = flt(
|
||||
self.grand_total_diff = flt(
|
||||
self.doc.grand_total - flt(self.doc.discount_amount) - tax.total,
|
||||
self.doc.precision("rounding_adjustment"),
|
||||
)
|
||||
@@ -552,11 +549,11 @@ class calculate_taxes_and_totals:
|
||||
return self.adjust_grand_total_for_inclusive_tax()
|
||||
|
||||
def adjust_grand_total_for_inclusive_tax(self):
|
||||
# if fully inclusive taxes and diff
|
||||
# if any inclusive taxes and diff
|
||||
if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")):
|
||||
last_tax = self.doc.get("taxes")[-1]
|
||||
non_inclusive_tax_amount = sum(
|
||||
flt(d.tax_amount_after_discount_amount)
|
||||
self.get_tax_amount_if_for_valuation_or_deduction(d.tax_amount_after_discount_amount, d)
|
||||
for d in self.doc.get("taxes")
|
||||
if not d.included_in_print_rate
|
||||
)
|
||||
@@ -573,27 +570,23 @@ class calculate_taxes_and_totals:
|
||||
diff = flt(diff, self.doc.precision("rounding_adjustment"))
|
||||
|
||||
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
|
||||
self.doc.grand_total_diff = diff
|
||||
else:
|
||||
self.doc.grand_total_diff = 0
|
||||
self.grand_total_diff = diff
|
||||
|
||||
def calculate_totals(self):
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(
|
||||
self.doc.get("grand_total_diff")
|
||||
)
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + self.grand_total_diff
|
||||
else:
|
||||
self.doc.grand_total = flt(self.doc.net_total)
|
||||
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.total_taxes_and_charges = flt(
|
||||
self.doc.grand_total - self.doc.net_total - flt(self.doc.get("grand_total_diff")),
|
||||
self.doc.grand_total - self.doc.net_total - self.grand_total_diff,
|
||||
self.doc.precision("total_taxes_and_charges"),
|
||||
)
|
||||
else:
|
||||
self.doc.total_taxes_and_charges = 0.0
|
||||
|
||||
self._set_in_company_currency(self.doc, ["total_taxes_and_charges", "rounding_adjustment"])
|
||||
self._set_in_company_currency(self.doc, ["total_taxes_and_charges"])
|
||||
|
||||
if self.doc.doctype in [
|
||||
"Quotation",
|
||||
@@ -643,7 +636,9 @@ class calculate_taxes_and_totals:
|
||||
|
||||
if self.doc.meta.get_field("rounded_total"):
|
||||
if self.doc.is_rounded_total_disabled():
|
||||
self.doc.rounded_total = self.doc.base_rounded_total = 0
|
||||
self.doc.rounded_total = 0
|
||||
self.doc.base_rounded_total = 0
|
||||
self.doc.rounding_adjustment = 0
|
||||
return
|
||||
|
||||
self.doc.rounded_total = round_based_on_smallest_currency_fraction(
|
||||
@@ -687,33 +682,29 @@ class calculate_taxes_and_totals:
|
||||
return
|
||||
|
||||
total_for_discount_amount = self.get_total_for_discount_amount()
|
||||
taxes = self.doc.get("taxes")
|
||||
net_total = 0
|
||||
expected_net_total = 0
|
||||
|
||||
if total_for_discount_amount:
|
||||
# calculate item amount after Discount Amount
|
||||
for i, item in enumerate(self._items):
|
||||
for item in self._items:
|
||||
distributed_amount = (
|
||||
flt(self.doc.discount_amount) * item.net_amount / total_for_discount_amount
|
||||
)
|
||||
|
||||
item.net_amount = flt(item.net_amount - distributed_amount, item.precision("net_amount"))
|
||||
adjusted_net_amount = item.net_amount - distributed_amount
|
||||
expected_net_total += adjusted_net_amount
|
||||
item.net_amount = flt(adjusted_net_amount, item.precision("net_amount"))
|
||||
net_total += item.net_amount
|
||||
|
||||
# discount amount rounding loss adjustment if no taxes
|
||||
if (
|
||||
self.doc.apply_discount_on == "Net Total"
|
||||
or not taxes
|
||||
or total_for_discount_amount == self.doc.net_total
|
||||
) and i == len(self._items) - 1:
|
||||
discount_amount_loss = flt(
|
||||
self.doc.net_total - net_total - self.doc.discount_amount,
|
||||
self.doc.precision("net_total"),
|
||||
)
|
||||
|
||||
# discount amount rounding adjustment
|
||||
if rounding_difference := flt(
|
||||
expected_net_total - net_total, self.doc.precision("net_total")
|
||||
):
|
||||
item.net_amount = flt(
|
||||
item.net_amount + discount_amount_loss, item.precision("net_amount")
|
||||
item.net_amount + rounding_difference, item.precision("net_amount")
|
||||
)
|
||||
net_total += rounding_difference
|
||||
|
||||
item.net_rate = (
|
||||
flt(item.net_amount / item.qty, item.precision("net_rate")) if item.qty else 0
|
||||
@@ -729,20 +720,44 @@ class calculate_taxes_and_totals:
|
||||
def get_total_for_discount_amount(self):
|
||||
if self.doc.apply_discount_on == "Net Total":
|
||||
return self.doc.net_total
|
||||
else:
|
||||
actual_taxes_dict = {}
|
||||
|
||||
for tax in self.doc.get("taxes"):
|
||||
if tax.charge_type in ["Actual", "On Item Quantity"]:
|
||||
tax_amount = self.get_tax_amount_if_for_valuation_or_deduction(tax.tax_amount, tax)
|
||||
actual_taxes_dict.setdefault(tax.idx, tax_amount)
|
||||
elif tax.row_id in actual_taxes_dict:
|
||||
actual_tax_amount = flt(actual_taxes_dict.get(tax.row_id, 0)) * flt(tax.rate) / 100
|
||||
actual_taxes_dict.setdefault(tax.idx, actual_tax_amount)
|
||||
total_actual_tax = 0
|
||||
actual_taxes_dict = {}
|
||||
|
||||
return flt(
|
||||
self.doc.grand_total - sum(actual_taxes_dict.values()), self.doc.precision("grand_total")
|
||||
def update_actual_tax_dict(tax, tax_amount):
|
||||
nonlocal total_actual_tax
|
||||
|
||||
if tax.get("add_deduct_tax") == "Deduct":
|
||||
tax_amount *= -1
|
||||
|
||||
if tax.get("category") != "Valuation":
|
||||
total_actual_tax += tax_amount
|
||||
|
||||
actual_taxes_dict[int(tax.idx)] = {
|
||||
"tax_amount": tax_amount,
|
||||
"cumulative_tax_amount": total_actual_tax,
|
||||
}
|
||||
|
||||
for tax in self.doc.get("taxes"):
|
||||
if tax.charge_type in ["Actual", "On Item Quantity"]:
|
||||
update_actual_tax_dict(tax, tax.tax_amount)
|
||||
continue
|
||||
|
||||
if not tax.row_id:
|
||||
continue
|
||||
|
||||
base_row = actual_taxes_dict.get(int(tax.row_id))
|
||||
if not base_row:
|
||||
continue
|
||||
|
||||
base_tax_amount = (
|
||||
base_row["tax_amount"]
|
||||
if tax.charge_type == "On Previous Row Amount"
|
||||
else base_row["cumulative_tax_amount"]
|
||||
)
|
||||
update_actual_tax_dict(tax, base_tax_amount * tax.rate / 100)
|
||||
|
||||
return self.doc.grand_total - total_actual_tax
|
||||
|
||||
def calculate_total_advance(self):
|
||||
if not self.doc.docstatus.is_cancelled():
|
||||
@@ -804,9 +819,12 @@ class calculate_taxes_and_totals:
|
||||
if (
|
||||
self.doc.is_return
|
||||
and self.doc.return_against
|
||||
and not self.doc.update_outstanding_for_self
|
||||
and not self.doc.get("is_pos")
|
||||
or self.is_internal_invoice()
|
||||
):
|
||||
# Do not calculate the outstanding amount for a return invoice if 'update_outstanding_for_self' is not enabled.
|
||||
self.doc.outstanding_amount = 0
|
||||
return
|
||||
|
||||
self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"])
|
||||
|
||||
@@ -1496,11 +1496,8 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
fields = ["name", "item_name", "item_group", "description"]
|
||||
fields.extend([field for field in searchfields if field not in ["name", "item_group", "description"]])
|
||||
|
||||
searchfields = searchfields + [
|
||||
field
|
||||
for field in [searchfield or "name", "item_code", "item_group", "item_name"]
|
||||
if field not in searchfields
|
||||
]
|
||||
if not searchfields:
|
||||
searchfields = ["name"]
|
||||
|
||||
query_filters = {"disabled": 0, "ifnull(end_of_life, '3099-12-31')": (">", today())}
|
||||
|
||||
|
||||
@@ -1616,7 +1616,7 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
|
||||
"posting_date": nowdate(),
|
||||
"for_quantity": row.job_card_qty or work_order.get("qty", 0),
|
||||
"operation_id": row.get("name"),
|
||||
"bom_no": row.get("bom"),
|
||||
"bom_no": row.get("bom") or work_order.bom_no,
|
||||
"project": work_order.project,
|
||||
"company": work_order.company,
|
||||
"sequence_id": row.get("sequence_id"),
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
"doc_view": "List",
|
||||
"label": "Learn Manufacturing",
|
||||
"type": "URL",
|
||||
"url": "https://frappe.school/courses/manufacturing?utm_source=in_app"
|
||||
"url": "https://school.frappe.io/lms/courses/manufacturing?utm_source=in_app"
|
||||
},
|
||||
{
|
||||
"color": "Grey",
|
||||
|
||||
@@ -210,7 +210,7 @@
|
||||
"doc_view": "List",
|
||||
"label": "Learn Project Management",
|
||||
"type": "URL",
|
||||
"url": "https://frappe.school/courses/project-management?utm_source=in_app"
|
||||
"url": "https://school.frappe.io/lms/courses/project-management?utm_source=in_app"
|
||||
},
|
||||
{
|
||||
"color": "Blue",
|
||||
|
||||
@@ -343,7 +343,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
calculate_taxes() {
|
||||
var me = this;
|
||||
this.frm.doc.rounding_adjustment = 0;
|
||||
this.grand_total_diff = 0;
|
||||
var actual_tax_dict = {};
|
||||
|
||||
// maintain actual tax rate based on idx
|
||||
@@ -417,7 +417,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
// adjust Discount Amount loss in last tax iteration
|
||||
if ((i == me.frm.doc["taxes"].length - 1) && me.discount_amount_applied
|
||||
&& me.frm.doc.apply_discount_on == "Grand Total" && me.frm.doc.discount_amount) {
|
||||
me.frm.doc.rounding_adjustment = flt(me.frm.doc.grand_total -
|
||||
me.grand_total_diff = flt(me.frm.doc.grand_total -
|
||||
flt(me.frm.doc.discount_amount) - tax.total, precision("rounding_adjustment"));
|
||||
}
|
||||
}
|
||||
@@ -535,7 +535,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
adjust_grand_total_for_inclusive_tax() {
|
||||
var me = this;
|
||||
// if fully inclusive taxes and diff
|
||||
|
||||
// if any inclusive taxes and diff
|
||||
if (this.frm.doc["taxes"] && this.frm.doc["taxes"].length) {
|
||||
var any_inclusive_tax = false;
|
||||
$.each(this.frm.doc.taxes || [], function(i, d) {
|
||||
@@ -546,7 +547,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
var non_inclusive_tax_amount = frappe.utils.sum($.map(this.frm.doc.taxes || [],
|
||||
function(d) {
|
||||
if(!d.included_in_print_rate) {
|
||||
return flt(d.tax_amount_after_discount_amount);
|
||||
let tax_amount = d.category === "Valuation" ? 0 : d.tax_amount_after_discount_amount;
|
||||
if (d.add_deduct_tax === "Deduct") tax_amount *= -1;
|
||||
return tax_amount;
|
||||
}
|
||||
}
|
||||
));
|
||||
@@ -560,9 +563,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
diff = flt(diff, precision("rounding_adjustment"));
|
||||
|
||||
if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) {
|
||||
me.frm.doc.grand_total_diff = diff;
|
||||
} else {
|
||||
me.frm.doc.grand_total_diff = 0;
|
||||
me.grand_total_diff = diff;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -573,7 +574,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
var me = this;
|
||||
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0;
|
||||
this.frm.doc.grand_total = flt(tax_count
|
||||
? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.grand_total_diff)
|
||||
? this.frm.doc["taxes"][tax_count - 1].total + this.grand_total_diff
|
||||
: this.frm.doc.net_total);
|
||||
|
||||
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
|
||||
@@ -605,9 +606,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
this.frm.doc.total_taxes_and_charges = flt(this.frm.doc.grand_total - this.frm.doc.net_total
|
||||
- flt(this.frm.doc.rounding_adjustment), precision("total_taxes_and_charges"));
|
||||
- this.grand_total_diff, precision("total_taxes_and_charges"));
|
||||
|
||||
this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges", "rounding_adjustment"]);
|
||||
this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges"]);
|
||||
|
||||
// Round grand total as per precision
|
||||
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "base_grand_total"]);
|
||||
@@ -627,6 +628,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
if (cint(disable_rounded_total)) {
|
||||
this.frm.doc.rounded_total = 0;
|
||||
this.frm.doc.base_rounded_total = 0;
|
||||
this.frm.doc.rounding_adjustment = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -695,22 +697,26 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
return;
|
||||
}
|
||||
|
||||
var total_for_discount_amount = this.get_total_for_discount_amount();
|
||||
var net_total = 0;
|
||||
const total_for_discount_amount = this.get_total_for_discount_amount();
|
||||
let net_total = 0;
|
||||
let expected_net_total = 0;
|
||||
|
||||
// calculate item amount after Discount Amount
|
||||
if (total_for_discount_amount) {
|
||||
$.each(this.frm._items || [], function(i, item) {
|
||||
distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
|
||||
item.net_amount = flt(item.net_amount - distributed_amount, precision("net_amount", item));
|
||||
|
||||
const adjusted_net_amount = item.net_amount - distributed_amount;
|
||||
expected_net_total += adjusted_net_amount
|
||||
item.net_amount = flt(adjusted_net_amount, precision("net_amount", item));
|
||||
net_total += item.net_amount;
|
||||
|
||||
// discount amount rounding loss adjustment if no taxes
|
||||
if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total"))
|
||||
&& i == (me.frm._items || []).length - 1) {
|
||||
var discount_amount_loss = flt(me.frm.doc.net_total - net_total
|
||||
- me.frm.doc.discount_amount, precision("net_total"));
|
||||
item.net_amount = flt(item.net_amount + discount_amount_loss,
|
||||
precision("net_amount", item));
|
||||
// discount amount rounding adjustment
|
||||
// assignment to rounding_difference is intentional
|
||||
const rounding_difference = flt(expected_net_total - net_total, precision("net_total"));
|
||||
if (rounding_difference) {
|
||||
item.net_amount = flt(item.net_amount + rounding_difference, precision("net_amount", item));
|
||||
net_total += rounding_difference;
|
||||
}
|
||||
item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0;
|
||||
me.set_in_company_currency(item, ["net_rate", "net_amount"]);
|
||||
@@ -723,29 +729,38 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
get_total_for_discount_amount() {
|
||||
if(this.frm.doc.apply_discount_on == "Net Total") {
|
||||
if(this.frm.doc.apply_discount_on == "Net Total")
|
||||
return this.frm.doc.net_total;
|
||||
} else {
|
||||
var total_actual_tax = 0.0;
|
||||
var actual_taxes_dict = {};
|
||||
|
||||
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
|
||||
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
|
||||
var tax_amount = (tax.category == "Valuation") ? 0.0 : tax.tax_amount;
|
||||
tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0;
|
||||
actual_taxes_dict[tax.idx] = tax_amount;
|
||||
} else if (actual_taxes_dict[tax.row_id] !== null) {
|
||||
var actual_tax_amount = flt(actual_taxes_dict[tax.row_id]) * flt(tax.rate) / 100;
|
||||
actual_taxes_dict[tax.idx] = actual_tax_amount;
|
||||
}
|
||||
});
|
||||
let total_actual_tax = 0.0;
|
||||
let actual_taxes_dict = {};
|
||||
|
||||
$.each(actual_taxes_dict, function(key, value) {
|
||||
if (value) total_actual_tax += value;
|
||||
});
|
||||
function update_actual_taxes_dict(tax, tax_amount) {
|
||||
if (tax.add_deduct_tax == "Deduct") tax_amount *= -1;
|
||||
if (tax.category != "Valuation") total_actual_tax += tax_amount;
|
||||
|
||||
return flt(this.frm.doc.grand_total - total_actual_tax, precision("grand_total"));
|
||||
actual_taxes_dict[tax.idx] = {
|
||||
tax_amount: tax_amount,
|
||||
cumulative_total: total_actual_tax
|
||||
};
|
||||
}
|
||||
|
||||
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
|
||||
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
|
||||
update_actual_taxes_dict(tax, tax.tax_amount);
|
||||
return;
|
||||
}
|
||||
|
||||
const base_row = actual_taxes_dict[tax.row_id];
|
||||
if (!base_row) return;
|
||||
|
||||
// if charge type is 'On Previous Row Amount', calculate tax on previous row amount
|
||||
// else (On Previous Row Total) calculate tax on cumulative total
|
||||
const base_tax_amount = tax.charge_type == "On Previous Row Amount" ? base_row["tax_amount"]: base_row["cumulative_total"];
|
||||
update_actual_taxes_dict(tax, base_tax_amount * tax.rate / 100);
|
||||
});
|
||||
|
||||
return this.frm.doc.grand_total - total_actual_tax;
|
||||
}
|
||||
|
||||
calculate_total_advance(update_paid_amount) {
|
||||
|
||||
@@ -150,6 +150,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
});
|
||||
}
|
||||
|
||||
if (this.frm.fields_dict["items"].grid.get_field("uom")) {
|
||||
this.frm.set_query("uom", "items", function(doc, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_item_uom_query",
|
||||
filters: {
|
||||
"item_code": row.item_code
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if(
|
||||
this.frm.docstatus < 2
|
||||
&& this.frm.fields_dict["payment_terms_template"]
|
||||
@@ -238,6 +251,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
}
|
||||
|
||||
use_serial_batch_fields(frm, cdt, cdn) {
|
||||
const item = locals[cdt][cdn];
|
||||
if (!item.use_serial_batch_fields) {
|
||||
frappe.model.set_value(cdt, cdn, "serial_no", "");
|
||||
frappe.model.set_value(cdt, cdn, "batch_no", "");
|
||||
frappe.model.set_value(cdt, cdn, "rejected_serial_no", "");
|
||||
}
|
||||
}
|
||||
|
||||
set_fields_onload_for_line_item() {
|
||||
if (this.frm.is_new() && this.frm.doc?.items) {
|
||||
this.frm.doc.items.forEach(item => {
|
||||
|
||||
@@ -263,6 +263,10 @@ $.extend(erpnext.utils, {
|
||||
fieldname: dimension["fieldname"],
|
||||
label: __(dimension["doctype"]),
|
||||
fieldtype: "MultiSelectList",
|
||||
depends_on:
|
||||
report_name === "Stock Balance"
|
||||
? "eval:doc.show_dimension_wise_stock === 1"
|
||||
: "",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options(dimension["doctype"], txt);
|
||||
},
|
||||
|
||||
@@ -91,7 +91,13 @@ erpnext.accounts.unreconcile_payment = {
|
||||
read_only: 1,
|
||||
options: "account_currency",
|
||||
},
|
||||
{ label: __("Currency"), fieldname: "account_currency", fieldtype: "Currency", read_only: 1 },
|
||||
{
|
||||
label: __("Currency"),
|
||||
fieldname: "account_currency",
|
||||
fieldtype: "Link",
|
||||
options: "Currency",
|
||||
read_only: 1,
|
||||
},
|
||||
];
|
||||
let unreconcile_dialog_fields = [
|
||||
{
|
||||
@@ -121,10 +127,10 @@ erpnext.accounts.unreconcile_payment = {
|
||||
};
|
||||
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: "UnReconcile Allocations",
|
||||
title: __("UnReconcile Allocations"),
|
||||
fields: unreconcile_dialog_fields,
|
||||
size: "large",
|
||||
primary_action_label: "UnReconcile",
|
||||
primary_action_label: __("UnReconcile"),
|
||||
primary_action(values) {
|
||||
let selected_allocations = values.allocations.filter((x) => x.__checked);
|
||||
if (selected_allocations.length > 0) {
|
||||
@@ -138,7 +144,7 @@ erpnext.accounts.unreconcile_payment = {
|
||||
);
|
||||
d.hide();
|
||||
} else {
|
||||
frappe.msgprint("No Selection");
|
||||
frappe.msgprint(__("No Selection"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,7 +18,6 @@ frappe.ui.form.on("Customer", {
|
||||
|
||||
frm.add_fetch("lead_name", "company_name", "customer_name");
|
||||
frm.add_fetch("default_sales_partner", "commission_rate", "default_commission_rate");
|
||||
frm.set_query("customer_group", { is_group: 0 });
|
||||
frm.set_query("default_price_list", { selling: 1 });
|
||||
frm.set_query("account", "accounts", function (doc, cdt, cdn) {
|
||||
let d = locals[cdt][cdn];
|
||||
|
||||
@@ -15,7 +15,7 @@ def get_data():
|
||||
"transactions": [
|
||||
{"label": _("Pre Sales"), "items": ["Opportunity", "Quotation"]},
|
||||
{"label": _("Orders"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]},
|
||||
{"label": _("Payments"), "items": ["Payment Entry", "Bank Account"]},
|
||||
{"label": _("Payments"), "items": ["Payment Entry", "Bank Account", "Dunning"]},
|
||||
{
|
||||
"label": _("Support"),
|
||||
"items": ["Issue", "Maintenance Visit", "Installation Note", "Warranty Claim"],
|
||||
|
||||
@@ -69,6 +69,8 @@ frappe.ui.form.on("Quotation", {
|
||||
erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController {
|
||||
onload(doc, dt, dn) {
|
||||
super.onload(doc, dt, dn);
|
||||
|
||||
this.frm.trigger("disable_customer_if_creating_from_opportunity");
|
||||
}
|
||||
party_name() {
|
||||
var me = this;
|
||||
@@ -373,6 +375,12 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
disable_customer_if_creating_from_opportunity(doc) {
|
||||
if (doc.opportunity) {
|
||||
this.frm.set_df_property("party_name", "read_only", 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cur_frm.script_manager.make(erpnext.selling.QuotationController);
|
||||
|
||||
@@ -875,7 +875,7 @@ def make_material_request(source_name, target_doc=None):
|
||||
"field_map": {
|
||||
"name": "sales_order_item",
|
||||
"parent": "sales_order",
|
||||
"delivery_date": "required_by",
|
||||
"delivery_date": "schedule_date",
|
||||
"bom_no": "bom_no",
|
||||
},
|
||||
"condition": lambda item: not frappe.db.exists(
|
||||
|
||||
@@ -23,10 +23,18 @@ frappe.listview_settings["Sales Order"] = {
|
||||
} else if (!doc.skip_delivery_note && flt(doc.per_delivered) < 100) {
|
||||
if (frappe.datetime.get_diff(doc.delivery_date) < 0) {
|
||||
// not delivered & overdue
|
||||
return [__("Overdue"), "red", "per_delivered,<,100|delivery_date,<,Today|status,!=,Closed"];
|
||||
return [
|
||||
__("Overdue"),
|
||||
"red",
|
||||
"per_delivered,<,100|delivery_date,<,Today|status,!=,Closed|docstatus,=,1",
|
||||
];
|
||||
} else if (flt(doc.grand_total) === 0) {
|
||||
// not delivered (zeroount order)
|
||||
return [__("To Deliver"), "orange", "per_delivered,<,100|grand_total,=,0|status,!=,Closed"];
|
||||
return [
|
||||
__("To Deliver"),
|
||||
"orange",
|
||||
"per_delivered,<,100|grand_total,=,0|status,!=,Closed|docstatus,=,1",
|
||||
];
|
||||
} else if (flt(doc.per_billed) < 100) {
|
||||
// not delivered & not billed
|
||||
return [
|
||||
|
||||
@@ -149,6 +149,26 @@ erpnext.PointOfSale.Controller = class {
|
||||
this.make_app();
|
||||
},
|
||||
});
|
||||
|
||||
frappe.realtime.on(`poe_${this.pos_opening}_closed`, (data) => {
|
||||
const route = frappe.get_route_str();
|
||||
if (data && route == "point-of-sale") {
|
||||
frappe.dom.freeze();
|
||||
frappe.msgprint({
|
||||
title: __("POS Closed"),
|
||||
indicator: "orange",
|
||||
message: __("POS has been closed at {0}. Please refresh the page.", [
|
||||
frappe.datetime.str_to_user(data.creation).bold(),
|
||||
]),
|
||||
primary_action_label: __("Refresh"),
|
||||
primary_action: {
|
||||
action() {
|
||||
window.location.reload();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
set_opening_entry_status() {
|
||||
|
||||
@@ -639,7 +639,7 @@
|
||||
"doc_view": "List",
|
||||
"label": "Learn Sales Management",
|
||||
"type": "URL",
|
||||
"url": "https://frappe.school/courses/sales-management-course?utm_source=in_app"
|
||||
"url": "https://school.frappe.io/lms/courses/sales-management-course?utm_source=in_app"
|
||||
},
|
||||
{
|
||||
"label": "Point of Sale",
|
||||
@@ -676,5 +676,6 @@
|
||||
"type": "Dashboard"
|
||||
}
|
||||
],
|
||||
"title": "Selling"
|
||||
"title": "Selling",
|
||||
"type": "Workspace"
|
||||
}
|
||||
@@ -10,6 +10,14 @@ from frappe.model.document import Document
|
||||
from frappe.utils import cint, comma_and, create_batch, get_link_to_form
|
||||
from frappe.utils.background_jobs import get_job, is_job_enqueued
|
||||
|
||||
LEDGER_ENTRY_DOCTYPES = frozenset(
|
||||
(
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Stock Ledger Entry",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TransactionDeletionRecord(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -475,31 +483,31 @@ def get_doctypes_to_be_ignored():
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_deletion_doc_running(company: str | None = None, err_msg: str | None = None):
|
||||
if company:
|
||||
if running_deletion_jobs := frappe.db.get_all(
|
||||
"Transaction Deletion Record",
|
||||
filters={"docstatus": 1, "company": company, "status": "Running"},
|
||||
):
|
||||
if not err_msg:
|
||||
err_msg = ""
|
||||
frappe.throw(
|
||||
title=_("Deletion in Progress!"),
|
||||
msg=_("Transaction Deletion Document: {0} is running for this Company. {1}").format(
|
||||
get_link_to_form("Transaction Deletion Record", running_deletion_jobs[0].name), err_msg
|
||||
),
|
||||
)
|
||||
if not company:
|
||||
return
|
||||
|
||||
running_deletion_job = frappe.db.get_value(
|
||||
"Transaction Deletion Record",
|
||||
{"docstatus": 1, "company": company, "status": "Running"},
|
||||
"name",
|
||||
)
|
||||
|
||||
if not running_deletion_job:
|
||||
return
|
||||
|
||||
frappe.throw(
|
||||
title=_("Deletion in Progress!"),
|
||||
msg=_("Transaction Deletion Document: {0} is running for this Company. {1}").format(
|
||||
get_link_to_form("Transaction Deletion Record", running_deletion_job), err_msg or ""
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def check_for_running_deletion_job(doc, method=None):
|
||||
# Check if DocType has 'company' field
|
||||
if doc.doctype not in ("GL Entry", "Payment Ledger Entry", "Stock Ledger Entry"):
|
||||
df = qb.DocType("DocField")
|
||||
if (
|
||||
qb.from_(df)
|
||||
.select(df.parent)
|
||||
.where((df.fieldname == "company") & (df.parent == doc.doctype))
|
||||
.run()
|
||||
):
|
||||
is_deletion_doc_running(
|
||||
doc.company, _("Cannot make any transactions until the deletion job is completed")
|
||||
)
|
||||
if doc.doctype in LEDGER_ENTRY_DOCTYPES or not doc.meta.has_field("company"):
|
||||
return
|
||||
|
||||
is_deletion_doc_running(
|
||||
doc.company, _("Cannot make any transactions until the deletion job is completed")
|
||||
)
|
||||
|
||||
@@ -182,7 +182,7 @@ def add_standard_navbar_items():
|
||||
{
|
||||
"item_label": "Frappe School",
|
||||
"item_type": "Route",
|
||||
"route": "https://frappe.school?utm_source=in_app",
|
||||
"route": "https://frappe.io/school?utm_source=in_app",
|
||||
"is_standard": 1,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -19,9 +19,9 @@ frappe.listview_settings["Delivery Note"] = {
|
||||
} else if (doc.status === "Return Issued") {
|
||||
return [__("Return Issued"), "grey", "status,=,Return Issued"];
|
||||
} else if (flt(doc.per_billed, 2) < 100) {
|
||||
return [__("To Bill"), "orange", "per_billed,<,100"];
|
||||
return [__("To Bill"), "orange", "per_billed,<,100|docstatus,=,1"];
|
||||
} else if (flt(doc.per_billed, 2) === 100) {
|
||||
return [__("Completed"), "green", "per_billed,=,100"];
|
||||
return [__("Completed"), "green", "per_billed,=,100|docstatus,=,1"];
|
||||
}
|
||||
},
|
||||
onload: function (doclist) {
|
||||
|
||||
@@ -1196,7 +1196,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
|
||||
return out
|
||||
|
||||
|
||||
def get_purchase_voucher_details(doctype, item_code, document_name):
|
||||
def get_purchase_voucher_details(doctype, item_code, document_name=None):
|
||||
parent_doc = frappe.qb.DocType(doctype)
|
||||
child_doc = frappe.qb.DocType(doctype + " Item")
|
||||
|
||||
@@ -1215,9 +1215,11 @@ def get_purchase_voucher_details(doctype, item_code, document_name):
|
||||
)
|
||||
.where(parent_doc.docstatus == 1)
|
||||
.where(child_doc.item_code == item_code)
|
||||
.where(parent_doc.name != document_name)
|
||||
)
|
||||
|
||||
if document_name:
|
||||
query = query.where(parent_doc.name != document_name)
|
||||
|
||||
if doctype in ("Purchase Receipt", "Purchase Invoice"):
|
||||
query = query.select(parent_doc.posting_date, parent_doc.posting_time)
|
||||
query = query.orderby(
|
||||
|
||||
@@ -13,7 +13,7 @@ frappe.listview_settings["Material Request"] = {
|
||||
return [__("Completed"), "green"];
|
||||
}
|
||||
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) {
|
||||
return [__("Pending"), "orange", "per_ordered,=,0"];
|
||||
return [__("Pending"), "orange", "per_ordered,=,0|docstatus,=,1"];
|
||||
} else if (
|
||||
doc.docstatus == 1 &&
|
||||
flt(doc.per_ordered, precision) < 100 &&
|
||||
|
||||
@@ -16,13 +16,13 @@ frappe.listview_settings["Purchase Receipt"] = {
|
||||
} else if (doc.status === "Closed") {
|
||||
return [__("Closed"), "green", "status,=,Closed"];
|
||||
} else if (flt(doc.per_returned, 2) === 100) {
|
||||
return [__("Return Issued"), "grey", "per_returned,=,100"];
|
||||
return [__("Return Issued"), "grey", "per_returned,=,100|docstatus,=,1"];
|
||||
} else if (flt(doc.grand_total) !== 0 && flt(doc.per_billed, 2) == 0) {
|
||||
return [__("To Bill"), "orange", "per_billed,<,100"];
|
||||
return [__("To Bill"), "orange", "per_billed,<,100|docstatus,=,1"];
|
||||
} else if (flt(doc.per_billed, 2) > 0 && flt(doc.per_billed, 2) < 100) {
|
||||
return [__("Partly Billed"), "yellow", "per_billed,<,100"];
|
||||
return [__("Partly Billed"), "yellow", "per_billed,<,100|docstatus,=,1"];
|
||||
} else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) {
|
||||
return [__("Completed"), "green", "per_billed,=,100"];
|
||||
return [__("Completed"), "green", "per_billed,=,100|docstatus,=,1"];
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -4077,6 +4077,54 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
pr.reload()
|
||||
self.assertEqual(pr.status, "To Bill")
|
||||
|
||||
def test_recreate_stock_ledgers(self):
|
||||
item_code = "Test Item for Recreate Stock Ledgers"
|
||||
create_item(item_code)
|
||||
|
||||
pr = make_purchase_receipt(item_code=item_code, qty=10, rate=100)
|
||||
pr.submit()
|
||||
|
||||
sles = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_type": pr.doctype, "voucher_no": pr.name},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
self.assertTrue(sles)
|
||||
|
||||
for row in sles:
|
||||
doc = frappe.get_doc("Stock Ledger Entry", row)
|
||||
doc.delete()
|
||||
|
||||
sles = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_type": pr.doctype, "voucher_no": pr.name},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
self.assertFalse(sles)
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Repost Item Valuation",
|
||||
"based_on": "Transaction",
|
||||
"voucher_type": pr.doctype,
|
||||
"voucher_no": pr.name,
|
||||
"posting_date": pr.posting_date,
|
||||
"posting_time": pr.posting_time,
|
||||
"company": pr.company,
|
||||
"recreate_stock_ledgers": 1,
|
||||
}
|
||||
).submit()
|
||||
|
||||
sles = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_type": pr.doctype, "voucher_no": pr.name},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
self.assertTrue(sles)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"allow_negative_stock",
|
||||
"via_landed_cost_voucher",
|
||||
"allow_zero_rate",
|
||||
"recreate_stock_ledgers",
|
||||
"amended_from",
|
||||
"error_section",
|
||||
"error_log",
|
||||
@@ -220,12 +221,20 @@
|
||||
"label": "Reposting Data File",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.based_on == \"Transaction\"",
|
||||
"fieldname": "recreate_stock_ledgers",
|
||||
"fieldtype": "Check",
|
||||
"label": "Recreate Stock Ledgers"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-27 16:55:23.150146",
|
||||
"modified": "2025-03-31 12:38:20.566196",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Repost Item Valuation",
|
||||
@@ -274,7 +283,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ class RepostItemValuation(Document):
|
||||
items_to_be_repost: DF.Code | None
|
||||
posting_date: DF.Date
|
||||
posting_time: DF.Time | None
|
||||
recreate_stock_ledgers: DF.Check
|
||||
reposting_data_file: DF.Attach | None
|
||||
status: DF.Literal["Queued", "In Progress", "Completed", "Skipped", "Failed"]
|
||||
total_reposting_count: DF.Int
|
||||
@@ -74,6 +75,7 @@ class RepostItemValuation(Document):
|
||||
self.reset_field_values()
|
||||
self.set_company()
|
||||
self.validate_accounts_freeze()
|
||||
self.reset_recreate_stock_ledgers()
|
||||
|
||||
def validate_period_closing_voucher(self):
|
||||
# Period Closing Voucher
|
||||
@@ -105,6 +107,10 @@ class RepostItemValuation(Document):
|
||||
msg = f"Due to closing stock balance {name}, you cannot repost item valuation before {to_date}"
|
||||
frappe.throw(_(msg))
|
||||
|
||||
def reset_recreate_stock_ledgers(self):
|
||||
if self.recreate_stock_ledgers and self.based_on != "Transaction":
|
||||
self.recreate_stock_ledgers = 0
|
||||
|
||||
def get_closing_stock_balance(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
@@ -250,6 +256,16 @@ class RepostItemValuation(Document):
|
||||
filters,
|
||||
)
|
||||
|
||||
def recreate_stock_ledger_entries(self):
|
||||
"""Recreate Stock Ledger Entries for the transaction."""
|
||||
if self.based_on == "Transaction" and self.recreate_stock_ledgers:
|
||||
doc = frappe.get_doc(self.voucher_type, self.voucher_no)
|
||||
doc.docstatus = 2
|
||||
doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
|
||||
|
||||
doc.docstatus = 1
|
||||
doc.update_stock_ledger(allow_negative_stock=True)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Repost Item Valuation", ["warehouse", "item_code"], "item_warehouse")
|
||||
@@ -268,6 +284,9 @@ def repost(doc):
|
||||
if not frappe.flags.in_test:
|
||||
frappe.db.commit()
|
||||
|
||||
if doc.recreate_stock_ledgers:
|
||||
doc.recreate_stock_ledger_entries()
|
||||
|
||||
repost_sl_entries(doc)
|
||||
repost_gl_entries(doc)
|
||||
|
||||
@@ -291,7 +310,7 @@ def repost(doc):
|
||||
|
||||
status = "Failed"
|
||||
# If failed because of timeout, set status to In Progress
|
||||
if traceback and "timeout" in traceback.lower():
|
||||
if traceback and ("timeout" in traceback.lower() or "Deadlock found" in traceback):
|
||||
status = "In Progress"
|
||||
|
||||
if traceback:
|
||||
@@ -306,13 +325,14 @@ def repost(doc):
|
||||
},
|
||||
)
|
||||
|
||||
outgoing_email_account = frappe.get_cached_value(
|
||||
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
||||
)
|
||||
if status == "Failed":
|
||||
outgoing_email_account = frappe.get_cached_value(
|
||||
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
||||
)
|
||||
|
||||
if outgoing_email_account and not isinstance(e, RecoverableErrors):
|
||||
notify_error_to_stock_managers(doc, message)
|
||||
doc.set_status("Failed")
|
||||
if outgoing_email_account and not isinstance(e, RecoverableErrors):
|
||||
notify_error_to_stock_managers(doc, message)
|
||||
doc.set_status("Failed")
|
||||
finally:
|
||||
if not frappe.flags.in_test:
|
||||
frappe.db.commit()
|
||||
|
||||
@@ -1907,6 +1907,59 @@ class TestStockEntry(FrappeTestCase):
|
||||
self.assertEqual(sle.stock_value_difference, 100)
|
||||
self.assertEqual(sle.stock_value, 100 * i)
|
||||
|
||||
def test_stock_entry_amount(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
rm_item_code = "Test Stock Entry Amount 1"
|
||||
make_item(rm_item_code, {"is_stock_item": 1})
|
||||
|
||||
fg_item_code = "Test Repack Stock Entry Amount 1"
|
||||
make_item(fg_item_code, {"is_stock_item": 1})
|
||||
|
||||
make_stock_entry(
|
||||
item_code=rm_item_code,
|
||||
qty=1,
|
||||
to_warehouse=warehouse,
|
||||
basic_rate=200,
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=rm_item_code,
|
||||
qty=1,
|
||||
purpose="Repack",
|
||||
basic_rate=100,
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
se.items[0].s_warehouse = warehouse
|
||||
se.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": fg_item_code,
|
||||
"qty": 1,
|
||||
"t_warehouse": warehouse,
|
||||
"uom": "Nos",
|
||||
"conversion_factor": 1.0,
|
||||
},
|
||||
)
|
||||
se.set_stock_entry_type()
|
||||
se.submit()
|
||||
|
||||
self.assertEqual(se.items[0].amount, 200)
|
||||
self.assertEqual(se.items[0].basic_amount, 200)
|
||||
|
||||
make_stock_entry(
|
||||
item_code=rm_item_code,
|
||||
qty=1,
|
||||
to_warehouse=warehouse,
|
||||
basic_rate=300,
|
||||
posting_date=add_days(nowdate(), -1),
|
||||
)
|
||||
|
||||
se.reload()
|
||||
self.assertEqual(se.items[0].amount, 300)
|
||||
self.assertEqual(se.items[0].basic_amount, 300)
|
||||
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
"allow_to_edit_stock_uom_qty_for_sales",
|
||||
"column_break_lznj",
|
||||
"allow_to_edit_stock_uom_qty_for_purchase",
|
||||
"section_break_ylhd",
|
||||
"allow_uom_with_conversion_rate_defined_in_item",
|
||||
"stock_validations_tab",
|
||||
"section_break_9",
|
||||
"over_delivery_receipt_allowance",
|
||||
@@ -490,6 +492,17 @@
|
||||
{
|
||||
"fieldname": "column_break_wslv",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ylhd",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, the system will allow selecting UOMs in sales and purchase transactions only if the conversion rate is set in the item master.",
|
||||
"fieldname": "allow_uom_with_conversion_rate_defined_in_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow UOM with Conversion Rate Defined in Item"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -497,7 +510,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-28 16:08:35.938840",
|
||||
"modified": "2025-03-31 15:34:20.752065",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Settings",
|
||||
@@ -518,7 +531,7 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
|
||||
@@ -33,6 +33,7 @@ class StockSettings(Document):
|
||||
allow_partial_reservation: DF.Check
|
||||
allow_to_edit_stock_uom_qty_for_purchase: DF.Check
|
||||
allow_to_edit_stock_uom_qty_for_sales: DF.Check
|
||||
allow_uom_with_conversion_rate_defined_in_item: DF.Check
|
||||
auto_create_serial_and_batch_bundle_for_outward: DF.Check
|
||||
auto_indent: DF.Check
|
||||
auto_insert_price_list_rate_if_missing: DF.Check
|
||||
|
||||
@@ -406,16 +406,17 @@ class StockBalanceReport:
|
||||
},
|
||||
]
|
||||
|
||||
for dimension in get_inventory_dimensions():
|
||||
columns.append(
|
||||
{
|
||||
"label": _(dimension.doctype),
|
||||
"fieldname": dimension.fieldname,
|
||||
"fieldtype": "Link",
|
||||
"options": dimension.doctype,
|
||||
"width": 110,
|
||||
}
|
||||
)
|
||||
if self.filters.get("show_dimension_wise_stock"):
|
||||
for dimension in get_inventory_dimensions():
|
||||
columns.append(
|
||||
{
|
||||
"label": _(dimension.doctype),
|
||||
"fieldname": dimension.fieldname,
|
||||
"fieldtype": "Link",
|
||||
"options": dimension.doctype,
|
||||
"width": 110,
|
||||
}
|
||||
)
|
||||
|
||||
columns.extend(
|
||||
[
|
||||
|
||||
@@ -184,11 +184,16 @@ def validate_serial_no(sle):
|
||||
frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
|
||||
|
||||
|
||||
def validate_cancellation(args):
|
||||
if args[0].get("is_cancelled"):
|
||||
def validate_cancellation(kargs):
|
||||
if kargs[0].get("is_cancelled"):
|
||||
repost_entry = frappe.db.get_value(
|
||||
"Repost Item Valuation",
|
||||
{"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
|
||||
{
|
||||
"voucher_type": kargs[0].voucher_type,
|
||||
"voucher_no": kargs[0].voucher_no,
|
||||
"docstatus": 1,
|
||||
"recreate_stock_ledgers": 0,
|
||||
},
|
||||
["name", "status"],
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -1233,7 +1238,11 @@ class update_entries_after:
|
||||
stock_entry.db_update()
|
||||
for d in stock_entry.items:
|
||||
# Update only the row that matches the voucher_detail_no or the row containing the FG/Scrap Item.
|
||||
if d.name == voucher_detail_no or (not d.s_warehouse and d.t_warehouse):
|
||||
if (
|
||||
d.name == voucher_detail_no
|
||||
or (not d.s_warehouse and d.t_warehouse)
|
||||
or stock_entry.purpose in ["Manufacture", "Repack"]
|
||||
):
|
||||
d.db_update()
|
||||
|
||||
def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
|
||||
|
||||
@@ -802,7 +802,7 @@
|
||||
"doc_view": "List",
|
||||
"label": "Learn Inventory Management",
|
||||
"type": "URL",
|
||||
"url": "https://frappe.school/courses/inventory-management?utm_source=in_app"
|
||||
"url": "https://school.frappe.io/lms/courses/inventory-management?utm_source=in_app"
|
||||
},
|
||||
{
|
||||
"color": "Yellow",
|
||||
|
||||
@@ -475,7 +475,7 @@ def get_repeated(values):
|
||||
|
||||
|
||||
def get_documents_with_active_service_level_agreement():
|
||||
sla_doctypes = frappe.cache().hget("service_level_agreement", "active")
|
||||
sla_doctypes = frappe.cache.get_value("doctypes_with_active_sla")
|
||||
|
||||
if sla_doctypes is None:
|
||||
return set_documents_with_active_service_level_agreement()
|
||||
@@ -484,20 +484,22 @@ def get_documents_with_active_service_level_agreement():
|
||||
|
||||
|
||||
def set_documents_with_active_service_level_agreement():
|
||||
active = [
|
||||
active = frozenset(
|
||||
sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])
|
||||
]
|
||||
frappe.cache().hset("service_level_agreement", "active", active)
|
||||
)
|
||||
frappe.cache.set_value("doctypes_with_active_sla", active)
|
||||
return active
|
||||
|
||||
|
||||
def apply(doc, method=None):
|
||||
# Applies SLA to document on validate
|
||||
flags = frappe.local.flags
|
||||
|
||||
if (
|
||||
frappe.flags.in_patch
|
||||
or frappe.flags.in_migrate
|
||||
or frappe.flags.in_install
|
||||
or frappe.flags.in_setup_wizard
|
||||
flags.in_patch
|
||||
or flags.in_migrate
|
||||
or flags.in_install
|
||||
or flags.in_setup_wizard
|
||||
or doc.doctype not in get_documents_with_active_service_level_agreement()
|
||||
):
|
||||
return
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<p>
|
||||
<a href="/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn={{ doc.name }}&dt={{ doc.doctype }}&submit_doc=1&order_type=Shopping Cart"
|
||||
class="btn btn-primary btn-sm" id="pay-for-order">
|
||||
{{ _("Pay", null, "Amount") }} {{doc.get_formatted("grand_total") }}
|
||||
{{ _("Pay", null, "Amount") }} {{ pay_amount }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ from frappe import _
|
||||
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import (
|
||||
ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST,
|
||||
get_amount,
|
||||
)
|
||||
|
||||
|
||||
@@ -50,11 +51,7 @@ def get_context(context):
|
||||
)
|
||||
context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points"))
|
||||
|
||||
context.show_pay_button = (
|
||||
"payments" in frappe.get_installed_apps()
|
||||
and frappe.db.get_single_value("Buying Settings", "show_pay_button")
|
||||
and context.doc.doctype in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST
|
||||
)
|
||||
context.show_pay_button, context.pay_amount = get_payment_details(context.doc)
|
||||
context.show_make_pi_button = False
|
||||
if context.doc.get("supplier"):
|
||||
# show Make Purchase Invoice button based on permission
|
||||
@@ -67,3 +64,19 @@ def get_attachments(dt, dn):
|
||||
fields=["name", "file_name", "file_url", "is_private"],
|
||||
filters={"attached_to_name": dn, "attached_to_doctype": dt, "is_private": 0},
|
||||
)
|
||||
|
||||
|
||||
def get_payment_details(doc):
|
||||
show_pay_button, amount = (
|
||||
(
|
||||
"payments" in frappe.get_installed_apps()
|
||||
and frappe.db.get_single_value("Buying Settings", "show_pay_button")
|
||||
and doc.doctype in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST
|
||||
),
|
||||
0,
|
||||
)
|
||||
if not show_pay_button:
|
||||
return show_pay_button, amount
|
||||
|
||||
amount = get_amount(doc)
|
||||
return bool(amount), amount
|
||||
|
||||
@@ -508,9 +508,9 @@ Close Loan,Darlehen schließen,
|
||||
Close the POS,Schließen Sie die Kasse,
|
||||
Closed,Geschlossen,
|
||||
Closed order cannot be cancelled. Unclose to cancel.,Geschlosser Auftrag kann nicht abgebrochen werden. Bitte wiedereröffnen um abzubrechen.,
|
||||
Closing (Cr),Schlußstand (Haben),
|
||||
Closing (Dr),Schlußstand (Soll),
|
||||
Closing (Opening + Total),Schließen (Eröffnung + Gesamt),
|
||||
Closing (Cr),Schlußstand (H),
|
||||
Closing (Dr),Schlußstand (S),
|
||||
Closing (Opening + Total),Schlußstand (Anfangssstand + Summe),
|
||||
Closing Account {0} must be of type Liability / Equity,Abschlußkonto {0} muss vom Typ Verbindlichkeiten/Eigenkapital sein,
|
||||
Closing Balance,Schlussbilanz,
|
||||
Code,Code,
|
||||
@@ -598,7 +598,7 @@ Course Code: ,Kurscode:,
|
||||
Course Enrollment {0} does not exists,Die Kursanmeldung {0} existiert nicht,
|
||||
Course Schedule,Kurstermine,
|
||||
Course: ,Kurs:,
|
||||
Cr,Haben,
|
||||
Cr,H,
|
||||
Create,Erstellen,
|
||||
Create BOM,Stückliste anlegen,
|
||||
Create Delivery Trip,Erstelle Auslieferungsfahrt,
|
||||
@@ -647,8 +647,8 @@ Creating Fees,Gebühren anlegen,
|
||||
Creating student groups,Erstelle Studentengruppen,
|
||||
Creating {0} Invoice,{0} Rechnung erstellen,
|
||||
Credit,Haben,
|
||||
Credit ({0}),Guthaben ({0}),
|
||||
Credit Account,Guthabenkonto,
|
||||
Credit ({0}),Haben ({0}),
|
||||
Credit Account,Haben-Konto,
|
||||
Credit Balance,Verfügbarer Kredit,
|
||||
Credit Card,Kreditkarte,
|
||||
Credit Days cannot be a negative number,Kredit-Tage können keine negative Zahl sein,
|
||||
@@ -712,14 +712,14 @@ Date of Transaction,Datum der Transaktion,
|
||||
Day,Tag,
|
||||
Debit,Soll,
|
||||
Debit ({0}),Soll ({0}),
|
||||
Debit Account,Sollkonto,
|
||||
Debit Account,Soll-Konto,
|
||||
Debit Note,Lastschrift,
|
||||
Debit Note Amount,Lastschriftbetrag,
|
||||
Debit Note Issued,Lastschrift ausgestellt am,
|
||||
Debit To is required,Debit Um erforderlich,
|
||||
Debit To is required,Forderungskonto ist erforderlich,
|
||||
Debit and Credit not equal for {0} #{1}. Difference is {2}.,Soll und Haben nicht gleich für {0} #{1}. Unterschied ist {2}.,
|
||||
Debtors,Schuldner,
|
||||
Debtors ({0}),Schuldnern ({0}),
|
||||
Debtors,Debitoren,
|
||||
Debtors ({0}),Debitoren ({0}),
|
||||
Declare Lost,Für verloren erklären,
|
||||
Default Activity Cost exists for Activity Type - {0},Es gibt Standard-Aktivitätskosten für Aktivitätsart - {0},
|
||||
Default BOM ({0}) must be active for this item or its template,Standardstückliste ({0}) muss für diesen Artikel oder dessen Vorlage aktiv sein,
|
||||
@@ -1628,7 +1628,7 @@ Open Item {0},Offene-Posten {0},
|
||||
Open Notifications,Offene Benachrichtigungen,
|
||||
Open Orders,Offene Bestellungen,
|
||||
Open a new ticket,Öffnen Sie ein neues Ticket,
|
||||
Opening,Eröffnung,
|
||||
Opening,Anfangssstand,
|
||||
Opening (Cr),Anfangssstand (Haben),
|
||||
Opening (Dr),Anfangsstand (Soll),
|
||||
Opening Accounting Balance,Eröffnungsbilanz,
|
||||
@@ -2215,8 +2215,8 @@ Retained Earnings,Gewinnrücklagen,
|
||||
Retention Stock Entry,Vorratsbestandseintrag,
|
||||
Retention Stock Entry already created or Sample Quantity not provided,Aufbewahrungsbestandseintrag bereits angelegt oder Musterbestand nicht bereitgestellt,
|
||||
Return,Zurück,
|
||||
Return / Credit Note,Return / Gutschrift,
|
||||
Return / Debit Note,Return / Lastschrift,
|
||||
Return / Credit Note,Rückgabe / Gutschrift,
|
||||
Return / Debit Note,Rückgabe / Lastschrift,
|
||||
Returns,Retouren,
|
||||
Reverse Journal Entry,Buchungssatz umkehren,
|
||||
Review Invitation Sent,Einladung überprüfen gesendet,
|
||||
@@ -2826,7 +2826,7 @@ To {0} | {1} {2},An {0} | {1} {2},
|
||||
Toggle Filters,Filter umschalten,
|
||||
Too many columns. Export the report and print it using a spreadsheet application.,Zu viele Spalten. Exportieren Sie den Bericht und drucken Sie ihn mit einem Tabellenkalkulationsprogramm aus.,
|
||||
Tools,Werkzeuge,
|
||||
Total (Credit),Insgesamt (Credit),
|
||||
Total (Credit),Insgesamt (Haben),
|
||||
Total (Without Tax),Summe (ohne Steuern),
|
||||
Total Achieved,Gesamtsumme erreicht,
|
||||
Total Actual,Summe Tatsächlich,
|
||||
@@ -2837,7 +2837,7 @@ Total Budget,Gesamtbudget; Gesamtetat,
|
||||
Total Collected: {0},Gesammelt gesammelt: {0},
|
||||
Total Commission,Gesamtprovision,
|
||||
Total Contribution Amount: {0},Gesamtbeitragsbetrag: {0},
|
||||
Total Credit/ Debit Amount should be same as linked Journal Entry,Der Gesamtkreditbetrag sollte identisch mit dem verknüpften Buchungssatz sein,
|
||||
Total Credit/ Debit Amount should be same as linked Journal Entry,Der gesamte Soll-/ Habenbetrag sollte identisch mit dem verknüpften Buchungssatz sein,
|
||||
Total Debit must be equal to Total Credit. The difference is {0},Gesamt-Soll muss gleich Gesamt-Haben sein. Die Differenz ist {0},
|
||||
Total Invoiced Amount,Gesamtrechnungsbetrag,
|
||||
Total Order Considered,Geschätzte Summe der Bestellungen,
|
||||
@@ -2915,7 +2915,7 @@ Unable to find score starting at {0}. You need to have standing scores covering
|
||||
Unable to find variable: ,Variable kann nicht gefunden werden:,
|
||||
Unblock Invoice,Rechnung entsperren,
|
||||
Uncheck all,Alle abwählen,
|
||||
Unclosed Fiscal Years Profit / Loss (Credit),Offener Gewinn / Verlust (Kredit) des Geschäftsjahres,
|
||||
Unclosed Fiscal Years Profit / Loss (Credit),Offener Gewinn / Verlust (Haben) des Geschäftsjahres,
|
||||
Unit,Einheit,
|
||||
Unit of Measure,Maßeinheit,
|
||||
Unit of Measure {0} has been entered more than once in Conversion Factor Table,Die Mengeneinheit {0} wurde mehr als einmal in die Umrechnungsfaktortabelle eingetragen.,
|
||||
@@ -3185,7 +3185,7 @@ on,Am,
|
||||
{0} {1}: Cost Center is required for 'Profit and Loss' account {2}. Please set up a default Cost Center for the Company.,"{0} {1}: Kostenstelle ist erforderlich für ""Gewinn- und Verlust"" Konto {2}. Bitte erstellen Sie eine Standard-Kostenstelle für das Unternehmen.",
|
||||
{0} {1}: Cost Center {2} does not belong to Company {3},{0} {1}: Kostenstelle {2} gehört nicht zu Unternehmen {3},
|
||||
{0} {1}: Customer is required against Receivable account {2},{0} {1}: Für das Eingangskonto {2} ist ein Kunde erforderlich,
|
||||
{0} {1}: Either debit or credit amount is required for {2},{0} {1}: Debit- oder Kreditbetrag ist für {2} erforderlich,
|
||||
{0} {1}: Either debit or credit amount is required for {2},{0} {1}: Soll- oder Habenbetrag ist für {2} erforderlich,
|
||||
{0} {1}: Supplier is required against Payable account {2},{0} {1}: Für das Kreditorenkonto ist ein Lieferant erforderlich {2},
|
||||
{0}% Billed,{0}% berechnet,
|
||||
{0}% Delivered,{0}% geliefert,
|
||||
@@ -3408,7 +3408,7 @@ Do you want to submit the material request,Möchten Sie die Materialanfrage einr
|
||||
Doctype,DocType,
|
||||
Document {0} successfully uncleared,Dokument {0} wurde nicht erfolgreich gelöscht,
|
||||
Download Template,Vorlage herunterladen,
|
||||
Dr,Soll,
|
||||
Dr,S,
|
||||
Due Date,Fälligkeitsdatum,
|
||||
Duplicate,Duplizieren,
|
||||
Duplicate Project with Tasks,Projekt mit Aufgaben duplizieren,
|
||||
@@ -3889,7 +3889,7 @@ Operation Id,Arbeitsgang-ID,
|
||||
Partially ordered,teilweise geordnete,
|
||||
Please select company first,Bitte wählen Sie zuerst die Firma aus,
|
||||
Please select patient,Bitte wählen Sie Patient,
|
||||
Printed On ,Gedruckt auf,
|
||||
Printed On ,Gedruckt am,
|
||||
Projected qty,Geplante Menge,
|
||||
Sales person,Vertriebsmitarbeiter,
|
||||
Serial No {0} Created,Seriennummer {0} Erstellt,
|
||||
@@ -4338,9 +4338,9 @@ Auto Created,Automatisch erstellt,
|
||||
Stock User,Lager-Benutzer,
|
||||
Fiscal Year Company,Geschäftsjahr Unternehmen,
|
||||
Debit Amount,Soll-Betrag,
|
||||
Credit Amount,Guthaben-Summe,
|
||||
Credit Amount,Haben-Betrag,
|
||||
Debit Amount in Account Currency,Soll-Betrag in Kontowährung,
|
||||
Credit Amount in Account Currency,(Gut)Haben-Betrag in Kontowährung,
|
||||
Credit Amount in Account Currency,Haben-Betrag in Kontowährung,
|
||||
Voucher Detail No,Belegdetail-Nr.,
|
||||
Is Opening,Ist Eröffnungsbuchung,
|
||||
Is Advance,Ist Anzahlung,
|
||||
@@ -5699,13 +5699,13 @@ Is Master Data Processed,Werden Stammdaten verarbeitet?,
|
||||
Is Master Data Imported,Werden Stammdaten importiert?,
|
||||
Tally Creditors Account,Tally Gläubigerkonto,
|
||||
Creditors Account set in Tally,Gläubigerkonto in Tally eingestellt,
|
||||
Tally Debtors Account,Tally Debtors Account,
|
||||
Tally Debtors Account,Tally Debitorenkonto,
|
||||
Debtors Account set in Tally,Debitorenkonto in Tally eingestellt,
|
||||
Tally Company,Tally Company,
|
||||
Company Name as per Imported Tally Data,Firmenname gemäß Imported Tally Data,
|
||||
Tally Company,Tally Unternehmen,
|
||||
Company Name as per Imported Tally Data,Firmenname gemäß Importierten Tally-Daten,
|
||||
Default UOM,Standard-UOM,
|
||||
UOM in case unspecified in imported data,"UOM für den Fall, dass in importierten Daten nicht angegeben",
|
||||
ERPNext Company,ERPNext Company,
|
||||
ERPNext Company,ERPNext Unternehmen,
|
||||
Your Company set in ERPNext,Ihr Unternehmen in ERPNext eingestellt,
|
||||
Processed Files,Verarbeitete Dateien,
|
||||
Parties,Parteien,
|
||||
@@ -8845,7 +8845,6 @@ Column {0},Spalte {0},
|
||||
Field Mapping,Feldzuordnung,
|
||||
Not Specified,Keine Angabe,
|
||||
Update Type,Aktualisierungsart,
|
||||
Dr,Soll,
|
||||
End Time,Endzeit,
|
||||
Fetching...,Abrufen ...,
|
||||
"It seems that there is an issue with the server's stripe configuration. In case of failure, the amount will get refunded to your account.","Es scheint, dass ein Problem mit der Stripe-Konfiguration des Servers vorliegt. Im Falle eines Fehlers wird der Betrag Ihrem Konto gutgeschrieben.",
|
||||
|
||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user