mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-31 18:59:08 +00:00
Merge branch 'develop' into bank-transaction-entries
This commit is contained in:
30
.github/workflows/label-base-on-title.yml
vendored
Normal file
30
.github/workflows/label-base-on-title.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: "Auto-label PRs based on title"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add-label-if-prefix-matches:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check PR title and add label if it matches prefixes
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const title = context.payload.pull_request.title.toLowerCase();
|
||||||
|
const prefixes = ['chore', 'ci', 'style', 'test', 'refactor'];
|
||||||
|
|
||||||
|
// Check if the PR title starts with any of the prefixes
|
||||||
|
if (prefixes.some(prefix => title.startsWith(prefix))) {
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.payload.pull_request.number,
|
||||||
|
labels: ['skip-release-notes']
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
files:
|
files:
|
||||||
- source: /erpnext/locale/main.pot
|
- source: /erpnext/locale/main.pot
|
||||||
translation: /erpnext/locale/%locale_with_underscore%.po
|
translation: /erpnext/locale/%two_letters_code%.po
|
||||||
pull_request_title: "fix: sync translations from crowdin"
|
pull_request_title: "fix: sync translations from crowdin"
|
||||||
pull_request_labels:
|
pull_request_labels:
|
||||||
- translation
|
- translation
|
||||||
|
- skip-release-notes
|
||||||
pull_request_reviewers:
|
pull_request_reviewers:
|
||||||
- barredterra # change to your GitHub username if you copied this file
|
- barredterra # change to your GitHub username if you copied this file
|
||||||
commit_message: "fix: %language% translations"
|
commit_message: "fix: %language% translations"
|
||||||
append_commit_message: false
|
append_commit_message: false
|
||||||
|
languages_mapping:
|
||||||
|
two_letters_code:
|
||||||
|
pt-BR: pt_BR
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ def identify_is_group(child):
|
|||||||
return is_group
|
return is_group
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
def get_chart(chart_template, existing_company=None):
|
def get_chart(chart_template, existing_company=None):
|
||||||
chart = {}
|
chart = {}
|
||||||
if existing_company:
|
if existing_company:
|
||||||
|
|||||||
@@ -160,9 +160,6 @@ def get_payment_entries_for_bank_clearance(
|
|||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
if bank_account:
|
|
||||||
condition += "and bank_account = %(bank_account)s"
|
|
||||||
|
|
||||||
payment_entries = frappe.db.sql(
|
payment_entries = frappe.db.sql(
|
||||||
f"""
|
f"""
|
||||||
select
|
select
|
||||||
@@ -184,7 +181,6 @@ def get_payment_entries_for_bank_clearance(
|
|||||||
"account": account,
|
"account": account,
|
||||||
"from": from_date,
|
"from": from_date,
|
||||||
"to": to_date,
|
"to": to_date,
|
||||||
"bank_account": bank_account,
|
|
||||||
},
|
},
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ 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 () {
|
frm.set_query("payment_document", "payment_entries", function () {
|
||||||
const payment_doctypes = frm.events.get_payment_doctypes(frm);
|
const payment_doctypes = frm.events.get_payment_doctypes(frm);
|
||||||
@@ -47,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 {
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -105,7 +105,8 @@
|
|||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
"oldfieldname": "cost_center",
|
"oldfieldname": "cost_center",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Cost Center"
|
"options": "Cost Center",
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "debit",
|
"fieldname": "debit",
|
||||||
@@ -358,7 +359,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-02-21 14:36:49.431166",
|
"modified": "2025-03-21 15:29:11.221890",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "GL Entry",
|
"name": "GL Entry",
|
||||||
|
|||||||
@@ -579,8 +579,22 @@ class JournalEntry(AccountsController):
|
|||||||
if customers:
|
if customers:
|
||||||
from erpnext.selling.doctype.customer.customer import check_credit_limit
|
from erpnext.selling.doctype.customer.customer import check_credit_limit
|
||||||
|
|
||||||
|
customer_details = frappe._dict(
|
||||||
|
frappe.db.get_all(
|
||||||
|
"Customer Credit Limit",
|
||||||
|
filters={
|
||||||
|
"parent": ["in", customers],
|
||||||
|
"parenttype": ["=", "Customer"],
|
||||||
|
"company": ["=", self.company],
|
||||||
|
},
|
||||||
|
fields=["parent", "bypass_credit_limit_check"],
|
||||||
|
as_list=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
for customer in customers:
|
for customer in customers:
|
||||||
check_credit_limit(customer, self.company)
|
ignore_outstanding_sales_order = bool(customer_details.get(customer))
|
||||||
|
check_credit_limit(customer, self.company, ignore_outstanding_sales_order)
|
||||||
|
|
||||||
def validate_cheque_info(self):
|
def validate_cheque_info(self):
|
||||||
if self.voucher_type in ["Bank Entry"]:
|
if self.voucher_type in ["Bank Entry"]:
|
||||||
@@ -828,14 +842,13 @@ class JournalEntry(AccountsController):
|
|||||||
"Debit Note",
|
"Debit Note",
|
||||||
"Credit Note",
|
"Credit Note",
|
||||||
]:
|
]:
|
||||||
invoice = frappe.db.get_value(
|
invoice = frappe.get_doc(reference_type, reference_name)
|
||||||
reference_type, reference_name, ["docstatus", "outstanding_amount"], as_dict=1
|
|
||||||
)
|
|
||||||
|
|
||||||
if invoice.docstatus != 1:
|
if invoice.docstatus != 1:
|
||||||
frappe.throw(_("{0} {1} is not submitted").format(reference_type, reference_name))
|
frappe.throw(_("{0} {1} is not submitted").format(reference_type, reference_name))
|
||||||
|
|
||||||
if total and flt(invoice.outstanding_amount) < total:
|
precision = invoice.precision("outstanding_amount")
|
||||||
|
if total and flt(invoice.outstanding_amount, precision) < flt(total, precision):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Payment against {0} {1} cannot be greater than Outstanding Amount {2}").format(
|
_("Payment against {0} {1} cannot be greater than Outstanding Amount {2}").format(
|
||||||
reference_type, reference_name, invoice.outstanding_amount
|
reference_type, reference_name, invoice.outstanding_amount
|
||||||
|
|||||||
@@ -200,14 +200,14 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "party",
|
"depends_on": "eval: doc.party && doc.party_type !== \"Employee\"",
|
||||||
"fieldname": "contact_person",
|
"fieldname": "contact_person",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Contact",
|
"label": "Contact",
|
||||||
"options": "Contact"
|
"options": "Contact"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "contact_person",
|
"depends_on": "eval: (doc.contact_person || doc.party_type === \"Employee\") && doc.contact_email",
|
||||||
"fieldname": "contact_email",
|
"fieldname": "contact_email",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Email",
|
"label": "Email",
|
||||||
@@ -777,7 +777,7 @@
|
|||||||
"table_fieldname": "payment_entries"
|
"table_fieldname": "payment_entries"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-01-31 11:24:58.076393",
|
"modified": "2025-03-24 16:18:19.920701",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry",
|
"name": "Payment Entry",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from functools import reduce
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import ValidationError, _, qb, scrub, throw
|
from frappe import ValidationError, _, qb, scrub, throw
|
||||||
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.query_builder import Tuple
|
from frappe.query_builder import Tuple
|
||||||
from frappe.query_builder.functions import Count
|
from frappe.query_builder.functions import Count
|
||||||
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
||||||
@@ -37,7 +38,11 @@ from erpnext.accounts.general_ledger import (
|
|||||||
make_reverse_gl_entries,
|
make_reverse_gl_entries,
|
||||||
process_gl_map,
|
process_gl_map,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.party import complete_contact_details, get_party_account, set_contact_details
|
from erpnext.accounts.party import (
|
||||||
|
complete_contact_details,
|
||||||
|
get_default_contact,
|
||||||
|
get_party_account,
|
||||||
|
)
|
||||||
from erpnext.accounts.utils import (
|
from erpnext.accounts.utils import (
|
||||||
cancel_exchange_gain_loss_journal,
|
cancel_exchange_gain_loss_journal,
|
||||||
get_account_currency,
|
get_account_currency,
|
||||||
@@ -524,12 +529,12 @@ class PaymentEntry(AccountsController):
|
|||||||
self.party_name = frappe.db.get_value(self.party_type, self.party, "name")
|
self.party_name = frappe.db.get_value(self.party_type, self.party, "name")
|
||||||
|
|
||||||
if self.party:
|
if self.party:
|
||||||
if not self.contact_person:
|
if self.party_type == "Employee":
|
||||||
set_contact_details(
|
self.contact_person = None
|
||||||
self, party=frappe._dict({"name": self.party}), party_type=self.party_type
|
elif not self.contact_person:
|
||||||
)
|
self.contact_person = get_default_contact(self.party_type, self.party)
|
||||||
else:
|
|
||||||
complete_contact_details(self)
|
complete_contact_details(self)
|
||||||
|
|
||||||
if not self.party_account:
|
if not self.party_account:
|
||||||
party_account = get_party_account(self.party_type, self.party, self.company)
|
party_account = get_party_account(self.party_type, self.party, self.company)
|
||||||
@@ -825,16 +830,39 @@ class PaymentEntry(AccountsController):
|
|||||||
outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
|
outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
|
||||||
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
|
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
|
||||||
|
|
||||||
|
conversion_rate = frappe.db.get_value(key[2], {"name": key[1]}, "conversion_rate")
|
||||||
|
base_paid_amount_precision = get_field_precision(
|
||||||
|
frappe.get_meta("Payment Schedule").get_field("base_paid_amount")
|
||||||
|
)
|
||||||
|
base_outstanding_precision = get_field_precision(
|
||||||
|
frappe.get_meta("Payment Schedule").get_field("base_outstanding")
|
||||||
|
)
|
||||||
|
|
||||||
|
base_paid_amount = flt(
|
||||||
|
(allocated_amount - discounted_amt) * conversion_rate, base_paid_amount_precision
|
||||||
|
)
|
||||||
|
base_outstanding = flt(allocated_amount * conversion_rate, base_outstanding_precision)
|
||||||
|
|
||||||
if cancel:
|
if cancel:
|
||||||
frappe.db.sql(
|
frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
UPDATE `tabPayment Schedule`
|
UPDATE `tabPayment Schedule`
|
||||||
SET
|
SET
|
||||||
paid_amount = `paid_amount` - %s,
|
paid_amount = `paid_amount` - %s,
|
||||||
|
base_paid_amount = `base_paid_amount` - %s,
|
||||||
discounted_amount = `discounted_amount` - %s,
|
discounted_amount = `discounted_amount` - %s,
|
||||||
outstanding = `outstanding` + %s
|
outstanding = `outstanding` + %s,
|
||||||
|
base_outstanding = `base_outstanding` - %s
|
||||||
WHERE parent = %s and payment_term = %s""",
|
WHERE parent = %s and payment_term = %s""",
|
||||||
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
|
(
|
||||||
|
allocated_amount - discounted_amt,
|
||||||
|
base_paid_amount,
|
||||||
|
discounted_amt,
|
||||||
|
allocated_amount,
|
||||||
|
base_outstanding,
|
||||||
|
key[1],
|
||||||
|
key[0],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if allocated_amount > outstanding:
|
if allocated_amount > outstanding:
|
||||||
@@ -850,10 +878,20 @@ class PaymentEntry(AccountsController):
|
|||||||
UPDATE `tabPayment Schedule`
|
UPDATE `tabPayment Schedule`
|
||||||
SET
|
SET
|
||||||
paid_amount = `paid_amount` + %s,
|
paid_amount = `paid_amount` + %s,
|
||||||
|
base_paid_amount = `base_paid_amount` + %s,
|
||||||
discounted_amount = `discounted_amount` + %s,
|
discounted_amount = `discounted_amount` + %s,
|
||||||
outstanding = `outstanding` - %s
|
outstanding = `outstanding` - %s,
|
||||||
|
base_outstanding = `base_outstanding` - %s
|
||||||
WHERE parent = %s and payment_term = %s""",
|
WHERE parent = %s and payment_term = %s""",
|
||||||
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
|
(
|
||||||
|
allocated_amount - discounted_amt,
|
||||||
|
base_paid_amount,
|
||||||
|
discounted_amt,
|
||||||
|
allocated_amount,
|
||||||
|
base_outstanding,
|
||||||
|
key[1],
|
||||||
|
key[0],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_allocated_amount_in_transaction_currency(
|
def get_allocated_amount_in_transaction_currency(
|
||||||
|
|||||||
@@ -24,7 +24,9 @@
|
|||||||
"paid_amount",
|
"paid_amount",
|
||||||
"discounted_amount",
|
"discounted_amount",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"base_payment_amount"
|
"base_payment_amount",
|
||||||
|
"base_outstanding",
|
||||||
|
"base_paid_amount"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -155,18 +157,34 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Payment Amount (Company Currency)",
|
"label": "Payment Amount (Company Currency)",
|
||||||
"options": "Company:company:default_currency"
|
"options": "Company:company:default_currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_outstanding",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Outstanding (Company Currency)",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "base_paid_amount",
|
||||||
|
"fieldname": "base_paid_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Paid Amount (Company Currency)",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:11.356171",
|
"modified": "2025-03-11 11:06:51.792982",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Schedule",
|
"name": "Payment Schedule",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class PaymentSchedule(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
|
base_outstanding: DF.Currency
|
||||||
|
base_paid_amount: DF.Currency
|
||||||
base_payment_amount: DF.Currency
|
base_payment_amount: DF.Currency
|
||||||
description: DF.SmallText | None
|
description: DF.SmallText | None
|
||||||
discount: DF.Float
|
discount: DF.Float
|
||||||
|
|||||||
@@ -2700,6 +2700,78 @@ class TestPurchaseInvoice(IntegrationTestCase, 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(
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from frappe import _, qb
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils.data import comma_and
|
from frappe.utils.data import comma_and
|
||||||
|
|
||||||
|
from erpnext.stock import get_warehouse_account_map
|
||||||
|
|
||||||
|
|
||||||
class RepostAccountingLedger(Document):
|
class RepostAccountingLedger(Document):
|
||||||
# begin: auto-generated types
|
# begin: auto-generated types
|
||||||
@@ -97,6 +99,9 @@ class RepostAccountingLedger(Document):
|
|||||||
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
|
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
|
||||||
if doc.doctype in ["Payment Entry", "Journal Entry"]:
|
if doc.doctype in ["Payment Entry", "Journal Entry"]:
|
||||||
gle_map = doc.build_gl_map()
|
gle_map = doc.build_gl_map()
|
||||||
|
elif doc.doctype == "Purchase Receipt":
|
||||||
|
warehouse_account_map = get_warehouse_account_map(doc.company)
|
||||||
|
gle_map = doc.get_gl_entries(warehouse_account_map)
|
||||||
else:
|
else:
|
||||||
gle_map = doc.get_gl_entries()
|
gle_map = doc.get_gl_entries()
|
||||||
|
|
||||||
@@ -177,6 +182,14 @@ def start_repost(account_repost_doc=str) -> None:
|
|||||||
doc.force_set_against_expense_account()
|
doc.force_set_against_expense_account()
|
||||||
doc.make_gl_entries()
|
doc.make_gl_entries()
|
||||||
|
|
||||||
|
elif doc.doctype == "Purchase Receipt":
|
||||||
|
if not repost_doc.delete_cancelled_entries:
|
||||||
|
doc.docstatus = 2
|
||||||
|
doc.make_gl_entries_on_cancel()
|
||||||
|
|
||||||
|
doc.docstatus = 1
|
||||||
|
doc.make_gl_entries(from_repost=True)
|
||||||
|
|
||||||
elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
|
elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
|
||||||
if not repost_doc.delete_cancelled_entries:
|
if not repost_doc.delete_cancelled_entries:
|
||||||
doc.make_gl_entries(1)
|
doc.make_gl_entries(1)
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ from erpnext.accounts.doctype.payment_request.payment_request import make_paymen
|
|||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries, make_purchase_receipt
|
||||||
|
|
||||||
|
|
||||||
class TestRepostAccountingLedger(AccountsTestMixin, IntegrationTestCase):
|
class TestRepostAccountingLedger(AccountsTestMixin, IntegrationTestCase):
|
||||||
@@ -209,9 +211,81 @@ class TestRepostAccountingLedger(AccountsTestMixin, IntegrationTestCase):
|
|||||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||||
|
|
||||||
|
def test_06_repost_purchase_receipt(self):
|
||||||
|
from erpnext.accounts.doctype.account.test_account import create_account
|
||||||
|
|
||||||
|
provisional_account = create_account(
|
||||||
|
account_name="Provision Account",
|
||||||
|
parent_account="Current Liabilities - _TC",
|
||||||
|
company=self.company,
|
||||||
|
)
|
||||||
|
|
||||||
|
another_provisional_account = create_account(
|
||||||
|
account_name="Another Provision Account",
|
||||||
|
parent_account="Current Liabilities - _TC",
|
||||||
|
company=self.company,
|
||||||
|
)
|
||||||
|
|
||||||
|
company = frappe.get_doc("Company", self.company)
|
||||||
|
company.enable_provisional_accounting_for_non_stock_items = 1
|
||||||
|
company.default_provisional_account = provisional_account
|
||||||
|
company.save()
|
||||||
|
|
||||||
|
test_cc = company.cost_center
|
||||||
|
default_expense_account = company.default_expense_account
|
||||||
|
|
||||||
|
item = make_item(properties={"is_stock_item": 0})
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(company=self.company, item_code=item.name, rate=1000.0, qty=1.0)
|
||||||
|
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||||
|
expected_pr_gles = [
|
||||||
|
{"account": provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||||
|
{"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
|
||||||
|
]
|
||||||
|
self.assertEqual(expected_pr_gles, pr_gl_entries)
|
||||||
|
|
||||||
|
# change the provisional account
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Purchase Receipt Item",
|
||||||
|
pr.items[0].name,
|
||||||
|
"provisional_expense_account",
|
||||||
|
another_provisional_account,
|
||||||
|
)
|
||||||
|
|
||||||
|
repost_doc = frappe.new_doc("Repost Accounting Ledger")
|
||||||
|
repost_doc.company = self.company
|
||||||
|
repost_doc.delete_cancelled_entries = True
|
||||||
|
repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name})
|
||||||
|
repost_doc.save().submit()
|
||||||
|
|
||||||
|
pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||||
|
expected_pr_gles_after_repost = [
|
||||||
|
{"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
|
||||||
|
{"account": another_provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||||
|
]
|
||||||
|
self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost))
|
||||||
|
self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost)
|
||||||
|
|
||||||
|
# teardown
|
||||||
|
repost_doc.cancel()
|
||||||
|
repost_doc.delete()
|
||||||
|
|
||||||
|
pr.reload()
|
||||||
|
pr.cancel()
|
||||||
|
|
||||||
|
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||||
|
company.default_provisional_account = None
|
||||||
|
company.save()
|
||||||
|
|
||||||
|
|
||||||
def update_repost_settings():
|
def update_repost_settings():
|
||||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
allowed_types = [
|
||||||
|
"Sales Invoice",
|
||||||
|
"Purchase Invoice",
|
||||||
|
"Payment Entry",
|
||||||
|
"Journal Entry",
|
||||||
|
"Purchase Receipt",
|
||||||
|
]
|
||||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||||
for x in allowed_types:
|
for x in allowed_types:
|
||||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||||
|
|||||||
@@ -1827,17 +1827,6 @@ class TestSalesInvoice(IntegrationTestCase):
|
|||||||
for field in expected_gle:
|
for field in expected_gle:
|
||||||
self.assertEqual(expected_gle[field], gle[field])
|
self.assertEqual(expected_gle[field], gle[field])
|
||||||
|
|
||||||
def test_invoice_exchange_rate(self):
|
|
||||||
si = create_sales_invoice(
|
|
||||||
customer="_Test Customer USD",
|
|
||||||
debit_to="_Test Receivable USD - _TC",
|
|
||||||
currency="USD",
|
|
||||||
conversion_rate=1,
|
|
||||||
do_not_save=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, si.save)
|
|
||||||
|
|
||||||
def test_invalid_currency(self):
|
def test_invalid_currency(self):
|
||||||
# Customer currency = USD
|
# Customer currency = USD
|
||||||
|
|
||||||
|
|||||||
@@ -280,32 +280,50 @@ def get_regional_address_details(party_details, doctype, company):
|
|||||||
|
|
||||||
|
|
||||||
def complete_contact_details(party_details):
|
def complete_contact_details(party_details):
|
||||||
if not party_details.contact_person:
|
contact_details = frappe._dict()
|
||||||
party_details.update(
|
|
||||||
{
|
if party_details.party_type == "Employee":
|
||||||
"contact_person": None,
|
contact_details = frappe.db.get_value(
|
||||||
"contact_display": None,
|
"Employee",
|
||||||
"contact_email": None,
|
party_details.party,
|
||||||
"contact_mobile": None,
|
[
|
||||||
"contact_phone": None,
|
"employee_name as contact_display",
|
||||||
"contact_designation": None,
|
"prefered_email as contact_email",
|
||||||
"contact_department": None,
|
"cell_number as contact_mobile",
|
||||||
}
|
"designation as contact_designation",
|
||||||
|
"department as contact_department",
|
||||||
|
],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
contact_details.update({"contact_person": None, "contact_phone": None})
|
||||||
|
elif party_details.contact_person:
|
||||||
|
contact_details = frappe.db.get_value(
|
||||||
|
"Contact",
|
||||||
|
party_details.contact_person,
|
||||||
|
[
|
||||||
|
"name as contact_person",
|
||||||
|
"full_name as contact_display",
|
||||||
|
"email_id as contact_email",
|
||||||
|
"mobile_no as contact_mobile",
|
||||||
|
"phone as contact_phone",
|
||||||
|
"designation as contact_designation",
|
||||||
|
"department as contact_department",
|
||||||
|
],
|
||||||
|
as_dict=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
fields = [
|
contact_details = {
|
||||||
"name as contact_person",
|
"contact_person": None,
|
||||||
"full_name as contact_display",
|
"contact_display": None,
|
||||||
"email_id as contact_email",
|
"contact_email": None,
|
||||||
"mobile_no as contact_mobile",
|
"contact_mobile": None,
|
||||||
"phone as contact_phone",
|
"contact_phone": None,
|
||||||
"designation as contact_designation",
|
"contact_designation": None,
|
||||||
"department as contact_department",
|
"contact_department": None,
|
||||||
]
|
}
|
||||||
|
|
||||||
contact_details = frappe.db.get_value("Contact", party_details.contact_person, fields, as_dict=True)
|
party_details.update(contact_details)
|
||||||
|
|
||||||
party_details.update(contact_details)
|
|
||||||
|
|
||||||
|
|
||||||
def set_contact_details(party_details, party, party_type):
|
def set_contact_details(party_details, party, party_type):
|
||||||
@@ -780,9 +798,9 @@ def validate_account_party_type(self):
|
|||||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||||
if account_type and (account_type not in ["Receivable", "Payable", "Equity"]):
|
if account_type and (account_type not in ["Receivable", "Payable", "Equity"]):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_("Party Type and Party can only be set for Receivable / Payable account<br><br>{0}").format(
|
||||||
"Party Type and Party can only be set for Receivable / Payable account<br><br>" "{0}"
|
self.account
|
||||||
).format(self.account)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -517,7 +517,7 @@ class ReceivablePayableReport:
|
|||||||
select
|
select
|
||||||
si.name, si.party_account_currency, si.currency, si.conversion_rate,
|
si.name, si.party_account_currency, si.currency, si.conversion_rate,
|
||||||
si.total_advance, ps.due_date, ps.payment_term, ps.payment_amount, ps.base_payment_amount,
|
si.total_advance, ps.due_date, ps.payment_term, ps.payment_amount, ps.base_payment_amount,
|
||||||
ps.description, ps.paid_amount, ps.discounted_amount
|
ps.description, ps.paid_amount, ps.base_paid_amount, ps.discounted_amount
|
||||||
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
|
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
|
||||||
where
|
where
|
||||||
si.name = ps.parent and ps.parenttype = '{row.voucher_type}' and
|
si.name = ps.parent and ps.parenttype = '{row.voucher_type}' and
|
||||||
@@ -540,20 +540,24 @@ class ReceivablePayableReport:
|
|||||||
# Deduct that from paid amount pre allocation
|
# Deduct that from paid amount pre allocation
|
||||||
row.paid -= flt(payment_terms_details[0].total_advance)
|
row.paid -= flt(payment_terms_details[0].total_advance)
|
||||||
|
|
||||||
|
company_currency = frappe.get_value("Company", self.filters.get("company"), "default_currency")
|
||||||
|
|
||||||
# If single payment terms, no need to split the row
|
# If single payment terms, no need to split the row
|
||||||
if len(payment_terms_details) == 1 and payment_terms_details[0].payment_term:
|
if len(payment_terms_details) == 1 and payment_terms_details[0].payment_term:
|
||||||
self.append_payment_term(row, payment_terms_details[0], original_row)
|
self.append_payment_term(row, payment_terms_details[0], original_row, company_currency)
|
||||||
return
|
return
|
||||||
|
|
||||||
for d in payment_terms_details:
|
for d in payment_terms_details:
|
||||||
term = frappe._dict(original_row)
|
term = frappe._dict(original_row)
|
||||||
self.append_payment_term(row, d, term)
|
self.append_payment_term(row, d, term, company_currency)
|
||||||
|
|
||||||
def append_payment_term(self, row, d, term):
|
def append_payment_term(self, row, d, term, company_currency):
|
||||||
if d.currency == d.party_account_currency:
|
invoiced = d.base_payment_amount
|
||||||
|
paid_amount = d.base_paid_amount
|
||||||
|
|
||||||
|
if company_currency == d.party_account_currency or self.filters.get("in_party_currency"):
|
||||||
invoiced = d.payment_amount
|
invoiced = d.payment_amount
|
||||||
else:
|
paid_amount = d.paid_amount
|
||||||
invoiced = d.base_payment_amount
|
|
||||||
|
|
||||||
row.payment_terms.append(
|
row.payment_terms.append(
|
||||||
term.update(
|
term.update(
|
||||||
@@ -562,15 +566,15 @@ class ReceivablePayableReport:
|
|||||||
"invoiced": invoiced,
|
"invoiced": invoiced,
|
||||||
"invoice_grand_total": row.invoiced,
|
"invoice_grand_total": row.invoiced,
|
||||||
"payment_term": d.description or d.payment_term,
|
"payment_term": d.description or d.payment_term,
|
||||||
"paid": d.paid_amount + d.discounted_amount,
|
"paid": paid_amount + d.discounted_amount,
|
||||||
"credit_note": 0.0,
|
"credit_note": 0.0,
|
||||||
"outstanding": invoiced - d.paid_amount - d.discounted_amount,
|
"outstanding": invoiced - paid_amount - d.discounted_amount,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if d.paid_amount:
|
if paid_amount:
|
||||||
row["paid"] -= d.paid_amount + d.discounted_amount
|
row["paid"] -= paid_amount + d.discounted_amount
|
||||||
|
|
||||||
def allocate_closing_to_term(self, row, term, key):
|
def allocate_closing_to_term(self, row, term, key):
|
||||||
if row[key]:
|
if row[key]:
|
||||||
|
|||||||
@@ -145,6 +145,130 @@ def get_asset_categories_for_grouped_by_category(filters):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_assets_for_grouped_by_category(filters):
|
||||||
|
condition = ""
|
||||||
|
if filters.get("asset_category"):
|
||||||
|
condition = f" and a.asset_category = '{filters.get('asset_category')}'"
|
||||||
|
finance_book_filter = ""
|
||||||
|
if filters.get("finance_book"):
|
||||||
|
finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s"
|
||||||
|
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
|
||||||
|
|
||||||
|
# nosemgrep
|
||||||
|
return frappe.db.sql(
|
||||||
|
f"""
|
||||||
|
SELECT results.asset_category,
|
||||||
|
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||||
|
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
|
||||||
|
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||||
|
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||||
|
from (SELECT a.asset_category,
|
||||||
|
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
||||||
|
gle.debit
|
||||||
|
else
|
||||||
|
0
|
||||||
|
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||||
|
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
|
||||||
|
gle.credit
|
||||||
|
else
|
||||||
|
0
|
||||||
|
end), 0) as depreciation_eliminated_via_reversal,
|
||||||
|
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||||
|
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||||
|
gle.debit
|
||||||
|
else
|
||||||
|
0
|
||||||
|
end), 0) as depreciation_eliminated_during_the_period,
|
||||||
|
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
|
||||||
|
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
|
||||||
|
gle.debit
|
||||||
|
else
|
||||||
|
0
|
||||||
|
end), 0) as depreciation_amount_during_the_period
|
||||||
|
from `tabGL Entry` gle
|
||||||
|
join `tabAsset` a on
|
||||||
|
gle.against_voucher = a.name
|
||||||
|
join `tabAsset Category Account` aca on
|
||||||
|
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||||
|
join `tabCompany` company on
|
||||||
|
company.name = %(company)s
|
||||||
|
where
|
||||||
|
a.docstatus=1
|
||||||
|
and a.company=%(company)s
|
||||||
|
and a.purchase_date <= %(to_date)s
|
||||||
|
and gle.is_cancelled = 0
|
||||||
|
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||||
|
{condition} {finance_book_filter}
|
||||||
|
group by a.asset_category
|
||||||
|
union
|
||||||
|
SELECT a.asset_category,
|
||||||
|
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then
|
||||||
|
0
|
||||||
|
else
|
||||||
|
a.opening_accumulated_depreciation
|
||||||
|
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||||
|
0 as depreciation_eliminated_via_reversal,
|
||||||
|
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
|
||||||
|
a.opening_accumulated_depreciation
|
||||||
|
else
|
||||||
|
0
|
||||||
|
end), 0) as depreciation_eliminated_during_the_period,
|
||||||
|
0 as depreciation_amount_during_the_period
|
||||||
|
from `tabAsset` a
|
||||||
|
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||||
|
group by a.asset_category) as results
|
||||||
|
group by results.asset_category
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"to_date": filters.to_date,
|
||||||
|
"from_date": filters.from_date,
|
||||||
|
"company": filters.company,
|
||||||
|
"finance_book": filters.get("finance_book", ""),
|
||||||
|
},
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_group_by_asset_data(filters):
|
||||||
|
data = []
|
||||||
|
|
||||||
|
asset_details = get_asset_details_for_grouped_by_category(filters)
|
||||||
|
assets = get_assets_for_grouped_by_asset(filters)
|
||||||
|
|
||||||
|
for asset_detail in asset_details:
|
||||||
|
row = frappe._dict()
|
||||||
|
row.update(asset_detail)
|
||||||
|
|
||||||
|
row.value_as_on_to_date = (
|
||||||
|
flt(row.value_as_on_from_date)
|
||||||
|
+ flt(row.value_of_new_purchase)
|
||||||
|
- flt(row.value_of_sold_asset)
|
||||||
|
- flt(row.value_of_scrapped_asset)
|
||||||
|
- flt(row.value_of_capitalized_asset)
|
||||||
|
)
|
||||||
|
|
||||||
|
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
|
||||||
|
|
||||||
|
row.accumulated_depreciation_as_on_to_date = (
|
||||||
|
flt(row.accumulated_depreciation_as_on_from_date)
|
||||||
|
+ flt(row.depreciation_amount_during_the_period)
|
||||||
|
- flt(row.depreciation_eliminated_during_the_period)
|
||||||
|
- flt(row.depreciation_eliminated_via_reversal)
|
||||||
|
)
|
||||||
|
|
||||||
|
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
||||||
|
row.accumulated_depreciation_as_on_from_date
|
||||||
|
)
|
||||||
|
|
||||||
|
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
|
||||||
|
row.accumulated_depreciation_as_on_to_date
|
||||||
|
)
|
||||||
|
|
||||||
|
data.append(row)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def get_asset_details_for_grouped_by_category(filters):
|
def get_asset_details_for_grouped_by_category(filters):
|
||||||
condition = ""
|
condition = ""
|
||||||
if filters.get("asset"):
|
if filters.get("asset"):
|
||||||
@@ -224,130 +348,6 @@ def get_asset_details_for_grouped_by_category(filters):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_group_by_asset_data(filters):
|
|
||||||
data = []
|
|
||||||
|
|
||||||
asset_details = get_asset_details_for_grouped_by_category(filters)
|
|
||||||
assets = get_assets_for_grouped_by_asset(filters)
|
|
||||||
|
|
||||||
for asset_detail in asset_details:
|
|
||||||
row = frappe._dict()
|
|
||||||
row.update(asset_detail)
|
|
||||||
|
|
||||||
row.value_as_on_to_date = (
|
|
||||||
flt(row.value_as_on_from_date)
|
|
||||||
+ flt(row.value_of_new_purchase)
|
|
||||||
- flt(row.value_of_sold_asset)
|
|
||||||
- flt(row.value_of_scrapped_asset)
|
|
||||||
- flt(row.value_of_capitalized_asset)
|
|
||||||
)
|
|
||||||
|
|
||||||
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
|
|
||||||
|
|
||||||
row.accumulated_depreciation_as_on_to_date = (
|
|
||||||
flt(row.accumulated_depreciation_as_on_from_date)
|
|
||||||
+ flt(row.depreciation_amount_during_the_period)
|
|
||||||
- flt(row.depreciation_eliminated_during_the_period)
|
|
||||||
- flt(row.depreciation_eliminated_via_reversal)
|
|
||||||
)
|
|
||||||
|
|
||||||
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
|
||||||
row.accumulated_depreciation_as_on_from_date
|
|
||||||
)
|
|
||||||
|
|
||||||
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
|
|
||||||
row.accumulated_depreciation_as_on_to_date
|
|
||||||
)
|
|
||||||
|
|
||||||
data.append(row)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def get_assets_for_grouped_by_category(filters):
|
|
||||||
condition = ""
|
|
||||||
if filters.get("asset_category"):
|
|
||||||
condition = f" and a.asset_category = '{filters.get('asset_category')}'"
|
|
||||||
finance_book_filter = ""
|
|
||||||
if filters.get("finance_book"):
|
|
||||||
finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s"
|
|
||||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
|
|
||||||
|
|
||||||
# nosemgrep
|
|
||||||
return frappe.db.sql(
|
|
||||||
f"""
|
|
||||||
SELECT results.asset_category,
|
|
||||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
|
||||||
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
|
|
||||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
|
||||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
|
||||||
from (SELECT a.asset_category,
|
|
||||||
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
|
||||||
gle.debit
|
|
||||||
else
|
|
||||||
0
|
|
||||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
|
||||||
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
|
|
||||||
gle.credit
|
|
||||||
else
|
|
||||||
0
|
|
||||||
end), 0) as depreciation_eliminated_via_reversal,
|
|
||||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
|
||||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
|
||||||
gle.debit
|
|
||||||
else
|
|
||||||
0
|
|
||||||
end), 0) as depreciation_eliminated_during_the_period,
|
|
||||||
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
|
|
||||||
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
|
|
||||||
gle.debit
|
|
||||||
else
|
|
||||||
0
|
|
||||||
end), 0) as depreciation_amount_during_the_period
|
|
||||||
from `tabGL Entry` gle
|
|
||||||
join `tabAsset` a on
|
|
||||||
gle.against_voucher = a.name
|
|
||||||
join `tabAsset Category Account` aca on
|
|
||||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
|
||||||
join `tabCompany` company on
|
|
||||||
company.name = %(company)s
|
|
||||||
where
|
|
||||||
a.docstatus=1
|
|
||||||
and a.company=%(company)s
|
|
||||||
and a.purchase_date <= %(to_date)s
|
|
||||||
and gle.is_cancelled = 0
|
|
||||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
|
||||||
{condition} {finance_book_filter}
|
|
||||||
group by a.asset_category
|
|
||||||
union
|
|
||||||
SELECT a.asset_category,
|
|
||||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
|
|
||||||
0
|
|
||||||
else
|
|
||||||
a.opening_accumulated_depreciation
|
|
||||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
|
||||||
0 as depreciation_eliminated_via_reversal,
|
|
||||||
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
|
|
||||||
a.opening_accumulated_depreciation
|
|
||||||
else
|
|
||||||
0
|
|
||||||
end), 0) as depreciation_eliminated_during_the_period,
|
|
||||||
0 as depreciation_amount_during_the_period
|
|
||||||
from `tabAsset` a
|
|
||||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
|
||||||
group by a.asset_category) as results
|
|
||||||
group by results.asset_category
|
|
||||||
""",
|
|
||||||
{
|
|
||||||
"to_date": filters.to_date,
|
|
||||||
"from_date": filters.from_date,
|
|
||||||
"company": filters.company,
|
|
||||||
"finance_book": filters.get("finance_book", ""),
|
|
||||||
},
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_assets_for_grouped_by_asset(filters):
|
def get_assets_for_grouped_by_asset(filters):
|
||||||
condition = ""
|
condition = ""
|
||||||
if filters.get("asset"):
|
if filters.get("asset"):
|
||||||
@@ -405,7 +405,7 @@ def get_assets_for_grouped_by_asset(filters):
|
|||||||
group by a.name
|
group by a.name
|
||||||
union
|
union
|
||||||
SELECT a.name as name,
|
SELECT a.name as name,
|
||||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
|
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then
|
||||||
0
|
0
|
||||||
else
|
else
|
||||||
a.opening_accumulated_depreciation
|
a.opening_accumulated_depreciation
|
||||||
|
|||||||
@@ -91,6 +91,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(
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import frappe
|
|||||||
import frappe.defaults
|
import frappe.defaults
|
||||||
from frappe import _, qb, throw
|
from frappe import _, qb, throw
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.query_builder import AliasedQuery, Criterion, Table
|
from frappe.query_builder import AliasedQuery, Case, Criterion, Table
|
||||||
from frappe.query_builder.functions import Count, Round, Sum
|
from frappe.query_builder.functions import Count, Max, Round, Sum
|
||||||
from frappe.query_builder.utils import DocType
|
from frappe.query_builder.utils import DocType
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_days,
|
add_days,
|
||||||
@@ -2008,6 +2008,15 @@ class QueryPaymentLedger:
|
|||||||
.select(
|
.select(
|
||||||
ple.against_voucher_no.as_("voucher_no"),
|
ple.against_voucher_no.as_("voucher_no"),
|
||||||
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
|
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
|
||||||
|
Max(
|
||||||
|
Case().when(
|
||||||
|
(
|
||||||
|
(ple.voucher_no == ple.against_voucher_no)
|
||||||
|
& (ple.voucher_type == ple.against_voucher_type)
|
||||||
|
),
|
||||||
|
(ple.posting_date),
|
||||||
|
)
|
||||||
|
).as_("invoice_date"),
|
||||||
)
|
)
|
||||||
.where(ple.delinked == 0)
|
.where(ple.delinked == 0)
|
||||||
.where(Criterion.all(filter_on_against_voucher_no))
|
.where(Criterion.all(filter_on_against_voucher_no))
|
||||||
@@ -2015,7 +2024,7 @@ class QueryPaymentLedger:
|
|||||||
.where(Criterion.all(self.dimensions_filter))
|
.where(Criterion.all(self.dimensions_filter))
|
||||||
.where(Criterion.all(self.voucher_posting_date))
|
.where(Criterion.all(self.voucher_posting_date))
|
||||||
.groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party)
|
.groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party)
|
||||||
.orderby(ple.posting_date, ple.voucher_no)
|
.orderby(ple.invoice_date, ple.voucher_no)
|
||||||
.having(qb.Field("amount_in_account_currency") > 0)
|
.having(qb.Field("amount_in_account_currency") > 0)
|
||||||
.limit(self.limit)
|
.limit(self.limit)
|
||||||
.run()
|
.run()
|
||||||
|
|||||||
@@ -444,21 +444,22 @@ class AccountsController(TransactionBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_party_address_and_contact(self):
|
def validate_party_address_and_contact(self):
|
||||||
party, party_type = None, None
|
party_type, party = self.get_party()
|
||||||
if self.get("customer"):
|
|
||||||
party, party_type = self.customer, "Customer"
|
if not (party_type and party):
|
||||||
|
return
|
||||||
|
|
||||||
|
if party_type == "Customer":
|
||||||
billing_address, shipping_address = (
|
billing_address, shipping_address = (
|
||||||
self.get("customer_address"),
|
self.get("customer_address"),
|
||||||
self.get("shipping_address_name"),
|
self.get("shipping_address_name"),
|
||||||
)
|
)
|
||||||
self.validate_party_address(party, party_type, billing_address, shipping_address)
|
self.validate_party_address(party, party_type, billing_address, shipping_address)
|
||||||
elif self.get("supplier"):
|
elif party_type == "Supplier":
|
||||||
party, party_type = self.supplier, "Supplier"
|
|
||||||
billing_address = self.get("supplier_address")
|
billing_address = self.get("supplier_address")
|
||||||
self.validate_party_address(party, party_type, billing_address)
|
self.validate_party_address(party, party_type, billing_address)
|
||||||
|
|
||||||
if party and party_type:
|
self.validate_party_contact(party, party_type)
|
||||||
self.validate_party_contact(party, party_type)
|
|
||||||
|
|
||||||
def validate_party_address(self, party, party_type, billing_address, shipping_address=None):
|
def validate_party_address(self, party, party_type, billing_address, shipping_address=None):
|
||||||
if billing_address or shipping_address:
|
if billing_address or shipping_address:
|
||||||
@@ -2380,6 +2381,9 @@ class AccountsController(TransactionBase):
|
|||||||
base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
|
base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
|
||||||
)
|
)
|
||||||
d.outstanding = d.payment_amount
|
d.outstanding = d.payment_amount
|
||||||
|
d.base_outstanding = flt(
|
||||||
|
d.payment_amount * self.get("conversion_rate"), d.precision("base_outstanding")
|
||||||
|
)
|
||||||
elif not d.invoice_portion:
|
elif not d.invoice_portion:
|
||||||
d.base_payment_amount = flt(
|
d.base_payment_amount = flt(
|
||||||
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
|
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
|
||||||
@@ -2708,12 +2712,17 @@ class AccountsController(TransactionBase):
|
|||||||
default_currency = erpnext.get_company_currency(self.company)
|
default_currency = erpnext.get_company_currency(self.company)
|
||||||
if not default_currency:
|
if not default_currency:
|
||||||
throw(_("Please enter default currency in Company Master"))
|
throw(_("Please enter default currency in Company Master"))
|
||||||
if (
|
|
||||||
(self.currency == default_currency and flt(self.conversion_rate) != 1.00)
|
if not self.conversion_rate:
|
||||||
or not self.conversion_rate
|
throw(_("Conversion rate cannot be 0"))
|
||||||
or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
|
|
||||||
):
|
if self.currency == default_currency and flt(self.conversion_rate) != 1.00:
|
||||||
throw(_("Conversion rate cannot be 0 or 1"))
|
throw(_("Conversion rate must be 1.00 if document currency is same as company currency"))
|
||||||
|
|
||||||
|
if self.currency != default_currency and flt(self.conversion_rate) == 1.00:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Conversion rate is 1.00, but document currency is different from company currency")
|
||||||
|
)
|
||||||
|
|
||||||
def check_finance_books(self, item, asset):
|
def check_finance_books(self, item, asset):
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -905,3 +905,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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -551,7 +551,11 @@ class SubcontractingController(StockController):
|
|||||||
def __get_batch_nos_for_bundle(self, qty, key):
|
def __get_batch_nos_for_bundle(self, qty, key):
|
||||||
available_batches = defaultdict(float)
|
available_batches = defaultdict(float)
|
||||||
|
|
||||||
|
precision = frappe.get_precision("Subcontracting Receipt Supplied Item", "consumed_qty")
|
||||||
for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
|
for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
|
||||||
|
if flt(batch_qty, precision) <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
qty_to_consumed = 0
|
qty_to_consumed = 0
|
||||||
if qty > 0:
|
if qty > 0:
|
||||||
if batch_qty >= qty:
|
if batch_qty >= qty:
|
||||||
|
|||||||
@@ -467,6 +467,7 @@ class calculate_taxes_and_totals:
|
|||||||
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"),
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f" net_amount: {current_net_amount:<20} tax_amount: {current_tax_amount:<20} - {tax.description}"
|
f" net_amount: {current_net_amount:<20} tax_amount: {current_tax_amount:<20} - {tax.description}"
|
||||||
)
|
)
|
||||||
@@ -727,40 +728,36 @@ 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"))
|
||||||
item.distributed_discount_amount = flt(
|
item.distributed_discount_amount = flt(
|
||||||
distributed_amount, item.precision("distributed_discount_amount")
|
distributed_amount, item.precision("distributed_discount_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")
|
||||||
)
|
)
|
||||||
item.distributed_discount_amount = flt(
|
item.distributed_discount_amount = flt(
|
||||||
distributed_amount + discount_amount_loss,
|
distributed_amount + rounding_difference,
|
||||||
item.precision("distributed_discount_amount"),
|
item.precision("distributed_discount_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
|
||||||
@@ -776,20 +773,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():
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ def get_transaction_list(
|
|||||||
filters=None,
|
filters=None,
|
||||||
limit_start=0,
|
limit_start=0,
|
||||||
limit_page_length=20,
|
limit_page_length=20,
|
||||||
order_by="creation",
|
order_by="creation desc",
|
||||||
custom=False,
|
custom=False,
|
||||||
):
|
):
|
||||||
user = frappe.session.user
|
user = frappe.session.user
|
||||||
@@ -115,7 +115,7 @@ def get_transaction_list(
|
|||||||
limit_page_length,
|
limit_page_length,
|
||||||
fields="name",
|
fields="name",
|
||||||
ignore_permissions=ignore_permissions,
|
ignore_permissions=ignore_permissions,
|
||||||
order_by="creation desc",
|
order_by=order_by,
|
||||||
)
|
)
|
||||||
|
|
||||||
if custom:
|
if custom:
|
||||||
|
|||||||
1386
erpnext/locale/ar.po
1386
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
60585
erpnext/locale/ar_SA.po
60585
erpnext/locale/ar_SA.po
File diff suppressed because it is too large
Load Diff
1412
erpnext/locale/bs.po
1412
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
60702
erpnext/locale/bs_BA.po
60702
erpnext/locale/bs_BA.po
File diff suppressed because it is too large
Load Diff
1404
erpnext/locale/de.po
1404
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
60704
erpnext/locale/de_DE.po
60704
erpnext/locale/de_DE.po
File diff suppressed because it is too large
Load Diff
1388
erpnext/locale/eo.po
1388
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
60585
erpnext/locale/eo_UY.po
60585
erpnext/locale/eo_UY.po
File diff suppressed because it is too large
Load Diff
1386
erpnext/locale/es.po
1386
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
60692
erpnext/locale/es_ES.po
60692
erpnext/locale/es_ES.po
File diff suppressed because it is too large
Load Diff
1678
erpnext/locale/fa.po
1678
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
60599
erpnext/locale/fa_IR.po
60599
erpnext/locale/fa_IR.po
File diff suppressed because it is too large
Load Diff
1392
erpnext/locale/fr.po
1392
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
60609
erpnext/locale/fr_FR.po
60609
erpnext/locale/fr_FR.po
File diff suppressed because it is too large
Load Diff
1454
erpnext/locale/hr.po
1454
erpnext/locale/hr.po
File diff suppressed because it is too large
Load Diff
60585
erpnext/locale/hr_HR.po
60585
erpnext/locale/hr_HR.po
File diff suppressed because it is too large
Load Diff
1382
erpnext/locale/hu.po
1382
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
60585
erpnext/locale/hu_HU.po
60585
erpnext/locale/hu_HU.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
5003
erpnext/locale/pl.po
5003
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
60636
erpnext/locale/pl_PL.po
60636
erpnext/locale/pl_PL.po
File diff suppressed because it is too large
Load Diff
100833
erpnext/locale/pt.po
100833
erpnext/locale/pt.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1382
erpnext/locale/ru.po
1382
erpnext/locale/ru.po
File diff suppressed because it is too large
Load Diff
60585
erpnext/locale/ru_RU.po
60585
erpnext/locale/ru_RU.po
File diff suppressed because it is too large
Load Diff
1390
erpnext/locale/sv.po
1390
erpnext/locale/sv.po
File diff suppressed because it is too large
Load Diff
60708
erpnext/locale/sv_SE.po
60708
erpnext/locale/sv_SE.po
File diff suppressed because it is too large
Load Diff
1378
erpnext/locale/th.po
1378
erpnext/locale/th.po
File diff suppressed because it is too large
Load Diff
60585
erpnext/locale/th_TH.po
60585
erpnext/locale/th_TH.po
File diff suppressed because it is too large
Load Diff
1386
erpnext/locale/tr.po
1386
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
60708
erpnext/locale/tr_TR.po
60708
erpnext/locale/tr_TR.po
File diff suppressed because it is too large
Load Diff
16832
erpnext/locale/zh.po
16832
erpnext/locale/zh.po
File diff suppressed because it is too large
Load Diff
60689
erpnext/locale/zh_CN.po
60689
erpnext/locale/zh_CN.po
File diff suppressed because it is too large
Load Diff
@@ -242,7 +242,7 @@ frappe.ui.form.on("BOM", {
|
|||||||
qty: data.qty || 0.0,
|
qty: data.qty || 0.0,
|
||||||
project: frm.doc.project,
|
project: frm.doc.project,
|
||||||
variant_items: variant_items,
|
variant_items: variant_items,
|
||||||
use_multi_level_bom: use_multi_level_bom,
|
use_multi_level_bom: frm.doc?.track_semi_finished_goods ? 0 : use_multi_level_bom,
|
||||||
},
|
},
|
||||||
freeze: true,
|
freeze: true,
|
||||||
callback(r) {
|
callback(r) {
|
||||||
@@ -331,12 +331,14 @@ frappe.ui.form.on("BOM", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
fields.push({
|
if (!frm.doc.track_semi_finished_goods) {
|
||||||
fieldtype: "Check",
|
fields.push({
|
||||||
label: __("Use Multi-Level BOM"),
|
fieldtype: "Check",
|
||||||
fieldname: "use_multi_level_bom",
|
label: __("Use Multi-Level BOM"),
|
||||||
default: frm.doc?.__onload.use_multi_level_bom,
|
fieldname: "use_multi_level_bom",
|
||||||
});
|
default: frm.doc?.__onload.use_multi_level_bom,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var has_template_rm = frm.doc.items.filter((d) => d.has_variants === 1) || [];
|
var has_template_rm = frm.doc.items.filter((d) => d.has_variants === 1) || [];
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ frappe.ui.form.on("Job Card", {
|
|||||||
|
|
||||||
setup_stock_entry(frm) {
|
setup_stock_entry(frm) {
|
||||||
if (
|
if (
|
||||||
|
frm.doc.manufactured_qty &&
|
||||||
frm.doc.finished_good &&
|
frm.doc.finished_good &&
|
||||||
frm.doc.docstatus === 1 &&
|
frm.doc.docstatus === 1 &&
|
||||||
!frm.doc.is_subcontracted &&
|
!frm.doc.is_subcontracted &&
|
||||||
@@ -86,11 +87,16 @@ frappe.ui.form.on("Job Card", {
|
|||||||
|
|
||||||
frm.toggle_enable("for_quantity", !has_stock_entry);
|
frm.toggle_enable("for_quantity", !has_stock_entry);
|
||||||
|
|
||||||
if (!frm.is_new() && !frm.doc.skip_material_transfer && has_items && frm.doc.docstatus < 2) {
|
if (frm.doc.docstatus != 0) {
|
||||||
|
frm.fields_dict["time_logs"].grid.update_docfield_property("completed_qty", "read_only", 1);
|
||||||
|
frm.fields_dict["time_logs"].grid.update_docfield_property("time_in_mins", "read_only", 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!frm.is_new() && !frm.doc.skip_material_transfer && frm.doc.docstatus < 2) {
|
||||||
let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
|
let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
|
||||||
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
|
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
|
||||||
|
|
||||||
if (to_request || excess_transfer_allowed) {
|
if (has_items && (to_request || excess_transfer_allowed)) {
|
||||||
frm.add_custom_button(__("Material Request"), () => {
|
frm.add_custom_button(__("Material Request"), () => {
|
||||||
frm.trigger("make_material_request");
|
frm.trigger("make_material_request");
|
||||||
});
|
});
|
||||||
@@ -100,7 +106,7 @@ frappe.ui.form.on("Job Card", {
|
|||||||
// in case of multiple items in JC
|
// in case of multiple items in JC
|
||||||
let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty);
|
let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty);
|
||||||
|
|
||||||
if (to_transfer || excess_transfer_allowed) {
|
if (has_items && (to_transfer || excess_transfer_allowed)) {
|
||||||
frm.add_custom_button(__("Material Transfer"), () => {
|
frm.add_custom_button(__("Material Transfer"), () => {
|
||||||
frm.trigger("make_stock_entry");
|
frm.trigger("make_stock_entry");
|
||||||
});
|
});
|
||||||
@@ -127,7 +133,8 @@ frappe.ui.form.on("Job Card", {
|
|||||||
frm.doc.for_quantity + frm.doc.process_loss_qty > frm.doc.total_completed_qty &&
|
frm.doc.for_quantity + frm.doc.process_loss_qty > frm.doc.total_completed_qty &&
|
||||||
(frm.doc.skip_material_transfer ||
|
(frm.doc.skip_material_transfer ||
|
||||||
frm.doc.transferred_qty >= frm.doc.for_quantity + frm.doc.process_loss_qty ||
|
frm.doc.transferred_qty >= frm.doc.for_quantity + frm.doc.process_loss_qty ||
|
||||||
!frm.doc.finished_good)
|
!frm.doc.finished_good ||
|
||||||
|
!has_items?.length)
|
||||||
) {
|
) {
|
||||||
if (!frm.doc.time_logs?.length) {
|
if (!frm.doc.time_logs?.length) {
|
||||||
frm.add_custom_button(__("Start Job"), () => {
|
frm.add_custom_button(__("Start Job"), () => {
|
||||||
@@ -163,7 +170,8 @@ frappe.ui.form.on("Job Card", {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (frm.doc.for_quantity - frm.doc.manufactured_qty > 0) {
|
let manufactured_qty = frm.doc.manufactured_qty || frm.doc.total_completed_qty;
|
||||||
|
if (frm.doc.for_quantity - (manufactured_qty + frm.doc.process_loss_qty) > 0) {
|
||||||
if (!frm.doc.is_paused) {
|
if (!frm.doc.is_paused) {
|
||||||
frm.add_custom_button(__("Pause Job"), () => {
|
frm.add_custom_button(__("Pause Job"), () => {
|
||||||
frm.call({
|
frm.call({
|
||||||
@@ -214,20 +222,60 @@ frappe.ui.form.on("Job Card", {
|
|||||||
let fields = [
|
let fields = [
|
||||||
{
|
{
|
||||||
fieldtype: "Float",
|
fieldtype: "Float",
|
||||||
label: __("Completed Quantity"),
|
label: __("Qty to Manufacture"),
|
||||||
fieldname: "qty",
|
fieldname: "for_quantity",
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
default: frm.doc.for_quantity - frm.doc.total_completed_qty,
|
default: frm.doc.for_quantity,
|
||||||
|
change() {
|
||||||
|
let doc = frm.job_completion_dialog;
|
||||||
|
|
||||||
|
doc.set_value("completed_qty", doc.get_value("for_quantity"));
|
||||||
|
doc.set_value("process_loss_qty", 0);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
fieldtype: "Float",
|
||||||
|
label: __("Completed Quantity"),
|
||||||
|
fieldname: "completed_qty",
|
||||||
|
reqd: 1,
|
||||||
|
default: frm.doc.for_quantity - frm.doc.total_completed_qty,
|
||||||
|
change() {
|
||||||
|
let doc = frm.job_completion_dialog;
|
||||||
|
|
||||||
|
let process_loss_qty = doc.get_value("for_quantity") - doc.get_value("completed_qty");
|
||||||
|
if (process_loss_qty > 0 && process_loss_qty != doc.get_value("process_loss_qty")) {
|
||||||
|
doc.set_value("process_loss_qty", process_loss_qty);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Float",
|
||||||
|
label: __("Process Loss Quantity"),
|
||||||
|
fieldname: "process_loss_qty",
|
||||||
|
reqd: 1,
|
||||||
|
onchange() {
|
||||||
|
let doc = frm.job_completion_dialog;
|
||||||
|
|
||||||
|
let completed_qty = doc.get_value("for_quantity") - doc.get_value("process_loss_qty");
|
||||||
|
doc.set_value("completed_qty", completed_qty);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Section Break",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let last_completed_row = get_last_completed_row(frm.doc.time_logs);
|
||||||
|
if (!last_completed_row || !last_completed_row.to_time) {
|
||||||
|
fields.push({
|
||||||
fieldtype: "Datetime",
|
fieldtype: "Datetime",
|
||||||
label: __("End Time"),
|
label: __("End Time"),
|
||||||
fieldname: "end_time",
|
fieldname: "end_time",
|
||||||
default: frappe.datetime.now_datetime(),
|
default: frappe.datetime.now_datetime(),
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
frappe.prompt(
|
frm.job_completion_dialog = frappe.prompt(
|
||||||
fields,
|
fields,
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.qty <= 0) {
|
if (data.qty <= 0) {
|
||||||
@@ -238,7 +286,8 @@ frappe.ui.form.on("Job Card", {
|
|||||||
method: "complete_job_card",
|
method: "complete_job_card",
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
args: {
|
args: {
|
||||||
qty: data.qty,
|
qty: data.completed_qty,
|
||||||
|
for_quantity: data.for_quantity,
|
||||||
end_time: data.end_time,
|
end_time: data.end_time,
|
||||||
},
|
},
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
@@ -619,15 +668,46 @@ frappe.ui.form.on("Job Card", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.form.on("Job Card Time Log", {
|
frappe.ui.form.on("Job Card Time Log", {
|
||||||
completed_qty: function (frm) {
|
completed_qty: function (frm, cdt, cdn) {
|
||||||
|
let row = locals[cdt][cdn];
|
||||||
|
if (!row.completed_qty) {
|
||||||
|
frappe.model.set_value(row.doctype, row.name, {
|
||||||
|
time_in_mins: 0,
|
||||||
|
to_time: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
frm.events.set_total_completed_qty(frm);
|
frm.events.set_total_completed_qty(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
to_time: function (frm) {
|
to_time: function (frm) {
|
||||||
frm.set_value("started_time", "");
|
frm.set_value("started_time", "");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
time_in_mins(frm, cdt, cdn) {
|
||||||
|
let d = locals[cdt][cdn];
|
||||||
|
if (d.time_in_mins) {
|
||||||
|
d.to_time = add_mins_to_time(d.from_time, d.time_in_mins);
|
||||||
|
frappe.model.set_value(cdt, cdn, "to_time", d.to_time);
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function get_seconds_diff(d1, d2) {
|
function get_seconds_diff(d1, d2) {
|
||||||
return moment(d1).diff(d2, "seconds");
|
return moment(d1).diff(d2, "seconds");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function add_mins_to_time(datetime, mins) {
|
||||||
|
let new_date = moment(datetime).add(mins, "minutes");
|
||||||
|
|
||||||
|
return new_date.format("YYYY-MM-DD HH:mm:ss");
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_last_completed_row(time_logs) {
|
||||||
|
let completed_rows = time_logs.filter((d) => d.to_time);
|
||||||
|
|
||||||
|
if (completed_rows?.length) {
|
||||||
|
let last_completed_row = completed_rows[completed_rows.length - 1];
|
||||||
|
return last_completed_row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@
|
|||||||
"status",
|
"status",
|
||||||
"operation_row_id",
|
"operation_row_id",
|
||||||
"is_paused",
|
"is_paused",
|
||||||
|
"track_semi_finished_goods",
|
||||||
"column_break_20",
|
"column_break_20",
|
||||||
"operation_row_number",
|
"operation_row_number",
|
||||||
"operation_id",
|
"operation_id",
|
||||||
@@ -525,15 +526,16 @@
|
|||||||
"fieldname": "finished_good",
|
"fieldname": "finished_good",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_preview": 1,
|
"in_preview": 1,
|
||||||
"label": "Finished Good",
|
"label": "Item to Manufacture",
|
||||||
"options": "Item",
|
"options": "Item",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:doc.track_semi_finished_goods",
|
||||||
"fieldname": "target_warehouse",
|
"fieldname": "target_warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Target Warehouse",
|
"label": "Target Warehouse",
|
||||||
"mandatory_depends_on": "eval:doc.finished_good",
|
"mandatory_depends_on": "eval:doc.track_semi_finished_goods",
|
||||||
"options": "Warehouse"
|
"options": "Warehouse"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -555,7 +557,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "semi_fg_bom",
|
"fieldname": "semi_fg_bom",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Semi Finished Goods BOM",
|
"label": "Manufacturing BOM",
|
||||||
"options": "BOM",
|
"options": "BOM",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -610,11 +612,19 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Is Paused",
|
"label": "Is Paused",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fetch_from": "work_order.track_semi_finished_goods",
|
||||||
|
"fieldname": "track_semi_finished_goods",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Track Semi Finished Goods"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-06-03 17:44:18.324743",
|
"modified": "2025-03-30 18:53:38.206399",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Job Card",
|
"name": "Job Card",
|
||||||
@@ -667,10 +677,11 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"show_preview_popup": 1,
|
"show_preview_popup": 1,
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "operation",
|
"title_field": "operation",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ class JobCard(Document):
|
|||||||
time_required: DF.Float
|
time_required: DF.Float
|
||||||
total_completed_qty: DF.Float
|
total_completed_qty: DF.Float
|
||||||
total_time_in_mins: DF.Float
|
total_time_in_mins: DF.Float
|
||||||
|
track_semi_finished_goods: DF.Check
|
||||||
transferred_qty: DF.Float
|
transferred_qty: DF.Float
|
||||||
wip_warehouse: DF.Link | None
|
wip_warehouse: DF.Link | None
|
||||||
work_order: DF.Link
|
work_order: DF.Link
|
||||||
@@ -723,7 +724,7 @@ class JobCard(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_job_card(self):
|
def validate_job_card(self):
|
||||||
if self.finished_good:
|
if self.track_semi_finished_goods:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.work_order and frappe.get_cached_value("Work Order", self.work_order, "status") == "Stopped":
|
if self.work_order and frappe.get_cached_value("Work Order", self.work_order, "status") == "Stopped":
|
||||||
@@ -794,7 +795,7 @@ class JobCard(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def update_work_order(self):
|
def update_work_order(self):
|
||||||
if self.finished_good:
|
if self.track_semi_finished_goods:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.work_order:
|
if not self.work_order:
|
||||||
@@ -1037,7 +1038,7 @@ class JobCard(Document):
|
|||||||
if self.docstatus == 0 and self.time_logs:
|
if self.docstatus == 0 and self.time_logs:
|
||||||
self.status = "Work In Progress"
|
self.status = "Work In Progress"
|
||||||
|
|
||||||
if not self.finished_good and self.docstatus < 2:
|
if not self.track_semi_finished_goods and self.docstatus < 2:
|
||||||
if flt(self.for_quantity) <= flt(self.transferred_qty):
|
if flt(self.for_quantity) <= flt(self.transferred_qty):
|
||||||
self.status = "Material Transferred"
|
self.status = "Material Transferred"
|
||||||
|
|
||||||
@@ -1187,6 +1188,14 @@ class JobCard(Document):
|
|||||||
row = self.append("time_logs", kwargs)
|
row = self.append("time_logs", kwargs)
|
||||||
row.db_update()
|
row.db_update()
|
||||||
self.db_set("status", "Work In Progress")
|
self.db_set("status", "Work In Progress")
|
||||||
|
elif not kwargs.from_time and not kwargs.to_time and kwargs.completed_qty:
|
||||||
|
update_status = True
|
||||||
|
for row in self.time_logs:
|
||||||
|
if row.employee != kwargs.employee:
|
||||||
|
continue
|
||||||
|
|
||||||
|
row.completed_qty = kwargs.completed_qty
|
||||||
|
row.db_update()
|
||||||
else:
|
else:
|
||||||
update_status = True
|
update_status = True
|
||||||
for row in self.time_logs:
|
for row in self.time_logs:
|
||||||
@@ -1246,6 +1255,13 @@ class JobCard(Document):
|
|||||||
|
|
||||||
if kwargs.end_time:
|
if kwargs.end_time:
|
||||||
self.add_time_logs(to_time=kwargs.end_time, completed_qty=kwargs.qty, employees=self.employee)
|
self.add_time_logs(to_time=kwargs.end_time, completed_qty=kwargs.qty, employees=self.employee)
|
||||||
|
|
||||||
|
if kwargs.for_quantity:
|
||||||
|
self.for_quantity = kwargs.for_quantity
|
||||||
|
|
||||||
|
self.save()
|
||||||
|
else:
|
||||||
|
self.add_time_logs(completed_qty=kwargs.qty, employees=self.employee)
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
if kwargs.auto_submit:
|
if kwargs.auto_submit:
|
||||||
@@ -1423,9 +1439,19 @@ def make_stock_entry(source_name, target_doc=None):
|
|||||||
target.qty = pending_rm_qty
|
target.qty = pending_rm_qty
|
||||||
|
|
||||||
def set_missing_values(source, target):
|
def set_missing_values(source, target):
|
||||||
|
if source.finished_good and not source.target_warehouse:
|
||||||
|
frappe.throw(_("Please set the Target Warehouse in the Job Card"))
|
||||||
|
|
||||||
|
if not source.skip_material_transfer or source.backflush_from_wip_warehouse:
|
||||||
|
if not source.wip_warehouse:
|
||||||
|
frappe.throw(_("Please set the WIP Warehouse in the Job Card"))
|
||||||
|
|
||||||
target.purpose = "Material Transfer for Manufacture"
|
target.purpose = "Material Transfer for Manufacture"
|
||||||
target.from_bom = 1
|
target.from_bom = 1
|
||||||
|
|
||||||
|
if source.semi_fg_bom:
|
||||||
|
target.bom_no = source.semi_fg_bom
|
||||||
|
|
||||||
# avoid negative 'For Quantity'
|
# avoid negative 'For Quantity'
|
||||||
pending_fg_qty = flt(source.get("for_quantity", 0)) - flt(source.get("transferred_qty", 0))
|
pending_fg_qty = flt(source.get("for_quantity", 0)) - flt(source.get("transferred_qty", 0))
|
||||||
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
|
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
|
||||||
|
|||||||
@@ -37,8 +37,7 @@
|
|||||||
"fieldname": "time_in_mins",
|
"fieldname": "time_in_mins",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Time In Mins",
|
"label": "Time In Mins"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
@@ -64,18 +63,20 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-05-21 12:41:55.765860",
|
"modified": "2025-03-25 20:05:13.807905",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Job Card Time Log",
|
"name": "Job Card Time Log",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2788,6 +2788,109 @@ class TestWorkOrder(IntegrationTestCase):
|
|||||||
batch_no = get_batch_from_bundle(row.serial_and_batch_bundle)
|
batch_no = get_batch_from_bundle(row.serial_and_batch_bundle)
|
||||||
self.assertEqual(batch_no, itemwise_batches[row.item_code])
|
self.assertEqual(batch_no, itemwise_batches[row.item_code])
|
||||||
|
|
||||||
|
def test_work_order_valuation_auto_pick(self):
|
||||||
|
fg_item = "Test FG Item For Non Transfer Item Batch"
|
||||||
|
rm_item = "Test RM Item For Non Transfer Item Batch"
|
||||||
|
|
||||||
|
make_item(fg_item, {"is_stock_item": 1})
|
||||||
|
make_item(
|
||||||
|
rm_item,
|
||||||
|
{
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "TST-BATCH-NTI-.###",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
source_warehouse = "_Test Warehouse - _TC"
|
||||||
|
wip_warehouse = "Stores - _TC"
|
||||||
|
finished_goods_warehouse = create_warehouse("_Test Finished Goods Warehouse", company="_Test Company")
|
||||||
|
|
||||||
|
batches = make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse)
|
||||||
|
|
||||||
|
if not frappe.db.get_value("BOM", {"item": fg_item}):
|
||||||
|
make_bom(item=fg_item, raw_materials=[rm_item])
|
||||||
|
|
||||||
|
wo = make_wo_order_test_record(
|
||||||
|
item=fg_item,
|
||||||
|
qty=5,
|
||||||
|
source_warehouse=source_warehouse,
|
||||||
|
wip_warehouse=wip_warehouse,
|
||||||
|
fg_warehouse=finished_goods_warehouse,
|
||||||
|
)
|
||||||
|
|
||||||
|
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 5))
|
||||||
|
stock_entry.items[0].batch_no = batches[1]
|
||||||
|
stock_entry.items[0].use_serial_batch_fields = 1
|
||||||
|
stock_entry.submit()
|
||||||
|
stock_entry.reload()
|
||||||
|
|
||||||
|
self.assertEqual(stock_entry.items[0].valuation_rate, 200)
|
||||||
|
|
||||||
|
original_value = frappe.db.get_single_value(
|
||||||
|
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
|
||||||
|
)
|
||||||
|
original_based_on = frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on")
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1)
|
||||||
|
frappe.db.set_single_value("Stock Settings", "pick_serial_and_batch_based_on", "Expiry")
|
||||||
|
|
||||||
|
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 5))
|
||||||
|
stock_entry.items[0].use_serial_batch_fields = 1
|
||||||
|
stock_entry.submit()
|
||||||
|
stock_entry.reload()
|
||||||
|
|
||||||
|
batch_no = get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle)
|
||||||
|
self.assertEqual(batch_no, batches[1])
|
||||||
|
self.assertEqual(stock_entry.items[0].valuation_rate, 200)
|
||||||
|
self.assertEqual(stock_entry.items[1].valuation_rate, 200)
|
||||||
|
|
||||||
|
frappe.db.set_single_value(
|
||||||
|
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", original_value
|
||||||
|
)
|
||||||
|
frappe.db.set_single_value("Stock Settings", "pick_serial_and_batch_based_on", original_based_on)
|
||||||
|
|
||||||
|
|
||||||
|
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||||
|
make_stock_entry as make_stock_entry_test_record,
|
||||||
|
)
|
||||||
|
|
||||||
|
batches = []
|
||||||
|
for qty, rate in ((5, 100), (5, 200)):
|
||||||
|
stock_entry = make_stock_entry_test_record(
|
||||||
|
item_code=rm_item,
|
||||||
|
target=source_warehouse,
|
||||||
|
qty=qty,
|
||||||
|
basic_rate=rate,
|
||||||
|
)
|
||||||
|
stock_entry.submit()
|
||||||
|
stock_entry.reload()
|
||||||
|
|
||||||
|
batch_no = get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle)
|
||||||
|
batch_doc = frappe.get_doc("Batch", batch_no)
|
||||||
|
|
||||||
|
# keep early expiry date for the batch having rate 200
|
||||||
|
days = 10 if rate == 100 else 1
|
||||||
|
batch_doc.db_set("expiry_date", add_to_date(now(), days=days))
|
||||||
|
|
||||||
|
batches.append(batch_no)
|
||||||
|
|
||||||
|
stock_entry = make_stock_entry_test_record(
|
||||||
|
item_code=rm_item,
|
||||||
|
target=wip_warehouse,
|
||||||
|
qty=qty,
|
||||||
|
basic_rate=rate,
|
||||||
|
)
|
||||||
|
stock_entry.submit()
|
||||||
|
stock_entry.reload()
|
||||||
|
batch_no = get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle)
|
||||||
|
batch_doc = frappe.get_doc("Batch", batch_no)
|
||||||
|
batch_doc.db_set("expiry_date", add_to_date(now(), days=10))
|
||||||
|
|
||||||
|
return batches
|
||||||
|
|
||||||
|
|
||||||
def make_operation(**kwargs):
|
def make_operation(**kwargs):
|
||||||
kwargs = frappe._dict(kwargs)
|
kwargs = frappe._dict(kwargs)
|
||||||
|
|||||||
@@ -161,6 +161,8 @@ frappe.ui.form.on("Work Order", {
|
|||||||
erpnext.work_order.set_custom_buttons(frm);
|
erpnext.work_order.set_custom_buttons(frm);
|
||||||
frm.set_intro("");
|
frm.set_intro("");
|
||||||
|
|
||||||
|
frm.toggle_enable("use_multi_level_bom", !frm.doc.track_semi_finished_goods);
|
||||||
|
|
||||||
if (frm.doc.docstatus === 0 && !frm.is_new()) {
|
if (frm.doc.docstatus === 0 && !frm.is_new()) {
|
||||||
frm.set_intro(__("Submit this Work Order for further processing."));
|
frm.set_intro(__("Submit this Work Order for further processing."));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -402,9 +402,10 @@ erpnext.patches.v15_0.sync_auto_reconcile_config
|
|||||||
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
|
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
|
||||||
erpnext.patches.v14_0.disable_add_row_in_gross_profit
|
erpnext.patches.v14_0.disable_add_row_in_gross_profit
|
||||||
erpnext.patches.v14_0.update_posting_datetime
|
erpnext.patches.v14_0.update_posting_datetime
|
||||||
erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference #2025-03-18
|
erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference
|
||||||
erpnext.patches.v15_0.recalculate_amount_difference_field
|
erpnext.patches.v15_0.recalculate_amount_difference_field #2025-03-18
|
||||||
erpnext.patches.v15_0.rename_sla_fields #2025-03-12
|
erpnext.patches.v15_0.rename_sla_fields #2025-03-12
|
||||||
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
|
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
|
||||||
erpnext.patches.v15_0.update_query_report
|
erpnext.patches.v15_0.update_query_report
|
||||||
erpnext.patches.v15_0.set_purchase_receipt_row_item_to_capitalization_stock_item
|
erpnext.patches.v15_0.set_purchase_receipt_row_item_to_capitalization_stock_item
|
||||||
|
erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
|||||||
|
|
||||||
|
|
||||||
def execute():
|
def execute():
|
||||||
|
if not frappe.db.has_table("Closing Stock Balance"):
|
||||||
|
return
|
||||||
|
|
||||||
add_inventory_dimensions_to_stock_closing_balance()
|
add_inventory_dimensions_to_stock_closing_balance()
|
||||||
create_stock_closing_entries()
|
create_stock_closing_entries()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.query_builder import DocType
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
invoice_types = ["Sales Invoice", "Purchase Invoice"]
|
||||||
|
for invoice_type in invoice_types:
|
||||||
|
invoice = DocType(invoice_type)
|
||||||
|
invoice_details = frappe.qb.from_(invoice).select(invoice.conversion_rate, invoice.name)
|
||||||
|
update_payment_schedule(invoice_details)
|
||||||
|
|
||||||
|
|
||||||
|
def update_payment_schedule(invoice_details):
|
||||||
|
ps = DocType("Payment Schedule")
|
||||||
|
|
||||||
|
frappe.qb.update(ps).join(invoice_details).on(ps.parent == invoice_details.name).set(
|
||||||
|
ps.base_paid_amount, ps.paid_amount * invoice_details.conversion_rate
|
||||||
|
).set(ps.base_outstanding, ps.outstanding * invoice_details.conversion_rate).run()
|
||||||
@@ -621,7 +621,7 @@ 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"));
|
- flt(this.frm.doc.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", "rounding_adjustment"]);
|
||||||
|
|
||||||
@@ -711,22 +711,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"]);
|
||||||
@@ -739,29 +743,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) {
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -335,7 +348,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
let d = locals[cdt][cdn];
|
let d = locals[cdt][cdn];
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
docstatus: ("<", 2),
|
docstatus: ["<", 2],
|
||||||
inspection_type: inspection_type,
|
inspection_type: inspection_type,
|
||||||
reference_name: doc.name,
|
reference_name: doc.name,
|
||||||
item_code: d.item_code
|
item_code: d.item_code
|
||||||
|
|||||||
@@ -447,22 +447,21 @@ erpnext.sales_common = {
|
|||||||
args: { project: this.frm.doc.project },
|
args: { project: this.frm.doc.project },
|
||||||
callback: function (r, rt) {
|
callback: function (r, rt) {
|
||||||
if (!r.exc) {
|
if (!r.exc) {
|
||||||
$.each(me.frm.doc["items"] || [], function (i, row) {
|
if (r.message) {
|
||||||
if (r.message) {
|
$.each(me.frm.doc["items"] || [], function (i, row) {
|
||||||
frappe.model.set_value(
|
frappe.model.set_value(
|
||||||
row.doctype,
|
row.doctype,
|
||||||
row.name,
|
row.name,
|
||||||
"cost_center",
|
"cost_center",
|
||||||
r.message
|
r.message
|
||||||
);
|
);
|
||||||
frappe.msgprint(
|
});
|
||||||
__(
|
frappe.msgprint(
|
||||||
"Cost Center For Item with Item Code {0} has been Changed to {1}",
|
__("Cost Center for Item rows has been updated to {0}", [
|
||||||
[row.item_name, r.message]
|
r.message,
|
||||||
)
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -607,7 +607,7 @@
|
|||||||
padding: var(--padding-sm);
|
padding: var(--padding-sm);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--gray-50);
|
background-color: var(--control-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
> .invoice-name-date {
|
> .invoice-name-date {
|
||||||
@@ -1157,8 +1157,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .new-btn {
|
> .new-btn {
|
||||||
background-color: var(--blue-500);
|
background-color: var(--btn-primary);
|
||||||
color: white;
|
color: var(--neutral);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
{{ address_line1 }}<br>
|
{{ address_line1 }}<br>
|
||||||
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
||||||
{% if country in ["Germany", "Deutschland"] %}
|
{{ pincode }} {{ city | upper }}<br>
|
||||||
{{ pincode }} {{ city }}
|
{{ country | upper }}
|
||||||
{% else %}
|
|
||||||
{{ pincode }} {{ city | upper }}<br>
|
|
||||||
{{ country | upper }}
|
|
||||||
{% endif %}
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{{ address_line1 }}<br>
|
||||||
|
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
||||||
|
{{ pincode }} {{ city | upper }}<br>
|
||||||
|
{{ country | upper }}
|
||||||
@@ -766,6 +766,13 @@ class SalesOrder(SellingController):
|
|||||||
voucher_type=self.doctype, voucher_no=self.name, sre_list=sre_list, notify=notify
|
voucher_type=self.doctype, voucher_no=self.name, sre_list=sre_list, notify=notify
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def set_missing_values(self, for_validate=False):
|
||||||
|
super().set_missing_values(for_validate)
|
||||||
|
|
||||||
|
if self.delivery_date:
|
||||||
|
for item in self.items:
|
||||||
|
item.delivery_date = self.delivery_date
|
||||||
|
|
||||||
|
|
||||||
def get_unreserved_qty(item: object, reserved_qty_details: dict) -> float:
|
def get_unreserved_qty(item: object, reserved_qty_details: dict) -> float:
|
||||||
"""Returns the unreserved quantity for the Sales Order Item."""
|
"""Returns the unreserved quantity for the Sales Order Item."""
|
||||||
|
|||||||
@@ -286,7 +286,6 @@ class DeprecatedBatchNoValuation:
|
|||||||
from erpnext.stock.utils import get_combine_datetime
|
from erpnext.stock.utils import get_combine_datetime
|
||||||
|
|
||||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
batch = frappe.qb.DocType("Batch")
|
|
||||||
|
|
||||||
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
|
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
|
||||||
if not self.sle.creation:
|
if not self.sle.creation:
|
||||||
@@ -301,8 +300,6 @@ class DeprecatedBatchNoValuation:
|
|||||||
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(sle)
|
frappe.qb.from_(sle)
|
||||||
.inner_join(batch)
|
|
||||||
.on(sle.batch_no == batch.name)
|
|
||||||
.select(
|
.select(
|
||||||
sle.stock_value,
|
sle.stock_value,
|
||||||
sle.qty_after_transaction,
|
sle.qty_after_transaction,
|
||||||
@@ -310,7 +307,6 @@ class DeprecatedBatchNoValuation:
|
|||||||
.where(
|
.where(
|
||||||
(sle.item_code == self.sle.item_code)
|
(sle.item_code == self.sle.item_code)
|
||||||
& (sle.warehouse == self.sle.warehouse)
|
& (sle.warehouse == self.sle.warehouse)
|
||||||
& (sle.batch_no.isnotnull())
|
|
||||||
& (sle.is_cancelled == 0)
|
& (sle.is_cancelled == 0)
|
||||||
)
|
)
|
||||||
.where(timestamp_condition)
|
.where(timestamp_condition)
|
||||||
|
|||||||
@@ -1200,7 +1200,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")
|
||||||
|
|
||||||
@@ -1219,9 +1219,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(
|
||||||
|
|||||||
@@ -4126,6 +4126,54 @@ class TestPurchaseReceipt(IntegrationTestCase):
|
|||||||
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
|
||||||
|
|||||||
@@ -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,8 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -108,6 +110,10 @@ class RepostItemValuation(Document):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
@@ -245,6 +251,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")
|
||||||
@@ -263,6 +279,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)
|
||||||
|
|
||||||
@@ -286,7 +305,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:
|
||||||
@@ -301,13 +320,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()
|
||||||
|
|||||||
@@ -1026,10 +1026,6 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (me.frm.doc.company && erpnext.is_perpetual_inventory_enabled(me.frm.doc.company)) {
|
|
||||||
this.frm.add_fetch("company", "stock_adjustment_account", "expense_account");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.frm.fields_dict.items.grid.get_field("expense_account").get_query = function () {
|
this.frm.fields_dict.items.grid.get_field("expense_account").get_query = function () {
|
||||||
if (erpnext.is_perpetual_inventory_enabled(me.frm.doc.company)) {
|
if (erpnext.is_perpetual_inventory_enabled(me.frm.doc.company)) {
|
||||||
return {
|
return {
|
||||||
@@ -1143,8 +1139,6 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
|||||||
this.frm.trigger("toggle_display_account_head");
|
this.frm.trigger("toggle_display_account_head");
|
||||||
|
|
||||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
||||||
if (this.frm.doc.company && erpnext.is_perpetual_inventory_enabled(this.frm.doc.company))
|
|
||||||
this.set_default_account("stock_adjustment_account", "expense_account");
|
|
||||||
this.set_default_account("cost_center", "cost_center");
|
this.set_default_account("cost_center", "cost_center");
|
||||||
|
|
||||||
this.frm.refresh_fields("items");
|
this.frm.refresh_fields("items");
|
||||||
|
|||||||
@@ -1725,7 +1725,7 @@ class StockEntry(StockController):
|
|||||||
if self.purpose == "Material Issue":
|
if self.purpose == "Material Issue":
|
||||||
ret["expense_account"] = item.get("expense_account") or item_group_defaults.get("expense_account")
|
ret["expense_account"] = item.get("expense_account") or item_group_defaults.get("expense_account")
|
||||||
|
|
||||||
if self.purpose == "Manufacture":
|
if self.purpose == "Manufacture" or not ret.get("expense_account"):
|
||||||
ret["expense_account"] = frappe.get_cached_value(
|
ret["expense_account"] = frappe.get_cached_value(
|
||||||
"Company", self.company, "stock_adjustment_account"
|
"Company", self.company, "stock_adjustment_account"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -250,6 +250,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||||
|
"fetch_from": "item_code.stock_uom",
|
||||||
"fieldname": "stock_uom",
|
"fieldname": "stock_uom",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Stock UOM",
|
"label": "Stock UOM",
|
||||||
@@ -588,7 +589,8 @@
|
|||||||
"label": "Serial and Batch Bundle",
|
"label": "Serial and Batch Bundle",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Serial and Batch Bundle",
|
"options": "Serial and Batch Bundle",
|
||||||
"print_hide": 1
|
"print_hide": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -606,18 +608,20 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:44.056282",
|
"modified": "2025-03-26 21:00:58.544797",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Entry Detail",
|
"name": "Stock Entry Detail",
|
||||||
"naming_rule": "Random",
|
"naming_rule": "Random",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -498,6 +500,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",
|
||||||
@@ -505,7 +518,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-02-28 15: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",
|
||||||
@@ -526,8 +539,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -185,11 +185,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,
|
||||||
)
|
)
|
||||||
@@ -1213,9 +1218,21 @@ class update_entries_after:
|
|||||||
frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
|
frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
|
||||||
|
|
||||||
# Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
|
# Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
|
||||||
if not sle.dependant_sle_voucher_detail_no:
|
if not sle.dependant_sle_voucher_detail_no or self.is_manufacture_entry_with_sabb(sle):
|
||||||
self.recalculate_amounts_in_stock_entry(sle.voucher_no, sle.voucher_detail_no)
|
self.recalculate_amounts_in_stock_entry(sle.voucher_no, sle.voucher_detail_no)
|
||||||
|
|
||||||
|
def is_manufacture_entry_with_sabb(self, sle):
|
||||||
|
if (
|
||||||
|
self.args.get("sle_id")
|
||||||
|
and sle.serial_and_batch_bundle
|
||||||
|
and sle.auto_created_serial_and_batch_bundle
|
||||||
|
):
|
||||||
|
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
|
||||||
|
if purpose in ["Manufacture", "Repack"]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def recalculate_amounts_in_stock_entry(self, voucher_no, voucher_detail_no):
|
def recalculate_amounts_in_stock_entry(self, voucher_no, voucher_detail_no):
|
||||||
stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
|
stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
|
||||||
stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
|
stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
|
||||||
|
|||||||
Reference in New Issue
Block a user