Merge pull request #46940 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2025-04-08 18:37:09 +05:30
committed by GitHub
67 changed files with 1759 additions and 1142 deletions

View File

@@ -62,7 +62,7 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB
## Learning and community ## 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. 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. 3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users. 4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.

View File

@@ -8,6 +8,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt from frappe.utils import cint, flt
from erpnext import get_default_cost_center from erpnext import get_default_cost_center
@@ -373,8 +374,6 @@ def auto_reconcile_vouchers(
from_reference_date=None, from_reference_date=None,
to_reference_date=None, to_reference_date=None,
): ):
frappe.flags.auto_reconcile_vouchers = True
bank_transactions = get_bank_transactions(bank_account) bank_transactions = get_bank_transactions(bank_account)
if len(bank_transactions) > 10: if len(bank_transactions) > 10:
@@ -403,6 +402,8 @@ def auto_reconcile_vouchers(
def start_auto_reconcile( def start_auto_reconcile(
bank_transactions, from_date, to_date, filter_by_reference_date, from_reference_date, to_reference_date 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() reconciled, partially_reconciled = set(), set()
for transaction in bank_transactions: for transaction in bank_transactions:
linked_payments = get_linked_payments( linked_payments = get_linked_payments(
@@ -517,16 +518,23 @@ def subtract_allocations(gl_account, vouchers):
voucher_allocated_amounts = get_total_allocated_amount(voucher_docs) voucher_allocated_amounts = get_total_allocated_amount(voucher_docs)
for voucher in vouchers: for voucher in vouchers:
rows = voucher_allocated_amounts.get((voucher.get("doctype"), voucher.get("name"))) or [] if amount := get_allocated_amount(voucher_allocated_amounts, voucher, gl_account):
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"]:
voucher["paid_amount"] -= amount voucher["paid_amount"] -= amount
copied.append(voucher) copied.append(voucher)
return copied 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( def check_matching(
bank_account, bank_account,
company, company,
@@ -796,26 +804,20 @@ def get_je_matching_query(
je = frappe.qb.DocType("Journal Entry") je = frappe.qb.DocType("Journal Entry")
jea = frappe.qb.DocType("Journal Entry Account") 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_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) filter_by_date = je.posting_date.between(from_date, to_date)
if cint(filter_by_reference_date): if cint(filter_by_reference_date):
filter_by_date = je.cheque_date.between(from_reference_date, to_reference_date) filter_by_date = je.cheque_date.between(from_reference_date, to_reference_date)
query = ( subquery = (
frappe.qb.from_(jea) frappe.qb.from_(jea)
.join(je) .join(je)
.on(jea.parent == je.name) .on(jea.parent == je.name)
.select( .select(
(ref_rank + amount_rank + 1).as_("rank"), Sum(getattr(jea, amount_field)).as_("paid_amount"),
ConstantColumn("Journal Entry").as_("doctype"), ConstantColumn("Journal Entry").as_("doctype"),
je.name, je.name,
getattr(jea, amount_field).as_("paid_amount"),
je.cheque_no.as_("reference_no"), je.cheque_no.as_("reference_no"),
je.cheque_date.as_("reference_date"), je.cheque_date.as_("reference_date"),
je.pay_to_recd_from.as_("party"), je.pay_to_recd_from.as_("party"),
@@ -827,13 +829,26 @@ def get_je_matching_query(
.where(je.voucher_type != "Opening Entry") .where(je.voucher_type != "Opening Entry")
.where(je.clearance_date.isnull()) .where(je.clearance_date.isnull())
.where(jea.account == common_filters.bank_account) .where(jea.account == common_filters.bank_account)
.where(amount_equality if exact_match else getattr(jea, amount_field) > 0.0)
.where(filter_by_date) .where(filter_by_date)
.groupby(je.name)
.orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date) .orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
) )
if frappe.flags.auto_reconcile_vouchers is True: 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 return query

View File

@@ -2,27 +2,6 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Bank Transaction", { 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) { setup: function (frm) {
frm.set_query("party_type", function () { frm.set_query("party_type", function () {
return { 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 () { 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) { function set_bank_statement_filter(frm) {
frm.set_query("bank_statement", function () { frm.set_query("bank_statement", function () {
return { return {

View File

@@ -5,7 +5,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.docstatus import DocStatus from frappe.model.docstatus import DocStatus
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt from frappe.utils import flt, getdate
class BankTransaction(Document): class BankTransaction(Document):
@@ -84,16 +84,16 @@ class BankTransaction(Document):
if not self.payment_entries: if not self.payment_entries:
return return
pe = [] references = set()
for row in self.payment_entries: for row in self.payment_entries:
reference = (row.payment_document, row.payment_entry) reference = (row.payment_document, row.payment_entry)
if reference in pe: if reference in references:
frappe.throw( frappe.throw(
_("{0} {1} is allocated twice in this Bank Transaction").format( _("{0} {1} is allocated twice in this Bank Transaction").format(
row.payment_document, row.payment_entry row.payment_document, row.payment_entry
) )
) )
pe.append(reference) references.add(reference)
def update_allocated_amount(self): def update_allocated_amount(self):
allocated_amount = ( allocated_amount = (
@@ -104,6 +104,19 @@ class BankTransaction(Document):
self.allocated_amount = flt(allocated_amount, self.precision("allocated_amount")) self.allocated_amount = flt(allocated_amount, self.precision("allocated_amount"))
self.unallocated_amount = flt(unallocated_amount, self.precision("unallocated_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): def before_submit(self):
self.allocate_payment_entries() self.allocate_payment_entries()
self.set_status() self.set_status()
@@ -113,13 +126,14 @@ class BankTransaction(Document):
def before_update_after_submit(self): def before_update_after_submit(self):
self.validate_duplicate_references() self.validate_duplicate_references()
self.allocate_payment_entries()
self.update_allocated_amount() self.update_allocated_amount()
self.delink_old_payment_entries()
self.allocate_payment_entries()
self.set_status() self.set_status()
def on_cancel(self): def on_cancel(self):
for payment_entry in self.payment_entries: 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() self.set_status()
@@ -152,43 +166,55 @@ class BankTransaction(Document):
- 0 > a: Error: already over-allocated - 0 > a: Error: already over-allocated
- clear means: set the latest transaction date as clearance date - 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 remaining_amount = self.unallocated_amount
to_remove = []
payment_entry_docs = [(pe.payment_document, pe.payment_entry) for pe in self.payment_entries] 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) 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: for payment_entry in list(self.payment_entries):
if payment_entry.allocated_amount == 0.0: if payment_entry.allocated_amount != 0:
unallocated_amount, should_clear, latest_transaction = get_clearance_details( continue
self,
payment_entry, allocable_amount, should_clear, clearance_date = get_clearance_details(
pe_bt_allocations.get((payment_entry.payment_document, payment_entry.payment_entry)) self,
or [], 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: self.update_allocated_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)
@frappe.whitelist() @frappe.whitelist()
def remove_payment_entries(self): def remove_payment_entries(self):
@@ -199,14 +225,64 @@ class BankTransaction(Document):
def remove_payment_entry(self, payment_entry): def remove_payment_entry(self, payment_entry):
"Clear payment entry and clearance" "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) self.remove(payment_entry)
def clear_linked_payment_entry(self, payment_entry, for_cancel=False): def delink_payment_entry(self, payment_entry):
clearance_date = None if for_cancel else self.date if payment_entry.payment_document == "Bank Transaction":
set_voucher_clearance( self.update_linked_bank_transaction(payment_entry.payment_entry, allocated_amount=None)
payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self 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): def auto_set_party(self):
from erpnext.accounts.doctype.bank_transaction.auto_match_party import AutoMatchParty 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") 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. There should only be one bank gl entry for a voucher, except for JE.
Could be none for a Bank Transaction. For JE, there can be multiple bank gl entries for the same account.
But if a JE, could affect two banks. In this case, the allocable_amount will be the sum of amounts of all gl entries of the account.
Should only clear the voucher if all bank gles are allocated. 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_date = getdate(transaction.date)
transaction.unallocated_amount,
get_paid_amount(payment_entry, transaction.currency, gl_bank_account), if payment_entry.payment_document == "Bank Transaction":
) bt = frappe.db.get_value(
unmatched_gles = len(gles) "Bank Transaction",
latest_transaction = transaction payment_entry.payment_entry,
for gle in gles: ("unallocated_amount", "bank_account"),
if gle["gl_account"] == gl_bank_account: as_dict=True,
if gle["amount"] <= 0.0: )
frappe.throw(
_("Voucher {0} value is broken: {1}").format(payment_entry.payment_entry, gle["amount"]) 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 return abs(bt.unallocated_amount), True, transaction_date
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 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 # nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
return frappe.db.sql( if not docs:
return {}
result = frappe.db.sql(
""" """
SELECT SELECT
ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount, gle.voucher_type AS doctype,
gle.account AS gl_account gle.voucher_no AS docname,
FROM gle.account AS gl_account,
`tabGL Entry` gle SUM(ABS(gle.credit_in_account_currency - gle.debit_in_account_currency)) AS amount
LEFT JOIN FROM
`tabAccount` ac ON ac.name=gle.account `tabGL Entry` gle
WHERE LEFT JOIN
ac.account_type = 'Bank' `tabAccount` ac ON ac.name = gle.account
AND gle.voucher_type = %(doctype)s WHERE
AND gle.voucher_no = %(docname)s ac.account_type = 'Bank'
AND is_cancelled = 0 AND (gle.voucher_type, gle.voucher_no) IN %(docs)s
""", AND gle.is_cancelled = 0
dict(doctype=doctype, docname=docname), GROUP BY
gle.voucher_type, gle.voucher_no, gle.account
""",
{"docs": docs},
as_dict=True, 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): def get_total_allocated_amount(docs):
""" """
Gets the sum of allocations for a voucher on each bank GL account 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 NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
""" """
if not docs: if not docs:
@@ -311,11 +423,10 @@ def get_total_allocated_amount(docs):
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql # nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
result = frappe.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 SELECT
ROW_NUMBER() OVER w AS rownum, ROW_NUMBER() OVER w AS rownum,
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account, btp.payment_document, btp.payment_entry) AS total, SUM(btp.allocated_amount) OVER(PARTITION BY ba.account, 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, FIRST_VALUE(bt.date) OVER w AS latest_date,
ba.account AS gl_account, ba.account AS gl_account,
btp.payment_document, btp.payment_document,
@@ -338,104 +449,14 @@ def get_total_allocated_amount(docs):
payment_allocation_details = {} payment_allocation_details = {}
for row in result: for row in result:
# Why is this *sometimes* a byte string? row["latest_date"] = getdate(row["latest_date"])
if isinstance(row["latest_name"], bytes): payment_allocation_details.setdefault((row["payment_document"], row["payment_entry"]), {})[
row["latest_name"] = row["latest_name"].decode() row["gl_account"]
row["latest_date"] = frappe.utils.getdate(row["latest_date"]) ] = row
payment_allocation_details.setdefault((row["payment_document"], row["payment_entry"]), []).append(row)
return payment_allocation_details 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): def get_reconciled_bank_transactions(doctype, docname):
return frappe.get_all( return frappe.get_all(
"Bank Transaction Payments", "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): def remove_from_bank_transaction(doctype, docname):
"""Remove a (cancelled) voucher from all Bank Transactions.""" """Remove a (cancelled) voucher from all Bank Transactions."""
for bt_name in get_reconciled_bank_transactions(doctype, docname): for bt_name in get_reconciled_bank_transactions(doctype, docname):

View File

@@ -220,6 +220,7 @@ def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str |
if not language: if not language:
language = doc.get("language") language = doc.get("language")
letter_text = None
if language: if language:
letter_text = frappe.db.get_value( letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1 DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1

View File

@@ -249,16 +249,18 @@ class PaymentEntry(AccountsController):
reference_names.add(key) reference_names.add(key)
def set_bank_account_data(self): def set_bank_account_data(self):
if self.bank_account: if not self.bank_account:
bank_data = get_bank_account_details(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 field = "paid_from" if self.payment_type == "Pay" else "paid_to"
self.bank_account_no = bank_data.bank_account_no
if not self.get(field): self.bank = bank_data.bank
self.set(field, bank_data.account) 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): def validate_payment_type_with_outstanding(self):
total_outstanding = sum(d.allocated_amount for d in self.get("references")) 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"): if self.party_type in ("Customer", "Supplier"):
self.validate_allocated_amount_with_latest_data() self.validate_allocated_amount_with_latest_data()
else: return
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 fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount): for d in self.get("references"):
frappe.throw(fail_message.format(d.idx)) 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): def validate_allocated_amount_as_per_payment_request(self):
""" """
@@ -322,91 +325,89 @@ class PaymentEntry(AccountsController):
return False return False
def validate_allocated_amount_with_latest_data(self): def validate_allocated_amount_with_latest_data(self):
if self.references: if not self.references:
uniq_vouchers = set([(x.reference_doctype, x.reference_name) for x in self.references]) return
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,
)
# Group latest_references by (voucher_type, voucher_no) uniq_vouchers = {(x.reference_doctype, x.reference_name) for x in self.references}
latest_lookup = {} vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in uniq_vouchers]
for d in latest_references: latest_references = get_outstanding_reference_documents(
d = frappe._dict(d) {
latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d "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): # Group latest_references by (voucher_type, voucher_no)
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict() 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 for idx, d in enumerate(self.get("references"), start=1):
if ( latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
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))
)
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key # If term based allocation is enabled, throw
latest = latest.get(d.payment_term) or latest.get(None) if (
# The reference has already been fully paid d.payment_term is None or d.payment_term == ""
if not latest: ) and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name):
frappe.throw( frappe.throw(
_("{0} {1} has already been fully paid.").format( _(
_(d.reference_doctype), d.reference_name "{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))
) )
# 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)
)
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 ( fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
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)
)
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): if (
frappe.throw(fail_message.format(d.idx)) 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):
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount): frappe.throw(fail_message.format(d.idx))
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): def delink_advance_entry_references(self):
for reference in self.references: for reference in self.references:
@@ -479,47 +480,48 @@ class PaymentEntry(AccountsController):
reference_exchange_details: dict | None = None, reference_exchange_details: dict | None = None,
) -> None: ) -> None:
for d in self.get("references"): for d in self.get("references"):
if d.allocated_amount: if not d.allocated_amount:
if update_ref_details_only_for and ( continue
(d.reference_doctype, d.reference_name) not in update_ref_details_only_for
): 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 continue
ref_details = get_reference_details( if field == "exchange_rate" or not d.get(field) or force:
d.reference_doctype, d.db_set(field, value)
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)
def validate_payment_type(self): def validate_payment_type(self):
if self.payment_type not in ("Receive", "Pay", "Internal Transfer"): if self.payment_type not in ("Receive", "Pay", "Internal Transfer"):
frappe.throw(_("Payment Type must be one of Receive, Pay and Internal Transfer")) frappe.throw(_("Payment Type must be one of Receive, Pay and Internal Transfer"))
def validate_party_details(self): def validate_party_details(self):
if self.party: if self.party and not frappe.db.exists(self.party_type, 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))
frappe.throw(_("{0} {1} does not exist").format(_(self.party_type), self.party))
def set_exchange_rate(self, ref_doc=None): def set_exchange_rate(self, ref_doc=None):
self.set_source_exchange_rate(ref_doc) self.set_source_exchange_rate(ref_doc)
@@ -529,12 +531,8 @@ class PaymentEntry(AccountsController):
if self.paid_from: if self.paid_from:
if self.paid_from_account_currency == self.company_currency: if self.paid_from_account_currency == self.company_currency:
self.source_exchange_rate = 1 self.source_exchange_rate = 1
else: elif ref_doc and self.paid_from_account_currency == ref_doc.currency:
if ref_doc: self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
if 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: if not self.source_exchange_rate:
self.source_exchange_rate = get_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: if self.paid_from_account_currency == self.paid_to_account_currency:
self.target_exchange_rate = self.source_exchange_rate self.target_exchange_rate = self.source_exchange_rate
elif self.paid_to and not self.target_exchange_rate: elif self.paid_to and not self.target_exchange_rate:
if ref_doc: if ref_doc and self.paid_to_account_currency == ref_doc.currency:
if self.paid_to_account_currency == ref_doc.currency: self.target_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
self.target_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
if not self.target_exchange_rate: if not self.target_exchange_rate:
self.target_exchange_rate = get_exchange_rate( self.target_exchange_rate = get_exchange_rate(
@@ -578,63 +575,61 @@ class PaymentEntry(AccountsController):
elif d.reference_name: elif d.reference_name:
if not frappe.db.exists(d.reference_doctype, 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)) 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": ref_doc = frappe.get_doc(d.reference_doctype, d.reference_name)
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()
if d.reference_doctype in frappe.get_hooks("invoice_doctypes"): if d.reference_doctype != "Journal Entry":
if self.party_type == "Customer": if self.party != ref_doc.get(scrub(self.party_type)):
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( 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): def get_valid_reference_doctypes(self):
if self.party_type == "Customer": if self.party_type == "Customer":
return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning", "Payment Entry") 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": elif self.party_type == "Supplier":
return ("Purchase Order", "Purchase Invoice", "Journal Entry", "Payment Entry") 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): def validate_paid_invoices(self):
no_oustanding_refs = {} no_oustanding_refs = {}
@@ -700,37 +695,39 @@ class PaymentEntry(AccountsController):
invoice_paid_amount_map = {} invoice_paid_amount_map = {}
for ref in self.get("references"): for ref in self.get("references"):
if ref.payment_term and ref.reference_name: if not ref.payment_term or not ref.reference_name:
key = (ref.payment_term, ref.reference_name, ref.reference_doctype) continue
invoice_payment_amount_map.setdefault(key, 0.0)
invoice_payment_amount_map[key] += ref.allocated_amount
if not invoice_paid_amount_map.get(key): key = (ref.payment_term, ref.reference_name, ref.reference_doctype)
payment_schedule = frappe.get_all( invoice_payment_amount_map.setdefault(key, 0.0)
"Payment Schedule", invoice_payment_amount_map[key] += ref.allocated_amount
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": if not invoice_paid_amount_map.get(key):
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * ( payment_schedule = frappe.get_all(
term.discount / 100 "Payment Schedule",
) filters={"parent": ref.reference_name},
else: fields=[
invoice_paid_amount_map[invoice_key]["discounted_amt"] = term.discount "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): for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1):
if not invoice_paid_amount_map.get(key): if not invoice_paid_amount_map.get(key):
@@ -977,14 +974,14 @@ class PaymentEntry(AccountsController):
applicable_tax = 0 applicable_tax = 0
base_applicable_tax = 0 base_applicable_tax = 0
for tax in self.get("taxes"): for tax in self.get("taxes"):
if not tax.included_in_paid_amount: if tax.included_in_paid_amount:
amount = -1 * tax.tax_amount if tax.add_deduct_tax == "Deduct" else tax.tax_amount continue
base_amount = (
-1 * tax.base_tax_amount if tax.add_deduct_tax == "Deduct" else tax.base_tax_amount
)
applicable_tax += amount amount = -1 * tax.tax_amount if tax.add_deduct_tax == "Deduct" else tax.tax_amount
base_applicable_tax += base_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( self.paid_amount_after_tax = flt(
flt(self.paid_amount) + flt(applicable_tax), self.precision("paid_amount_after_tax") 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): def add_deductions_gl_entries(self, gl_entries):
for d in self.get("deductions"): for d in self.get("deductions"):
if d.amount: if not d.amount:
account_currency = get_account_currency(d.account) continue
if account_currency != self.company_currency:
frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency))
gl_entries.append( account_currency = get_account_currency(d.account)
self.get_gl_dict( if account_currency != self.company_currency:
{ frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency))
"account": d.account,
"account_currency": account_currency, gl_entries.append(
"against": self.party or self.paid_from, self.get_gl_dict(
"debit_in_account_currency": d.amount, {
"debit_in_transaction_currency": d.amount / self.transaction_exchange_rate, "account": d.account,
"debit": d.amount, "account_currency": account_currency,
"cost_center": d.cost_center, "against": self.party or self.paid_from,
}, "debit_in_account_currency": d.amount,
item=d, "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): def get_party_account_for_taxes(self):
if self.payment_type == "Receive": if self.payment_type == "Receive":

View File

@@ -1,9 +1,9 @@
import json import json
import frappe import frappe
from frappe import _, qb from frappe import _
from frappe.model.document import Document 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 import flt, nowdate
from frappe.utils.background_jobs import enqueue from frappe.utils.background_jobs import enqueue
@@ -12,7 +12,6 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
) )
from erpnext.accounts.doctype.payment_entry.payment_entry import ( from erpnext.accounts.doctype.payment_entry.payment_entry import (
get_company_defaults,
get_payment_entry, get_payment_entry,
) )
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
@@ -120,13 +119,13 @@ class PaymentRequest(Document):
title=_("Invalid Amount"), 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) 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": if not hasattr(ref_doc, "order_type") or ref_doc.order_type != "Shopping Cart":
ref_amount = get_amount(ref_doc, self.payment_account) 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: if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
frappe.throw( frappe.throw(
@@ -544,6 +543,8 @@ def make_payment_request(**args):
gateway_account = get_gateway_details(args) or frappe._dict() gateway_account = get_gateway_details(args) or frappe._dict()
grand_total = get_amount(ref_doc, gateway_account.get("payment_account")) 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": if args.loyalty_points and args.dt == "Sales Order":
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points 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) frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False)
grand_total = grand_total - loyalty_amount 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 # fetches existing payment request `grand_total` amount
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name) existing_payment_request_amount = get_existing_payment_request_amount(ref_doc)
existing_paid_amount = get_existing_paid_amount(ref_doc.doctype, ref_doc.name)
def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount): def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount):
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 args.order_type == "Shopping Cart":
# If Payment Request is in an advanced stage, then create for remaining amount. # If Payment Request is in an advanced stage, then create for remaining amount.
if get_existing_payment_request_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) grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
else: else:
@@ -587,14 +577,10 @@ def make_payment_request(**args):
else: else:
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount) grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
if existing_paid_amount: draft_payment_request = frappe.db.get_value(
if ref_doc.party_account_currency == ref_doc.currency: "Payment Request",
if ref_doc.conversion_rate: {"reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "docstatus": 0},
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate) )
else:
grand_total -= flt(existing_paid_amount)
else:
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
if draft_payment_request: if draft_payment_request:
frappe.db.set_value( frappe.db.set_value(
@@ -602,6 +588,11 @@ def make_payment_request(**args):
) )
pr = frappe.get_doc("Payment Request", draft_payment_request) pr = frappe.get_doc("Payment Request", draft_payment_request)
else: 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") pr = frappe.new_doc("Payment Request")
if not args.get("payment_request_type"): if not args.get("payment_request_type"):
@@ -675,22 +666,35 @@ def make_payment_request(**args):
def get_amount(ref_doc, payment_account=None): def get_amount(ref_doc, payment_account=None):
"""get amount based on doctype""" """get amount based on doctype"""
grand_total = 0
dt = ref_doc.doctype dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]: 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"]: 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: 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: else:
grand_total = flt( grand_total = flt(flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate)
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
elif dt == "POS Invoice": elif dt == "POS Invoice":
for pay in ref_doc.payments: for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account: if pay.type == "Phone" and pay.account == payment_account:
@@ -699,10 +703,7 @@ def get_amount(ref_doc, payment_account=None):
elif dt == "Fees": elif dt == "Fees":
grand_total = ref_doc.outstanding_amount grand_total = ref_doc.outstanding_amount
if grand_total > 0: return flt(grand_total, get_currency_precision()) if grand_total > 0 else 0
return flt(grand_total, get_currency_precision())
else:
frappe.throw(_("Payment Entry is already created"))
def get_irequest_status(payment_requests: None | list = None) -> list: 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") 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. 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 = ( query = (
frappe.qb.from_(PR) frappe.qb.from_(PR)
.select(Sum(PR.grand_total)) .select(Sum(PR.outstanding_amount))
.where(PR.reference_doctype == ref_dt) .where(PR.reference_doctype == ref_doc.doctype)
.where(PR.reference_name == ref_dn) .where(PR.reference_name == ref_doc.name)
.where(PR.docstatus == 1) .where(PR.docstatus == 1)
) )
@@ -764,43 +765,12 @@ def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None =
response = query.run() 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): return os_amount_in_transaction_currency
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
def get_gateway_details(args): # nosemgrep def get_gateway_details(args): # nosemgrep

View File

@@ -313,6 +313,16 @@ class TestPaymentRequest(FrappeTestCase):
self.assertEqual(pr.outstanding_amount, 800) self.assertEqual(pr.outstanding_amount, 800)
self.assertEqual(pr.grand_total, 1000) 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 # complete payment
pe = pr.create_payment_entry() pe = pr.create_payment_entry()
@@ -331,7 +341,7 @@ class TestPaymentRequest(FrappeTestCase):
# creating a more payment Request must not allowed # creating a more payment Request must not allowed
self.assertRaisesRegex( self.assertRaisesRegex(
frappe.exceptions.ValidationError, frappe.exceptions.ValidationError,
re.compile(r"Payment Request is already created"), re.compile(r"Payment Entry is already created"),
make_payment_request, make_payment_request,
dt="Sales Order", dt="Sales Order",
dn=so.name, dn=so.name,
@@ -361,6 +371,17 @@ class TestPaymentRequest(FrappeTestCase):
self.assertEqual(pr.party_account_currency, "INR") self.assertEqual(pr.party_account_currency, "INR")
self.assertEqual(pr.status, "Initiated") 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 # to make partial payment
pe = pr.create_payment_entry(submit=False) pe = pr.create_payment_entry(submit=False)
pe.paid_amount = 2000 pe.paid_amount = 2000
@@ -389,7 +410,7 @@ class TestPaymentRequest(FrappeTestCase):
# creating a more payment Request must not allowed # creating a more payment Request must not allowed
self.assertRaisesRegex( self.assertRaisesRegex(
frappe.exceptions.ValidationError, frappe.exceptions.ValidationError,
re.compile(r"Payment Request is already created"), re.compile(r"Payment Entry is already created"),
make_payment_request, make_payment_request,
dt="Purchase Invoice", dt="Purchase Invoice",
dn=pi.name, dn=pi.name,

View File

@@ -124,6 +124,11 @@ class POSClosingEntry(StatusUpdater):
def on_submit(self): def on_submit(self):
consolidate_pos_invoices(closing_entry=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): def on_cancel(self):
unconsolidate_pos_invoices(closing_entry=self) unconsolidate_pos_invoices(closing_entry=self)

View File

@@ -196,6 +196,7 @@ class POSInvoice(SalesInvoice):
# run on validate method of selling controller # run on validate method of selling controller
super(SalesInvoice, self).validate() super(SalesInvoice, self).validate()
self.validate_pos_opening_entry()
self.validate_auto_set_posting_time() self.validate_auto_set_posting_time()
self.validate_mode_of_payment() self.validate_mode_of_payment()
self.validate_uom_is_integer("stock_uom", "stock_qty") 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) _("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): def validate_stock_availablility(self):
if self.is_return: if self.is_return:
return return

View File

@@ -26,6 +26,12 @@ class TestPOSInvoice(unittest.TestCase):
make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=800, basic_rate=100) make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=800, basic_rate=100)
frappe.db.sql("delete from `tabTax Rule`") 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): def tearDown(self):
if frappe.session.user != "Administrator": if frappe.session.user != "Administrator":
frappe.set_user("Administrator") frappe.set_user("Administrator")

View File

@@ -70,3 +70,6 @@ class POSOpeningEntry(StatusUpdater):
def on_submit(self): def on_submit(self):
self.set_status(update=True) self.set_status(update=True)
def on_cancel(self):
self.set_status(update=True)

View File

@@ -2094,7 +2094,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
1, 1,
) )
pi = make_pi_from_pr(pr.name) 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) automatically_fetch_payment_terms(enable=0)
frappe.db.set_value( frappe.db.set_value(
@@ -2683,6 +2683,78 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertRaises(StockOverReturnError, return_doc.save) 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): def set_advance_flag(company, flag, default_account):
frappe.db.set_value( frappe.db.set_value(

View File

@@ -267,8 +267,8 @@ class SalesInvoice(SellingController):
self.indicator_title = _("Paid") self.indicator_title = _("Paid")
def validate(self): def validate(self):
super().validate()
self.validate_auto_set_posting_time() self.validate_auto_set_posting_time()
super().validate()
if not (self.is_pos or self.is_debit_note): if not (self.is_pos or self.is_debit_note):
self.so_dn_required() self.so_dn_required()

View File

@@ -4284,6 +4284,35 @@ class TestSalesInvoice(FrappeTestCase):
pos_return = make_sales_return(pos.name) pos_return = make_sales_return(pos.name)
self.assertEqual(abs(pos_return.payments[0].amount), pos.payments[0].amount) 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): def test_prevents_fully_returned_invoice_with_zero_quantity(self):
from erpnext.controllers.sales_and_purchase_return import StockOverReturnError, make_return_doc from erpnext.controllers.sales_and_purchase_return import StockOverReturnError, make_return_doc

View File

@@ -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() { function get_party_type_options() {
let options = []; let options = [];

View File

@@ -21,7 +21,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
def tearDown(self): def tearDown(self):
frappe.db.rollback() 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") frappe.set_user("Administrator")
si = create_sales_invoice( si = create_sales_invoice(
item=self.item, item=self.item,
@@ -34,6 +34,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
rate=100, rate=100,
price_list_rate=100, price_list_rate=100,
do_not_save=1, do_not_save=1,
**args,
) )
if not no_payment_schedule: if not no_payment_schedule:
si.append( si.append(
@@ -108,7 +109,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note]) self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
pos_inv.cancel() pos_inv.cancel()
def test_accounts_receivable(self): def test_accounts_receivable_with_payment(self):
filters = { filters = {
"company": self.company, "company": self.company,
"based_on_payment_terms": 1, "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 = self.create_credit_note(si.name, do_not_submit=True)
cr_note.update_outstanding_for_self = False cr_note.update_outstanding_for_self = False
cr_note.save().submit() 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) 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( self.assertEqual(
expected_data_after_credit_note, 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): def test_cr_note_flag_to_update_self(self):
filters = { filters = {
"company": self.company, "company": self.company,

View File

@@ -52,11 +52,6 @@ frappe.query_reports["General Ledger"] = {
frappe.query_report.set_filter_value("group_by", "Group by Voucher (Consolidated)"); frappe.query_report.set_filter_value("group_by", "Group by Voucher (Consolidated)");
}, },
}, },
{
fieldname: "against_voucher_no",
label: __("Against Voucher No"),
fieldtype: "Data",
},
{ {
fieldtype: "Break", fieldtype: "Break",
}, },
@@ -66,7 +61,7 @@ frappe.query_reports["General Ledger"] = {
fieldtype: "Autocomplete", fieldtype: "Autocomplete",
options: Object.keys(frappe.boot.party_account_types), options: Object.keys(frappe.boot.party_account_types),
on_change: function () { on_change: function () {
frappe.query_report.set_filter_value("party", ""); frappe.query_report.set_filter_value("party", []);
}, },
}, },
{ {

View File

@@ -224,9 +224,6 @@ def get_conditions(filters):
if filters.get("voucher_no"): if filters.get("voucher_no"):
conditions.append("voucher_no=%(voucher_no)s") 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"): if filters.get("ignore_err"):
err_journals = frappe.db.get_all( err_journals = frappe.db.get_all(
"Journal Entry", "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] = 0
data[key][rev_dr_or_cr + "_in_account_currency"] = 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) from_date, to_date = getdate(filters.from_date), getdate(filters.to_date)
show_opening_entries = filters.get("show_opening_entries") show_opening_entries = filters.get("show_opening_entries")
@@ -695,14 +689,6 @@ def get_columns(filters):
columns.extend( 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}, {"label": _("Supplier Invoice No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 100},
] ]
) )

View File

@@ -11,7 +11,7 @@ import erpnext
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import ( from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
add_sub_total_row, add_sub_total_row,
add_total_row, add_total_row,
apply_group_by_conditions, apply_order_by_conditions,
get_grand_total, get_grand_total,
get_group_by_and_display_fields, get_group_by_and_display_fields,
get_tax_accounts, get_tax_accounts,
@@ -305,12 +305,6 @@ def apply_conditions(query, pi, pii, filters):
if filters.get("item_group"): if filters.get("item_group"):
query = query.where(pii.item_group == 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 return query
@@ -372,7 +366,17 @@ def get_items(filters, additional_table_columns):
query = apply_conditions(query, pi, pii, filters) 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(): def get_aii_accounts():

View File

@@ -384,27 +384,24 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
| (si.unrealized_profit_loss_account == filters.get("income_account")) | (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(): for key, value in (additional_conditions or {}).items():
query = query.where(si[key] == value) query = query.where(si[key] == value)
return query return query
def apply_group_by_conditions(query, si, ii, filters): def apply_order_by_conditions(query, si, ii, filters):
if filters.get("group_by") == "Invoice": if not filters.get("group_by"):
query = query.orderby(ii.parent, order=Order.desc) 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": 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": 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"): 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 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) 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): def get_delivery_notes_against_sales_order(item_list):

View File

@@ -397,7 +397,6 @@ def get_invoices(filters, additional_query_columns):
pi.mode_of_payment, pi.mode_of_payment,
) )
.where(pi.docstatus == 1) .where(pi.docstatus == 1)
.orderby(pi.posting_date, pi.name, order=Order.desc)
) )
if additional_query_columns: if additional_query_columns:
@@ -421,8 +420,17 @@ def get_invoices(filters, additional_query_columns):
) )
query = query.where(pi.credit_to.isin(party_account)) query = query.where(pi.credit_to.isin(party_account))
invoices = query.run(as_dict=True) from frappe.desk.reportview import build_match_conditions
return invoices
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): def get_conditions(filters, query, doctype):

View File

@@ -439,7 +439,6 @@ def get_invoices(filters, additional_query_columns):
si.company, si.company,
) )
.where(si.docstatus == 1) .where(si.docstatus == 1)
.orderby(si.posting_date, si.name, order=Order.desc)
) )
if additional_query_columns: 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" filters, query, doctype="Sales Invoice", child_doctype="Sales Invoice Item"
) )
invoices = query.run(as_dict=True) from frappe.desk.reportview import build_match_conditions
return invoices
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): def get_conditions(filters, query, doctype):

View File

@@ -85,6 +85,7 @@ class AccountsTestMixin:
"attribute_name": "bank", "attribute_name": "bank",
"account_name": "HDFC", "account_name": "HDFC",
"parent_account": "Bank Accounts - " + abbr, "parent_account": "Bank Accounts - " + abbr,
"account_type": "Bank",
} }
), ),
frappe._dict( frappe._dict(

View File

@@ -1854,14 +1854,17 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
): ):
outstanding = voucher_outstanding[0] outstanding = voucher_outstanding[0]
ref_doc = frappe.get_doc(voucher_type, voucher_no) 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 # 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( frappe.db.set_value(
voucher_type, voucher_type,
voucher_no, voucher_no,
"outstanding_amount", "outstanding_amount",
outstanding["outstanding_in_account_currency"] or 0.0, outstanding_amount,
) )
ref_doc.set_status(update=True) ref_doc.set_status(update=True)

View File

@@ -621,7 +621,7 @@
"doc_view": "List", "doc_view": "List",
"label": "Learn Accounting", "label": "Learn Accounting",
"type": "URL", "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", "label": "Chart of Accounts",

View File

@@ -22,15 +22,27 @@ frappe.listview_settings["Purchase Order"] = {
return [ return [
__("To Receive and Bill"), __("To Receive and Bill"),
"orange", "orange",
"per_received,<,100|per_billed,<,100|status,!=,Closed", "per_received,<,100|per_billed,<,100|status,!=,Closed|docstatus,=,1",
]; ];
} else { } 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") { } 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") { } 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) { onload: function (listview) {

View File

@@ -537,7 +537,7 @@
"doc_view": "List", "doc_view": "List",
"label": "Learn Procurement", "label": "Learn Procurement",
"type": "URL", "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", "color": "Yellow",

View File

@@ -165,6 +165,48 @@ class AccountsController(TransactionBase):
raise_exception=1, 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): def validate(self):
if not self.get("is_return") and not self.get("is_debit_note"): if not self.get("is_return") and not self.get("is_debit_note"):
self.validate_qty_is_not_zero() self.validate_qty_is_not_zero()
@@ -193,6 +235,7 @@ class AccountsController(TransactionBase):
self.disable_tax_included_prices_for_internal_transfer() self.disable_tax_included_prices_for_internal_transfer()
self.set_incoming_rate() self.set_incoming_rate()
self.init_internal_values() self.init_internal_values()
self.validate_against_voucher_outstanding()
# Need to set taxes based on taxes_and_charges template # Need to set taxes based on taxes_and_charges template
# before calculating taxes and totals # 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" 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)): if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
self.set_advances() self.set_advances()
@@ -2328,7 +2357,9 @@ class AccountsController(TransactionBase):
and automatically_fetch_payment_terms and automatically_fetch_payment_terms
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype) 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"): if self.get("payment_terms_template"):
self.ignore_default_payment_terms_template = 1 self.ignore_default_payment_terms_template = 1
elif self.get("payment_terms_template"): 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") d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
) )
else: 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 self.ignore_default_payment_terms_template = 1
def get_order_details(self): def get_order_details(self):
@@ -2410,7 +2443,9 @@ class AccountsController(TransactionBase):
def linked_order_has_payment_schedule(self, po_or_so): def linked_order_has_payment_schedule(self, po_or_so):
return frappe.get_all("Payment Schedule", filters={"parent": 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. 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, "invoice_portion": schedule.invoice_portion,
"mode_of_payment": schedule.mode_of_payment, "mode_of_payment": schedule.mode_of_payment,
"description": schedule.description, "description": schedule.description,
"payment_amount": schedule.payment_amount,
"base_payment_amount": schedule.base_payment_amount,
"outstanding": schedule.outstanding,
"paid_amount": schedule.paid_amount, "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": if schedule.discount_type == "Percentage":
payment_schedule["discount_type"] = schedule.discount_type payment_schedule["discount_type"] = schedule.discount_type
payment_schedule["discount"] = schedule.discount payment_schedule["discount"] = schedule.discount

View File

@@ -898,3 +898,32 @@ def get_filtered_child_rows(doctype, txt, searchfield, start, page_len, filters)
) )
return query.run(as_dict=False) 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,
)

View File

@@ -774,7 +774,7 @@ class StockController(AccountsController):
if row.get("batch_no"): if row.get("batch_no"):
update_values["batch_no"] = None 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 update_values["serial_and_batch_bundle"] = None
frappe.db.set_value( frappe.db.set_value(
"Serial and Batch Bundle", row.serial_and_batch_bundle, {"is_cancelled": 1} "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): 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( if allow_force_reposting and frappe.db.get_single_value(
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction" "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) 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( data = frappe.db.sql(
""" """
select item_code, warehouse, count(name) as total_row select item_code, warehouse, count(name) as total_row
from `tabStock Ledger Entry` force index (item_warehouse) from `tabStock Ledger Entry`
where where
({}) ({})
and timestamp(posting_date, posting_time) and posting_datetime >= %(posting_datetime)s
>= timestamp(%(posting_date)s, %(posting_time)s)
and voucher_no != %(voucher_no)s and voucher_no != %(voucher_no)s
and is_cancelled = 0 and is_cancelled = 0
GROUP BY GROUP BY

View File

@@ -743,7 +743,9 @@ class SubcontractingController(StockController):
): ):
continue 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( for bom_item in self.__get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items") row.item_code, row.bom, row.get("include_exploded_items")
): ):

View File

@@ -377,9 +377,7 @@ class calculate_taxes_and_totals:
self._calculate() self._calculate()
def calculate_taxes(self): def calculate_taxes(self):
rounding_adjustment_computed = self.doc.get("is_consolidated") and self.doc.get("rounding_adjustment") self.grand_total_diff = 0
if not rounding_adjustment_computed:
self.doc.rounding_adjustment = 0
# maintain actual tax rate based on idx # maintain actual tax rate based on idx
actual_tax_dict = dict( actual_tax_dict = dict(
@@ -446,9 +444,8 @@ class calculate_taxes_and_totals:
and self.discount_amount_applied and self.discount_amount_applied
and self.doc.discount_amount and self.doc.discount_amount
and self.doc.apply_discount_on == "Grand Total" 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.grand_total - flt(self.doc.discount_amount) - tax.total,
self.doc.precision("rounding_adjustment"), self.doc.precision("rounding_adjustment"),
) )
@@ -552,11 +549,11 @@ class calculate_taxes_and_totals:
return self.adjust_grand_total_for_inclusive_tax() return self.adjust_grand_total_for_inclusive_tax()
def adjust_grand_total_for_inclusive_tax(self): 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")): 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] last_tax = self.doc.get("taxes")[-1]
non_inclusive_tax_amount = sum( 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") for d in self.doc.get("taxes")
if not d.included_in_print_rate if not d.included_in_print_rate
) )
@@ -573,27 +570,23 @@ class calculate_taxes_and_totals:
diff = flt(diff, self.doc.precision("rounding_adjustment")) diff = flt(diff, self.doc.precision("rounding_adjustment"))
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")): if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
self.doc.grand_total_diff = diff self.grand_total_diff = diff
else:
self.doc.grand_total_diff = 0
def calculate_totals(self): def calculate_totals(self):
if self.doc.get("taxes"): if self.doc.get("taxes"):
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt( self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + self.grand_total_diff
self.doc.get("grand_total_diff")
)
else: else:
self.doc.grand_total = flt(self.doc.net_total) self.doc.grand_total = flt(self.doc.net_total)
if self.doc.get("taxes"): if self.doc.get("taxes"):
self.doc.total_taxes_and_charges = flt( 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"), self.doc.precision("total_taxes_and_charges"),
) )
else: else:
self.doc.total_taxes_and_charges = 0.0 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 [ if self.doc.doctype in [
"Quotation", "Quotation",
@@ -643,7 +636,9 @@ class calculate_taxes_and_totals:
if self.doc.meta.get_field("rounded_total"): if self.doc.meta.get_field("rounded_total"):
if self.doc.is_rounded_total_disabled(): 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 return
self.doc.rounded_total = round_based_on_smallest_currency_fraction( self.doc.rounded_total = round_based_on_smallest_currency_fraction(
@@ -687,33 +682,29 @@ class calculate_taxes_and_totals:
return return
total_for_discount_amount = self.get_total_for_discount_amount() total_for_discount_amount = self.get_total_for_discount_amount()
taxes = self.doc.get("taxes")
net_total = 0 net_total = 0
expected_net_total = 0
if total_for_discount_amount: if total_for_discount_amount:
# calculate item amount after Discount Amount # calculate item amount after Discount Amount
for i, item in enumerate(self._items): for item in self._items:
distributed_amount = ( distributed_amount = (
flt(self.doc.discount_amount) * item.net_amount / total_for_discount_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 net_total += item.net_amount
# discount amount rounding loss adjustment if no taxes # discount amount rounding adjustment
if ( if rounding_difference := flt(
self.doc.apply_discount_on == "Net Total" expected_net_total - net_total, self.doc.precision("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"),
)
item.net_amount = flt( 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 = ( item.net_rate = (
flt(item.net_amount / item.qty, item.precision("net_rate")) if item.qty else 0 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): def get_total_for_discount_amount(self):
if self.doc.apply_discount_on == "Net Total": if self.doc.apply_discount_on == "Net Total":
return self.doc.net_total return self.doc.net_total
else:
actual_taxes_dict = {}
for tax in self.doc.get("taxes"): total_actual_tax = 0
if tax.charge_type in ["Actual", "On Item Quantity"]: actual_taxes_dict = {}
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)
return flt( def update_actual_tax_dict(tax, tax_amount):
self.doc.grand_total - sum(actual_taxes_dict.values()), self.doc.precision("grand_total") 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): def calculate_total_advance(self):
if not self.doc.docstatus.is_cancelled(): if not self.doc.docstatus.is_cancelled():
@@ -804,9 +819,12 @@ class calculate_taxes_and_totals:
if ( if (
self.doc.is_return self.doc.is_return
and self.doc.return_against and self.doc.return_against
and not self.doc.update_outstanding_for_self
and not self.doc.get("is_pos") and not self.doc.get("is_pos")
or self.is_internal_invoice() 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 return
self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"]) self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"])

View File

@@ -1496,11 +1496,8 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
fields = ["name", "item_name", "item_group", "description"] fields = ["name", "item_name", "item_group", "description"]
fields.extend([field for field in searchfields if field not in ["name", "item_group", "description"]]) fields.extend([field for field in searchfields if field not in ["name", "item_group", "description"]])
searchfields = searchfields + [ if not searchfields:
field searchfields = ["name"]
for field in [searchfield or "name", "item_code", "item_group", "item_name"]
if field not in searchfields
]
query_filters = {"disabled": 0, "ifnull(end_of_life, '3099-12-31')": (">", today())} query_filters = {"disabled": 0, "ifnull(end_of_life, '3099-12-31')": (">", today())}

View File

@@ -1616,7 +1616,7 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
"posting_date": nowdate(), "posting_date": nowdate(),
"for_quantity": row.job_card_qty or work_order.get("qty", 0), "for_quantity": row.job_card_qty or work_order.get("qty", 0),
"operation_id": row.get("name"), "operation_id": row.get("name"),
"bom_no": row.get("bom"), "bom_no": row.get("bom") or work_order.bom_no,
"project": work_order.project, "project": work_order.project,
"company": work_order.company, "company": work_order.company,
"sequence_id": row.get("sequence_id"), "sequence_id": row.get("sequence_id"),

View File

@@ -336,7 +336,7 @@
"doc_view": "List", "doc_view": "List",
"label": "Learn Manufacturing", "label": "Learn Manufacturing",
"type": "URL", "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", "color": "Grey",

View File

@@ -210,7 +210,7 @@
"doc_view": "List", "doc_view": "List",
"label": "Learn Project Management", "label": "Learn Project Management",
"type": "URL", "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", "color": "Blue",

View File

@@ -343,7 +343,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
calculate_taxes() { calculate_taxes() {
var me = this; var me = this;
this.frm.doc.rounding_adjustment = 0; this.grand_total_diff = 0;
var actual_tax_dict = {}; var actual_tax_dict = {};
// maintain actual tax rate based on idx // 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 // adjust Discount Amount loss in last tax iteration
if ((i == me.frm.doc["taxes"].length - 1) && me.discount_amount_applied 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.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")); 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() { adjust_grand_total_for_inclusive_tax() {
var me = this; 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) { if (this.frm.doc["taxes"] && this.frm.doc["taxes"].length) {
var any_inclusive_tax = false; var any_inclusive_tax = false;
$.each(this.frm.doc.taxes || [], function(i, d) { $.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 || [], var non_inclusive_tax_amount = frappe.utils.sum($.map(this.frm.doc.taxes || [],
function(d) { function(d) {
if(!d.included_in_print_rate) { 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")); diff = flt(diff, precision("rounding_adjustment"));
if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) { if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) {
me.frm.doc.grand_total_diff = diff; me.grand_total_diff = diff;
} else {
me.frm.doc.grand_total_diff = 0;
} }
} }
} }
@@ -573,7 +574,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
var me = this; var me = this;
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0; var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0;
this.frm.doc.grand_total = flt(tax_count 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); : this.frm.doc.net_total);
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) { 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 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 // Round grand total as per precision
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "base_grand_total"]); 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)) { if (cint(disable_rounded_total)) {
this.frm.doc.rounded_total = 0; this.frm.doc.rounded_total = 0;
this.frm.doc.base_rounded_total = 0; this.frm.doc.base_rounded_total = 0;
this.frm.doc.rounding_adjustment = 0;
return; return;
} }
@@ -695,22 +697,26 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
return; return;
} }
var total_for_discount_amount = this.get_total_for_discount_amount(); const total_for_discount_amount = this.get_total_for_discount_amount();
var net_total = 0; let net_total = 0;
let expected_net_total = 0;
// calculate item amount after Discount Amount // calculate item amount after Discount Amount
if (total_for_discount_amount) { if (total_for_discount_amount) {
$.each(this.frm._items || [], function(i, item) { $.each(this.frm._items || [], function(i, item) {
distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount; 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; net_total += item.net_amount;
// discount amount rounding loss adjustment if no taxes // discount amount rounding adjustment
if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total")) // assignment to rounding_difference is intentional
&& i == (me.frm._items || []).length - 1) { const rounding_difference = flt(expected_net_total - net_total, precision("net_total"));
var discount_amount_loss = flt(me.frm.doc.net_total - net_total if (rounding_difference) {
- me.frm.doc.discount_amount, precision("net_total")); item.net_amount = flt(item.net_amount + rounding_difference, precision("net_amount", item));
item.net_amount = flt(item.net_amount + discount_amount_loss, net_total += rounding_difference;
precision("net_amount", item));
} }
item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0; 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"]); 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() { 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; 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) { let total_actual_tax = 0.0;
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) { let actual_taxes_dict = {};
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;
}
});
$.each(actual_taxes_dict, function(key, value) { function update_actual_taxes_dict(tax, tax_amount) {
if (value) total_actual_tax += value; 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) { calculate_total_advance(update_paid_amount) {

View File

@@ -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( if(
this.frm.docstatus < 2 this.frm.docstatus < 2
&& this.frm.fields_dict["payment_terms_template"] && 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() { set_fields_onload_for_line_item() {
if (this.frm.is_new() && this.frm.doc?.items) { if (this.frm.is_new() && this.frm.doc?.items) {
this.frm.doc.items.forEach(item => { this.frm.doc.items.forEach(item => {

View File

@@ -263,6 +263,10 @@ $.extend(erpnext.utils, {
fieldname: dimension["fieldname"], fieldname: dimension["fieldname"],
label: __(dimension["doctype"]), label: __(dimension["doctype"]),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
depends_on:
report_name === "Stock Balance"
? "eval:doc.show_dimension_wise_stock === 1"
: "",
get_data: function (txt) { get_data: function (txt) {
return frappe.db.get_link_options(dimension["doctype"], txt); return frappe.db.get_link_options(dimension["doctype"], txt);
}, },

View File

@@ -91,7 +91,13 @@ erpnext.accounts.unreconcile_payment = {
read_only: 1, read_only: 1,
options: "account_currency", 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 = [ let unreconcile_dialog_fields = [
{ {
@@ -121,10 +127,10 @@ erpnext.accounts.unreconcile_payment = {
}; };
let d = new frappe.ui.Dialog({ let d = new frappe.ui.Dialog({
title: "UnReconcile Allocations", title: __("UnReconcile Allocations"),
fields: unreconcile_dialog_fields, fields: unreconcile_dialog_fields,
size: "large", size: "large",
primary_action_label: "UnReconcile", primary_action_label: __("UnReconcile"),
primary_action(values) { primary_action(values) {
let selected_allocations = values.allocations.filter((x) => x.__checked); let selected_allocations = values.allocations.filter((x) => x.__checked);
if (selected_allocations.length > 0) { if (selected_allocations.length > 0) {
@@ -138,7 +144,7 @@ erpnext.accounts.unreconcile_payment = {
); );
d.hide(); d.hide();
} else { } else {
frappe.msgprint("No Selection"); frappe.msgprint(__("No Selection"));
} }
}, },
}); });

View File

@@ -18,7 +18,6 @@ frappe.ui.form.on("Customer", {
frm.add_fetch("lead_name", "company_name", "customer_name"); frm.add_fetch("lead_name", "company_name", "customer_name");
frm.add_fetch("default_sales_partner", "commission_rate", "default_commission_rate"); 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("default_price_list", { selling: 1 });
frm.set_query("account", "accounts", function (doc, cdt, cdn) { frm.set_query("account", "accounts", function (doc, cdt, cdn) {
let d = locals[cdt][cdn]; let d = locals[cdt][cdn];

View File

@@ -15,7 +15,7 @@ def get_data():
"transactions": [ "transactions": [
{"label": _("Pre Sales"), "items": ["Opportunity", "Quotation"]}, {"label": _("Pre Sales"), "items": ["Opportunity", "Quotation"]},
{"label": _("Orders"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]}, {"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"), "label": _("Support"),
"items": ["Issue", "Maintenance Visit", "Installation Note", "Warranty Claim"], "items": ["Issue", "Maintenance Visit", "Installation Note", "Warranty Claim"],

View File

@@ -69,6 +69,8 @@ frappe.ui.form.on("Quotation", {
erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController { erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController {
onload(doc, dt, dn) { onload(doc, dt, dn) {
super.onload(doc, dt, dn); super.onload(doc, dt, dn);
this.frm.trigger("disable_customer_if_creating_from_opportunity");
} }
party_name() { party_name() {
var me = this; 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); cur_frm.script_manager.make(erpnext.selling.QuotationController);

View File

@@ -875,7 +875,7 @@ def make_material_request(source_name, target_doc=None):
"field_map": { "field_map": {
"name": "sales_order_item", "name": "sales_order_item",
"parent": "sales_order", "parent": "sales_order",
"delivery_date": "required_by", "delivery_date": "schedule_date",
"bom_no": "bom_no", "bom_no": "bom_no",
}, },
"condition": lambda item: not frappe.db.exists( "condition": lambda item: not frappe.db.exists(

View File

@@ -23,10 +23,18 @@ frappe.listview_settings["Sales Order"] = {
} else if (!doc.skip_delivery_note && flt(doc.per_delivered) < 100) { } else if (!doc.skip_delivery_note && flt(doc.per_delivered) < 100) {
if (frappe.datetime.get_diff(doc.delivery_date) < 0) { if (frappe.datetime.get_diff(doc.delivery_date) < 0) {
// not delivered & overdue // 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) { } else if (flt(doc.grand_total) === 0) {
// not delivered (zeroount order) // 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) { } else if (flt(doc.per_billed) < 100) {
// not delivered & not billed // not delivered & not billed
return [ return [

View File

@@ -149,6 +149,26 @@ erpnext.PointOfSale.Controller = class {
this.make_app(); 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() { set_opening_entry_status() {

View File

@@ -639,7 +639,7 @@
"doc_view": "List", "doc_view": "List",
"label": "Learn Sales Management", "label": "Learn Sales Management",
"type": "URL", "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", "label": "Point of Sale",
@@ -676,5 +676,6 @@
"type": "Dashboard" "type": "Dashboard"
} }
], ],
"title": "Selling" "title": "Selling",
"type": "Workspace"
} }

View File

@@ -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 import cint, comma_and, create_batch, get_link_to_form
from frappe.utils.background_jobs import get_job, is_job_enqueued 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): class TransactionDeletionRecord(Document):
# begin: auto-generated types # begin: auto-generated types
@@ -475,31 +483,31 @@ def get_doctypes_to_be_ignored():
@frappe.whitelist() @frappe.whitelist()
def is_deletion_doc_running(company: str | None = None, err_msg: str | None = None): def is_deletion_doc_running(company: str | None = None, err_msg: str | None = None):
if company: if not company:
if running_deletion_jobs := frappe.db.get_all( return
"Transaction Deletion Record",
filters={"docstatus": 1, "company": company, "status": "Running"}, running_deletion_job = frappe.db.get_value(
): "Transaction Deletion Record",
if not err_msg: {"docstatus": 1, "company": company, "status": "Running"},
err_msg = "" "name",
frappe.throw( )
title=_("Deletion in Progress!"),
msg=_("Transaction Deletion Document: {0} is running for this Company. {1}").format( if not running_deletion_job:
get_link_to_form("Transaction Deletion Record", running_deletion_jobs[0].name), err_msg 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): def check_for_running_deletion_job(doc, method=None):
# Check if DocType has 'company' field # Check if DocType has 'company' field
if doc.doctype not in ("GL Entry", "Payment Ledger Entry", "Stock Ledger Entry"): if doc.doctype in LEDGER_ENTRY_DOCTYPES or not doc.meta.has_field("company"):
df = qb.DocType("DocField") return
if (
qb.from_(df) is_deletion_doc_running(
.select(df.parent) doc.company, _("Cannot make any transactions until the deletion job is completed")
.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")
)

View File

@@ -182,7 +182,7 @@ def add_standard_navbar_items():
{ {
"item_label": "Frappe School", "item_label": "Frappe School",
"item_type": "Route", "item_type": "Route",
"route": "https://frappe.school?utm_source=in_app", "route": "https://frappe.io/school?utm_source=in_app",
"is_standard": 1, "is_standard": 1,
}, },
{ {

View File

@@ -19,9 +19,9 @@ frappe.listview_settings["Delivery Note"] = {
} else if (doc.status === "Return Issued") { } else if (doc.status === "Return Issued") {
return [__("Return Issued"), "grey", "status,=,Return Issued"]; return [__("Return Issued"), "grey", "status,=,Return Issued"];
} else if (flt(doc.per_billed, 2) < 100) { } 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) { } 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) { onload: function (doclist) {

View File

@@ -1196,7 +1196,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
return out 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) parent_doc = frappe.qb.DocType(doctype)
child_doc = frappe.qb.DocType(doctype + " Item") 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(parent_doc.docstatus == 1)
.where(child_doc.item_code == item_code) .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"): if doctype in ("Purchase Receipt", "Purchase Invoice"):
query = query.select(parent_doc.posting_date, parent_doc.posting_time) query = query.select(parent_doc.posting_date, parent_doc.posting_time)
query = query.orderby( query = query.orderby(

View File

@@ -13,7 +13,7 @@ frappe.listview_settings["Material Request"] = {
return [__("Completed"), "green"]; return [__("Completed"), "green"];
} }
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) { } 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 ( } else if (
doc.docstatus == 1 && doc.docstatus == 1 &&
flt(doc.per_ordered, precision) < 100 && flt(doc.per_ordered, precision) < 100 &&

View File

@@ -16,13 +16,13 @@ frappe.listview_settings["Purchase Receipt"] = {
} else if (doc.status === "Closed") { } else if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"]; return [__("Closed"), "green", "status,=,Closed"];
} else if (flt(doc.per_returned, 2) === 100) { } 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) { } 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) { } 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) { } 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"];
} }
}, },

View File

@@ -4077,6 +4077,54 @@ class TestPurchaseReceipt(FrappeTestCase):
pr.reload() pr.reload()
self.assertEqual(pr.status, "To Bill") 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(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -19,6 +19,7 @@
"allow_negative_stock", "allow_negative_stock",
"via_landed_cost_voucher", "via_landed_cost_voucher",
"allow_zero_rate", "allow_zero_rate",
"recreate_stock_ledgers",
"amended_from", "amended_from",
"error_section", "error_section",
"error_log", "error_log",
@@ -220,12 +221,20 @@
"label": "Reposting Data File", "label": "Reposting Data File",
"no_copy": 1, "no_copy": 1,
"read_only": 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, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-06-27 16:55:23.150146", "modified": "2025-03-31 12:38:20.566196",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Repost Item Valuation", "name": "Repost Item Valuation",
@@ -274,7 +283,7 @@
"write": 1 "write": 1
} }
], ],
"sort_field": "modified", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []
} }

View File

@@ -47,6 +47,7 @@ class RepostItemValuation(Document):
items_to_be_repost: DF.Code | None items_to_be_repost: DF.Code | None
posting_date: DF.Date posting_date: DF.Date
posting_time: DF.Time | None posting_time: DF.Time | None
recreate_stock_ledgers: DF.Check
reposting_data_file: DF.Attach | None reposting_data_file: DF.Attach | None
status: DF.Literal["Queued", "In Progress", "Completed", "Skipped", "Failed"] status: DF.Literal["Queued", "In Progress", "Completed", "Skipped", "Failed"]
total_reposting_count: DF.Int total_reposting_count: DF.Int
@@ -74,6 +75,7 @@ class RepostItemValuation(Document):
self.reset_field_values() self.reset_field_values()
self.set_company() self.set_company()
self.validate_accounts_freeze() self.validate_accounts_freeze()
self.reset_recreate_stock_ledgers()
def validate_period_closing_voucher(self): def validate_period_closing_voucher(self):
# Period Closing Voucher # 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}" msg = f"Due to closing stock balance {name}, you cannot repost item valuation before {to_date}"
frappe.throw(_(msg)) 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): def get_closing_stock_balance(self):
filters = { filters = {
"company": self.company, "company": self.company,
@@ -250,6 +256,16 @@ class RepostItemValuation(Document):
filters, 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(): def on_doctype_update():
frappe.db.add_index("Repost Item Valuation", ["warehouse", "item_code"], "item_warehouse") 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: if not frappe.flags.in_test:
frappe.db.commit() frappe.db.commit()
if doc.recreate_stock_ledgers:
doc.recreate_stock_ledger_entries()
repost_sl_entries(doc) repost_sl_entries(doc)
repost_gl_entries(doc) repost_gl_entries(doc)
@@ -291,7 +310,7 @@ def repost(doc):
status = "Failed" status = "Failed"
# If failed because of timeout, set status to In Progress # 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" status = "In Progress"
if traceback: if traceback:
@@ -306,13 +325,14 @@ def repost(doc):
}, },
) )
outgoing_email_account = frappe.get_cached_value( if status == "Failed":
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name" 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): if outgoing_email_account and not isinstance(e, RecoverableErrors):
notify_error_to_stock_managers(doc, message) notify_error_to_stock_managers(doc, message)
doc.set_status("Failed") doc.set_status("Failed")
finally: finally:
if not frappe.flags.in_test: if not frappe.flags.in_test:
frappe.db.commit() frappe.db.commit()

View File

@@ -1907,6 +1907,59 @@ class TestStockEntry(FrappeTestCase):
self.assertEqual(sle.stock_value_difference, 100) self.assertEqual(sle.stock_value_difference, 100)
self.assertEqual(sle.stock_value, 100 * i) 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): def make_serialized_item(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -22,6 +22,8 @@
"allow_to_edit_stock_uom_qty_for_sales", "allow_to_edit_stock_uom_qty_for_sales",
"column_break_lznj", "column_break_lznj",
"allow_to_edit_stock_uom_qty_for_purchase", "allow_to_edit_stock_uom_qty_for_purchase",
"section_break_ylhd",
"allow_uom_with_conversion_rate_defined_in_item",
"stock_validations_tab", "stock_validations_tab",
"section_break_9", "section_break_9",
"over_delivery_receipt_allowance", "over_delivery_receipt_allowance",
@@ -490,6 +492,17 @@
{ {
"fieldname": "column_break_wslv", "fieldname": "column_break_wslv",
"fieldtype": "Column Break" "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", "icon": "icon-cog",
@@ -497,7 +510,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2025-02-28 16:08:35.938840", "modified": "2025-03-31 15:34:20.752065",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Settings", "name": "Stock Settings",
@@ -518,7 +531,7 @@
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "creation",
"sort_order": "ASC", "sort_order": "ASC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1

View File

@@ -33,6 +33,7 @@ class StockSettings(Document):
allow_partial_reservation: DF.Check allow_partial_reservation: DF.Check
allow_to_edit_stock_uom_qty_for_purchase: DF.Check allow_to_edit_stock_uom_qty_for_purchase: DF.Check
allow_to_edit_stock_uom_qty_for_sales: 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_create_serial_and_batch_bundle_for_outward: DF.Check
auto_indent: DF.Check auto_indent: DF.Check
auto_insert_price_list_rate_if_missing: DF.Check auto_insert_price_list_rate_if_missing: DF.Check

View File

@@ -406,16 +406,17 @@ class StockBalanceReport:
}, },
] ]
for dimension in get_inventory_dimensions(): if self.filters.get("show_dimension_wise_stock"):
columns.append( for dimension in get_inventory_dimensions():
{ columns.append(
"label": _(dimension.doctype), {
"fieldname": dimension.fieldname, "label": _(dimension.doctype),
"fieldtype": "Link", "fieldname": dimension.fieldname,
"options": dimension.doctype, "fieldtype": "Link",
"width": 110, "options": dimension.doctype,
} "width": 110,
) }
)
columns.extend( columns.extend(
[ [

View File

@@ -184,11 +184,16 @@ def validate_serial_no(sle):
frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction) frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
def validate_cancellation(args): def validate_cancellation(kargs):
if args[0].get("is_cancelled"): if kargs[0].get("is_cancelled"):
repost_entry = frappe.db.get_value( repost_entry = frappe.db.get_value(
"Repost Item Valuation", "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"], ["name", "status"],
as_dict=1, as_dict=1,
) )
@@ -1233,7 +1238,11 @@ class update_entries_after:
stock_entry.db_update() stock_entry.db_update()
for d in stock_entry.items: for d in stock_entry.items:
# Update only the row that matches the voucher_detail_no or the row containing the FG/Scrap Item. # 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() d.db_update()
def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate): def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):

View File

@@ -802,7 +802,7 @@
"doc_view": "List", "doc_view": "List",
"label": "Learn Inventory Management", "label": "Learn Inventory Management",
"type": "URL", "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", "color": "Yellow",

View File

@@ -475,7 +475,7 @@ def get_repeated(values):
def get_documents_with_active_service_level_agreement(): 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: if sla_doctypes is None:
return set_documents_with_active_service_level_agreement() 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(): 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"]) 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 return active
def apply(doc, method=None): def apply(doc, method=None):
# Applies SLA to document on validate # Applies SLA to document on validate
flags = frappe.local.flags
if ( if (
frappe.flags.in_patch flags.in_patch
or frappe.flags.in_migrate or flags.in_migrate
or frappe.flags.in_install or flags.in_install
or frappe.flags.in_setup_wizard or flags.in_setup_wizard
or doc.doctype not in get_documents_with_active_service_level_agreement() or doc.doctype not in get_documents_with_active_service_level_agreement()
): ):
return return

View File

@@ -40,7 +40,7 @@
<p> <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" <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"> class="btn btn-primary btn-sm" id="pay-for-order">
{{ _("Pay", null, "Amount") }} {{doc.get_formatted("grand_total") }} {{ _("Pay", null, "Amount") }} {{ pay_amount }}
</a> </a>
</p> </p>
</div> </div>

View File

@@ -6,6 +6,7 @@ from frappe import _
from erpnext.accounts.doctype.payment_request.payment_request import ( from erpnext.accounts.doctype.payment_request.payment_request import (
ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST, 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.available_loyalty_points = int(loyalty_program_details.get("loyalty_points"))
context.show_pay_button = ( context.show_pay_button, context.pay_amount = get_payment_details(context.doc)
"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_make_pi_button = False context.show_make_pi_button = False
if context.doc.get("supplier"): if context.doc.get("supplier"):
# show Make Purchase Invoice button based on permission # 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"], fields=["name", "file_name", "file_url", "is_private"],
filters={"attached_to_name": dn, "attached_to_doctype": dt, "is_private": 0}, 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

View File

@@ -508,9 +508,9 @@ Close Loan,Darlehen schließen,
Close the POS,Schließen Sie die Kasse, Close the POS,Schließen Sie die Kasse,
Closed,Geschlossen, Closed,Geschlossen,
Closed order cannot be cancelled. Unclose to cancel.,Geschlosser Auftrag kann nicht abgebrochen werden. Bitte wiedereröffnen um abzubrechen., 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 (Cr),Schlußstand (H),
Closing (Dr),Schlußstand (Soll), Closing (Dr),Schlußstand (S),
Closing (Opening + Total),Schließen (Eröffnung + Gesamt), 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 Account {0} must be of type Liability / Equity,Abschlußkonto {0} muss vom Typ Verbindlichkeiten/Eigenkapital sein,
Closing Balance,Schlussbilanz, Closing Balance,Schlussbilanz,
Code,Code, Code,Code,
@@ -598,7 +598,7 @@ Course Code: ,Kurscode:,
Course Enrollment {0} does not exists,Die Kursanmeldung {0} existiert nicht, Course Enrollment {0} does not exists,Die Kursanmeldung {0} existiert nicht,
Course Schedule,Kurstermine, Course Schedule,Kurstermine,
Course: ,Kurs:, Course: ,Kurs:,
Cr,Haben, Cr,H,
Create,Erstellen, Create,Erstellen,
Create BOM,Stückliste anlegen, Create BOM,Stückliste anlegen,
Create Delivery Trip,Erstelle Auslieferungsfahrt, Create Delivery Trip,Erstelle Auslieferungsfahrt,
@@ -647,8 +647,8 @@ Creating Fees,Gebühren anlegen,
Creating student groups,Erstelle Studentengruppen, Creating student groups,Erstelle Studentengruppen,
Creating {0} Invoice,{0} Rechnung erstellen, Creating {0} Invoice,{0} Rechnung erstellen,
Credit,Haben, Credit,Haben,
Credit ({0}),Guthaben ({0}), Credit ({0}),Haben ({0}),
Credit Account,Guthabenkonto, Credit Account,Haben-Konto,
Credit Balance,Verfügbarer Kredit, Credit Balance,Verfügbarer Kredit,
Credit Card,Kreditkarte, Credit Card,Kreditkarte,
Credit Days cannot be a negative number,Kredit-Tage können keine negative Zahl sein, 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, Day,Tag,
Debit,Soll, Debit,Soll,
Debit ({0}),Soll ({0}), Debit ({0}),Soll ({0}),
Debit Account,Sollkonto, Debit Account,Soll-Konto,
Debit Note,Lastschrift, Debit Note,Lastschrift,
Debit Note Amount,Lastschriftbetrag, Debit Note Amount,Lastschriftbetrag,
Debit Note Issued,Lastschrift ausgestellt am, 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}., 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,Debitoren,
Debtors ({0}),Schuldnern ({0}), Debtors ({0}),Debitoren ({0}),
Declare Lost,Für verloren erklären, 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 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, 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 Notifications,Offene Benachrichtigungen,
Open Orders,Offene Bestellungen, Open Orders,Offene Bestellungen,
Open a new ticket,Öffnen Sie ein neues Ticket, Open a new ticket,Öffnen Sie ein neues Ticket,
Opening,Eröffnung, Opening,Anfangssstand,
Opening (Cr),Anfangssstand (Haben), Opening (Cr),Anfangssstand (Haben),
Opening (Dr),Anfangsstand (Soll), Opening (Dr),Anfangsstand (Soll),
Opening Accounting Balance,Eröffnungsbilanz, Opening Accounting Balance,Eröffnungsbilanz,
@@ -2215,8 +2215,8 @@ Retained Earnings,Gewinnrücklagen,
Retention Stock Entry,Vorratsbestandseintrag, Retention Stock Entry,Vorratsbestandseintrag,
Retention Stock Entry already created or Sample Quantity not provided,Aufbewahrungsbestandseintrag bereits angelegt oder Musterbestand nicht bereitgestellt, Retention Stock Entry already created or Sample Quantity not provided,Aufbewahrungsbestandseintrag bereits angelegt oder Musterbestand nicht bereitgestellt,
Return,Zurück, Return,Zurück,
Return / Credit Note,Return / Gutschrift, Return / Credit Note,Rückgabe / Gutschrift,
Return / Debit Note,Return / Lastschrift, Return / Debit Note,Rückgabe / Lastschrift,
Returns,Retouren, Returns,Retouren,
Reverse Journal Entry,Buchungssatz umkehren, Reverse Journal Entry,Buchungssatz umkehren,
Review Invitation Sent,Einladung überprüfen gesendet, Review Invitation Sent,Einladung überprüfen gesendet,
@@ -2826,7 +2826,7 @@ To {0} | {1} {2},An {0} | {1} {2},
Toggle Filters,Filter umschalten, 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., 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, Tools,Werkzeuge,
Total (Credit),Insgesamt (Credit), Total (Credit),Insgesamt (Haben),
Total (Without Tax),Summe (ohne Steuern), Total (Without Tax),Summe (ohne Steuern),
Total Achieved,Gesamtsumme erreicht, Total Achieved,Gesamtsumme erreicht,
Total Actual,Summe Tatsächlich, Total Actual,Summe Tatsächlich,
@@ -2837,7 +2837,7 @@ Total Budget,Gesamtbudget; Gesamtetat,
Total Collected: {0},Gesammelt gesammelt: {0}, Total Collected: {0},Gesammelt gesammelt: {0},
Total Commission,Gesamtprovision, Total Commission,Gesamtprovision,
Total Contribution Amount: {0},Gesamtbeitragsbetrag: {0}, 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 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 Invoiced Amount,Gesamtrechnungsbetrag,
Total Order Considered,Geschätzte Summe der Bestellungen, 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:, Unable to find variable: ,Variable kann nicht gefunden werden:,
Unblock Invoice,Rechnung entsperren, Unblock Invoice,Rechnung entsperren,
Uncheck all,Alle abwählen, 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,Einheit,
Unit of Measure,Maß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., 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 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}: 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}: 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} {1}: Supplier is required against Payable account {2},{0} {1}: Für das Kreditorenkonto ist ein Lieferant erforderlich {2},
{0}% Billed,{0}% berechnet, {0}% Billed,{0}% berechnet,
{0}% Delivered,{0}% geliefert, {0}% Delivered,{0}% geliefert,
@@ -3408,7 +3408,7 @@ Do you want to submit the material request,Möchten Sie die Materialanfrage einr
Doctype,DocType, Doctype,DocType,
Document {0} successfully uncleared,Dokument {0} wurde nicht erfolgreich gelöscht, Document {0} successfully uncleared,Dokument {0} wurde nicht erfolgreich gelöscht,
Download Template,Vorlage herunterladen, Download Template,Vorlage herunterladen,
Dr,Soll, Dr,S,
Due Date,Fälligkeitsdatum, Due Date,Fälligkeitsdatum,
Duplicate,Duplizieren, Duplicate,Duplizieren,
Duplicate Project with Tasks,Projekt mit Aufgaben duplizieren, Duplicate Project with Tasks,Projekt mit Aufgaben duplizieren,
@@ -3889,7 +3889,7 @@ Operation Id,Arbeitsgang-ID,
Partially ordered,teilweise geordnete, Partially ordered,teilweise geordnete,
Please select company first,Bitte wählen Sie zuerst die Firma aus, Please select company first,Bitte wählen Sie zuerst die Firma aus,
Please select patient,Bitte wählen Sie Patient, Please select patient,Bitte wählen Sie Patient,
Printed On ,Gedruckt auf, Printed On ,Gedruckt am,
Projected qty,Geplante Menge, Projected qty,Geplante Menge,
Sales person,Vertriebsmitarbeiter, Sales person,Vertriebsmitarbeiter,
Serial No {0} Created,Seriennummer {0} Erstellt, Serial No {0} Created,Seriennummer {0} Erstellt,
@@ -4338,9 +4338,9 @@ Auto Created,Automatisch erstellt,
Stock User,Lager-Benutzer, Stock User,Lager-Benutzer,
Fiscal Year Company,Geschäftsjahr Unternehmen, Fiscal Year Company,Geschäftsjahr Unternehmen,
Debit Amount,Soll-Betrag, Debit Amount,Soll-Betrag,
Credit Amount,Guthaben-Summe, Credit Amount,Haben-Betrag,
Debit Amount in Account Currency,Soll-Betrag in Kontowährung, 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., Voucher Detail No,Belegdetail-Nr.,
Is Opening,Ist Eröffnungsbuchung, Is Opening,Ist Eröffnungsbuchung,
Is Advance,Ist Anzahlung, Is Advance,Ist Anzahlung,
@@ -5699,13 +5699,13 @@ Is Master Data Processed,Werden Stammdaten verarbeitet?,
Is Master Data Imported,Werden Stammdaten importiert?, Is Master Data Imported,Werden Stammdaten importiert?,
Tally Creditors Account,Tally Gläubigerkonto, Tally Creditors Account,Tally Gläubigerkonto,
Creditors Account set in Tally,Gläubigerkonto in Tally eingestellt, 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, Debtors Account set in Tally,Debitorenkonto in Tally eingestellt,
Tally Company,Tally Company, Tally Company,Tally Unternehmen,
Company Name as per Imported Tally Data,Firmenname gemäß Imported Tally Data, Company Name as per Imported Tally Data,Firmenname gemäß Importierten Tally-Daten,
Default UOM,Standard-UOM, Default UOM,Standard-UOM,
UOM in case unspecified in imported data,"UOM für den Fall, dass in importierten Daten nicht angegeben", 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, Your Company set in ERPNext,Ihr Unternehmen in ERPNext eingestellt,
Processed Files,Verarbeitete Dateien, Processed Files,Verarbeitete Dateien,
Parties,Parteien, Parties,Parteien,
@@ -8845,7 +8845,6 @@ Column {0},Spalte {0},
Field Mapping,Feldzuordnung, Field Mapping,Feldzuordnung,
Not Specified,Keine Angabe, Not Specified,Keine Angabe,
Update Type,Aktualisierungsart, Update Type,Aktualisierungsart,
Dr,Soll,
End Time,Endzeit, End Time,Endzeit,
Fetching...,Abrufen ..., 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.", "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.