Merge branch 'develop' into make-against-field-dynamic

This commit is contained in:
Gursheen Kaur Anand 2023-11-24 15:23:44 +05:30 committed by GitHub
commit 01f133f8c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
240 changed files with 2807 additions and 1397 deletions

View File

@ -73,8 +73,6 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB
1. [Issue Guidelines](https://github.com/frappe/erpnext/wiki/Issue-Guidelines) 1. [Issue Guidelines](https://github.com/frappe/erpnext/wiki/Issue-Guidelines)
1. [Report Security Vulnerabilities](https://erpnext.com/security) 1. [Report Security Vulnerabilities](https://erpnext.com/security)
1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines) 1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
1. [Translations](https://translate.erpnext.com)
## License ## License

View File

@ -66,7 +66,12 @@
"show_balance_in_coa", "show_balance_in_coa",
"banking_tab", "banking_tab",
"enable_party_matching", "enable_party_matching",
"enable_fuzzy_matching" "enable_fuzzy_matching",
"reports_tab",
"remarks_section",
"general_ledger_remarks_length",
"column_break_lvjk",
"receivable_payable_remarks_length"
], ],
"fields": [ "fields": [
{ {
@ -422,6 +427,34 @@
"fieldname": "round_row_wise_tax", "fieldname": "round_row_wise_tax",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Round Tax Amount Row-wise" "label": "Round Tax Amount Row-wise"
},
{
"fieldname": "reports_tab",
"fieldtype": "Tab Break",
"label": "Reports"
},
{
"default": "0",
"description": "Truncates 'Remarks' column to set character length",
"fieldname": "general_ledger_remarks_length",
"fieldtype": "Int",
"label": "General Ledger"
},
{
"default": "0",
"description": "Truncates 'Remarks' column to set character length",
"fieldname": "receivable_payable_remarks_length",
"fieldtype": "Int",
"label": "Accounts Receivable/Payable"
},
{
"fieldname": "column_break_lvjk",
"fieldtype": "Column Break"
},
{
"fieldname": "remarks_section",
"fieldtype": "Section Break",
"label": "Remarks Column Length"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@ -429,7 +462,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-08-28 00:12:02.740633", "modified": "2023-11-20 09:37:47.650347",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -53,10 +53,18 @@ frappe.ui.form.on('Chart of Accounts Importer', {
of Accounts. Please enter the account names and add more rows as per your requirement.`); of Accounts. Please enter the account names and add more rows as per your requirement.`);
} }
} }
} },
{
label : "Company",
fieldname: "company",
fieldtype: "Link",
reqd: 1,
hidden: 1,
default: frm.doc.company,
},
], ],
primary_action: function() { primary_action: function() {
var data = d.get_values(); let data = d.get_values();
if (!data.template_type) { if (!data.template_type) {
frappe.throw(__('Please select <b>Template Type</b> to download template')); frappe.throw(__('Please select <b>Template Type</b> to download template'));
@ -66,7 +74,8 @@ frappe.ui.form.on('Chart of Accounts Importer', {
'/api/method/erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.download_template', '/api/method/erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.download_template',
{ {
file_type: data.file_type, file_type: data.file_type,
template_type: data.template_type template_type: data.template_type,
company: data.company
} }
); );

View File

@ -8,6 +8,7 @@ from functools import reduce
import frappe import frappe
from frappe import _ from frappe import _
from frappe.desk.form.linked_with import get_linked_fields
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint, cstr from frappe.utils import cint, cstr
from frappe.utils.csvutils import UnicodeWriter from frappe.utils.csvutils import UnicodeWriter
@ -294,10 +295,8 @@ def build_response_as_excel(writer):
@frappe.whitelist() @frappe.whitelist()
def download_template(file_type, template_type): def download_template(file_type, template_type, company):
data = frappe._dict(frappe.local.form_dict) writer = get_template(template_type, company)
writer = get_template(template_type)
if file_type == "CSV": if file_type == "CSV":
# download csv file # download csv file
@ -308,8 +307,7 @@ def download_template(file_type, template_type):
build_response_as_excel(writer) build_response_as_excel(writer)
def get_template(template_type): def get_template(template_type, company):
fields = [ fields = [
"Account Name", "Account Name",
"Parent Account", "Parent Account",
@ -335,34 +333,17 @@ def get_template(template_type):
["", "", "", "", 0, account_type.get("account_type"), account_type.get("root_type")] ["", "", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
) )
else: else:
writer = get_sample_template(writer) writer = get_sample_template(writer, company)
return writer return writer
def get_sample_template(writer): def get_sample_template(writer, company):
template = [ currency = frappe.db.get_value("Company", company, "default_currency")
["Application Of Funds(Assets)", "", "", "", 1, "", "Asset"], with open(os.path.join(os.path.dirname(__file__), "coa_sample_template.csv"), "r") as f:
["Sources Of Funds(Liabilities)", "", "", "", 1, "", "Liability"], for row in f:
["Equity", "", "", "", 1, "", "Equity"], row = row.strip().split(",") + [currency]
["Expenses", "", "", "", 1, "", "Expense"], writer.writerow(row)
["Income", "", "", "", 1, "", "Income"],
["Bank Accounts", "Application Of Funds(Assets)", "", "", 1, "Bank", "Asset"],
["Cash In Hand", "Application Of Funds(Assets)", "", "", 1, "Cash", "Asset"],
["Stock Assets", "Application Of Funds(Assets)", "", "", 1, "Stock", "Asset"],
["Cost Of Goods Sold", "Expenses", "", "", 0, "Cost of Goods Sold", "Expense"],
["Asset Depreciation", "Expenses", "", "", 0, "Depreciation", "Expense"],
["Fixed Assets", "Application Of Funds(Assets)", "", "", 0, "Fixed Asset", "Asset"],
["Accounts Payable", "Sources Of Funds(Liabilities)", "", "", 0, "Payable", "Liability"],
["Accounts Receivable", "Application Of Funds(Assets)", "", "", 1, "Receivable", "Asset"],
["Stock Expenses", "Expenses", "", "", 0, "Stock Adjustment", "Expense"],
["Sample Bank", "Bank Accounts", "", "", 0, "Bank", "Asset"],
["Cash", "Cash In Hand", "", "", 0, "Cash", "Asset"],
["Stores", "Stock Assets", "", "", 0, "Stock", "Asset"],
]
for row in template:
writer.writerow(row)
return writer return writer
@ -453,14 +434,11 @@ def get_mandatory_account_types():
def unset_existing_data(company): def unset_existing_data(company):
linked = frappe.db.sql(
'''select fieldname from tabDocField
where fieldtype="Link" and options="Account" and parent="Company"''',
as_dict=True,
)
# remove accounts data from company # remove accounts data from company
update_values = {d.fieldname: "" for d in linked}
fieldnames = get_linked_fields("Account").get("Company", {}).get("fieldname", [])
linked = [{"fieldname": name} for name in fieldnames]
update_values = {d.get("fieldname"): "" for d in linked}
frappe.db.set_value("Company", company, update_values, update_values) frappe.db.set_value("Company", company, update_values, update_values)
# remove accounts data from various doctypes # remove accounts data from various doctypes

View File

@ -0,0 +1,17 @@
Application Of Funds(Assets),,,,1,,Asset
Sources Of Funds(Liabilities),,,,1,,Liability
Equity,,,,1,,Equity
Expenses,,,,1,Expense Account,Expense
Income,,,,1,Income Account,Income
Bank Accounts,Application Of Funds(Assets),,,1,Bank,Asset
Cash In Hand,Application Of Funds(Assets),,,1,Cash,Asset
Stock Assets,Application Of Funds(Assets),,,1,Stock,Asset
Cost Of Goods Sold,Expenses,,,0,Cost of Goods Sold,Expense
Asset Depreciation,Expenses,,,0,Depreciation,Expense
Fixed Assets,Application Of Funds(Assets),,,0,Fixed Asset,Asset
Accounts Payable,Sources Of Funds(Liabilities),,,0,Payable,Liability
Accounts Receivable,Application Of Funds(Assets),,,1,Receivable,Asset
Stock Expenses,Expenses,,,0,Stock Adjustment,Expense
Sample Bank,Bank Accounts,,,0,Bank,Asset
Cash,Cash In Hand,,,0,Cash,Asset
Stores,Stock Assets,,,0,Stock,Asset
1 Application Of Funds(Assets) 1 Asset
2 Sources Of Funds(Liabilities) 1 Liability
3 Equity 1 Equity
4 Expenses 1 Expense Account Expense
5 Income 1 Income Account Income
6 Bank Accounts Application Of Funds(Assets) 1 Bank Asset
7 Cash In Hand Application Of Funds(Assets) 1 Cash Asset
8 Stock Assets Application Of Funds(Assets) 1 Stock Asset
9 Cost Of Goods Sold Expenses 0 Cost of Goods Sold Expense
10 Asset Depreciation Expenses 0 Depreciation Expense
11 Fixed Assets Application Of Funds(Assets) 0 Fixed Asset Asset
12 Accounts Payable Sources Of Funds(Liabilities) 0 Payable Liability
13 Accounts Receivable Application Of Funds(Assets) 1 Receivable Asset
14 Stock Expenses Expenses 0 Stock Adjustment Expense
15 Sample Bank Bank Accounts 0 Bank Asset
16 Cash Cash In Hand 0 Cash Asset
17 Stores Stock Assets 0 Stock Asset

View File

@ -51,7 +51,7 @@ frappe.ui.form.on("Journal Entry", {
}, __('Make')); }, __('Make'));
} }
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
}, },
before_save: function(frm) { before_save: function(frm) {
if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) { if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) {

View File

@ -548,8 +548,16 @@
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 176, "idx": 176,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [
"modified": "2023-08-10 14:32:22.366895", {
"is_child_table": 1,
"link_doctype": "Bank Transaction Payments",
"link_fieldname": "payment_entry",
"parent_doctype": "Bank Transaction",
"table_fieldname": "payment_entries"
}
],
"modified": "2023-11-23 12:11:04.128015",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry", "name": "Journal Entry",

View File

@ -910,7 +910,7 @@ class JournalEntry(AccountsController):
party_account_currency = d.account_currency party_account_currency = d.account_currency
elif frappe.get_cached_value("Account", d.account, "account_type") in ["Bank", "Cash"]: elif frappe.get_cached_value("Account", d.account, "account_type") in ["Bank", "Cash"]:
bank_amount += d.debit_in_account_currency or d.credit_in_account_currency bank_amount += flt(d.debit_in_account_currency) or flt(d.credit_in_account_currency)
bank_account_currency = d.account_currency bank_account_currency = d.account_currency
if party_type and pay_to_recd_from: if party_type and pay_to_recd_from:

View File

@ -205,7 +205,8 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Reference Type", "label": "Reference Type",
"no_copy": 1, "no_copy": 1,
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry" "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry",
"search_index": 1
}, },
{ {
"fieldname": "reference_name", "fieldname": "reference_name",
@ -213,7 +214,8 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Reference Name", "label": "Reference Name",
"no_copy": 1, "no_copy": 1,
"options": "reference_type" "options": "reference_type",
"search_index": 1
}, },
{ {
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])", "depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])",
@ -301,7 +303,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-11-08 12:20:21.489496", "modified": "2023-11-23 11:44:25.841187",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry Account", "name": "Journal Entry Account",

View File

@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges");
frappe.ui.form.on('Payment Entry', { frappe.ui.form.on('Payment Entry', {
onload: function(frm) { onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payments', 'Unreconcile Payment Entries']; frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries'];
if(frm.doc.__islocal) { if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
@ -154,13 +154,13 @@ frappe.ui.form.on('Payment Entry', {
frm.events.set_dynamic_labels(frm); frm.events.set_dynamic_labels(frm);
frm.events.show_general_ledger(frm); frm.events.show_general_ledger(frm);
erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm); erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm);
if(frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0})) { if((frm.doc.references) && (frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0}))) {
frm.add_custom_button(__("View Exchange Gain/Loss Journals"), function() { frm.add_custom_button(__("View Exchange Gain/Loss Journals"), function() {
frappe.set_route("List", "Journal Entry", {"voucher_type": "Exchange Gain Or Loss", "reference_name": frm.doc.name}); frappe.set_route("List", "Journal Entry", {"voucher_type": "Exchange Gain Or Loss", "reference_name": frm.doc.name});
}, __('Actions')); }, __('Actions'));
} }
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
}, },
validate_company: (frm) => { validate_company: (frm) => {
@ -853,6 +853,7 @@ frappe.ui.form.on('Payment Entry', {
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding; var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
} else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) { } else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) {
total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount"))
if(paid_amount > total_negative_outstanding) { if(paid_amount > total_negative_outstanding) {
if(total_negative_outstanding == 0) { if(total_negative_outstanding == 0) {
frappe.msgprint( frappe.msgprint(

View File

@ -595,6 +595,7 @@
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Status", "label": "Status",
"no_copy": 1,
"options": "\nDraft\nSubmitted\nCancelled", "options": "\nDraft\nSubmitted\nCancelled",
"read_only": 1 "read_only": 1
}, },
@ -749,8 +750,16 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [
"modified": "2023-06-23 18:07:38.023010", {
"is_child_table": 1,
"link_doctype": "Bank Transaction Payments",
"link_fieldname": "payment_entry",
"parent_doctype": "Bank Transaction",
"table_fieldname": "payment_entries"
}
],
"modified": "2023-11-23 12:07:20.887885",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",

View File

@ -33,6 +33,7 @@ from erpnext.accounts.utils import (
get_account_currency, get_account_currency,
get_balance_on, get_balance_on,
get_outstanding_invoices, get_outstanding_invoices,
get_party_types_from_account_type,
) )
from erpnext.controllers.accounts_controller import ( from erpnext.controllers.accounts_controller import (
AccountsController, AccountsController,
@ -83,7 +84,6 @@ class PaymentEntry(AccountsController):
self.apply_taxes() self.apply_taxes()
self.set_amounts_after_tax() self.set_amounts_after_tax()
self.clear_unallocated_reference_document_rows() self.clear_unallocated_reference_document_rows()
self.validate_payment_against_negative_invoice()
self.validate_transaction_reference() self.validate_transaction_reference()
self.set_title() self.set_title()
self.set_remarks() self.set_remarks()
@ -148,7 +148,7 @@ class PaymentEntry(AccountsController):
"Repost Payment Ledger Items", "Repost Payment Ledger Items",
"Repost Accounting Ledger", "Repost Accounting Ledger",
"Repost Accounting Ledger Items", "Repost Accounting Ledger Items",
"Unreconcile Payments", "Unreconcile Payment",
"Unreconcile Payment Entries", "Unreconcile Payment Entries",
) )
super(PaymentEntry, self).on_cancel() super(PaymentEntry, self).on_cancel()
@ -952,35 +952,6 @@ class PaymentEntry(AccountsController):
self.name, self.name,
) )
def validate_payment_against_negative_invoice(self):
if (self.payment_type != "Pay" or self.party_type != "Customer") and (
self.payment_type != "Receive" or self.party_type != "Supplier"
):
return
total_negative_outstanding = sum(
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
)
paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
additional_charges = sum(flt(d.amount) for d in self.deductions)
if not total_negative_outstanding:
if self.party_type == "Customer":
msg = _("Cannot pay to Customer without any negative outstanding invoice")
else:
msg = _("Cannot receive from Supplier without any negative outstanding invoice")
frappe.throw(msg, InvalidPaymentEntry)
elif paid_amount - additional_charges > total_negative_outstanding:
frappe.throw(
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
fmt_money(total_negative_outstanding)
),
InvalidPaymentEntry,
)
def set_title(self): def set_title(self):
if frappe.flags.in_import and self.title: if frappe.flags.in_import and self.title:
# do not set title dynamically if title exists during data import. # do not set title dynamically if title exists during data import.
@ -1051,6 +1022,7 @@ class PaymentEntry(AccountsController):
self.add_bank_gl_entries(gl_entries) self.add_bank_gl_entries(gl_entries)
self.add_deductions_gl_entries(gl_entries) self.add_deductions_gl_entries(gl_entries)
self.add_tax_gl_entries(gl_entries) self.add_tax_gl_entries(gl_entries)
add_regional_gl_entries(gl_entries, self)
return gl_entries return gl_entries
def make_gl_entries(self, cancel=0, adv_adj=0): def make_gl_entries(self, cancel=0, adv_adj=0):
@ -1085,11 +1057,9 @@ class PaymentEntry(AccountsController):
item=self, item=self,
) )
dr_or_cr = (
"credit" if erpnext.get_party_account_type(self.party_type) == "Receivable" else "debit"
)
for d in self.get("references"): for d in self.get("references"):
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
cost_center = self.cost_center cost_center = self.cost_center
if d.reference_doctype == "Sales Invoice" and not cost_center: if d.reference_doctype == "Sales Invoice" and not cost_center:
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center") cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
@ -1105,10 +1075,25 @@ class PaymentEntry(AccountsController):
against_voucher_type = d.reference_doctype against_voucher_type = d.reference_doctype
against_voucher = d.reference_name against_voucher = d.reference_name
reverse_dr_or_cr = 0
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
payable_party_types = get_party_types_from_account_type("Payable")
receivable_party_types = get_party_types_from_account_type("Receivable")
if is_return and self.party_type in receivable_party_types and (self.payment_type == "Pay"):
reverse_dr_or_cr = 1
elif (
is_return and self.party_type in payable_party_types and (self.payment_type == "Receive")
):
reverse_dr_or_cr = 1
if is_return and not reverse_dr_or_cr:
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
gle.update( gle.update(
{ {
dr_or_cr: allocated_amount_in_company_currency, dr_or_cr: abs(allocated_amount_in_company_currency),
dr_or_cr + "_in_account_currency": d.allocated_amount, dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
"against_voucher_type": against_voucher_type, "against_voucher_type": against_voucher_type,
"against_voucher": against_voucher, "against_voucher": against_voucher,
"cost_center": cost_center, "cost_center": cost_center,
@ -1116,6 +1101,7 @@ class PaymentEntry(AccountsController):
) )
gl_entries.append(gle) gl_entries.append(gle)
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
if self.unallocated_amount: if self.unallocated_amount:
exchange_rate = self.get_exchange_rate() exchange_rate = self.get_exchange_rate()
base_unallocated_amount = self.unallocated_amount * exchange_rate base_unallocated_amount = self.unallocated_amount * exchange_rate
@ -1711,13 +1697,42 @@ def get_outstanding_reference_documents(args, validate=False):
return data return data
def split_invoices_based_on_payment_terms(outstanding_invoices, company): def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list:
invoice_ref_based_on_payment_terms = {} """Split a list of invoices based on their payment terms."""
exc_rates = get_currency_data(outstanding_invoices, company)
outstanding_invoices_after_split = []
for entry in outstanding_invoices:
if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
if payment_term_template := frappe.db.get_value(
entry.voucher_type, entry.voucher_no, "payment_terms_template"
):
split_rows = get_split_invoice_rows(entry, payment_term_template, exc_rates)
if not split_rows:
continue
frappe.msgprint(
_("Splitting {0} {1} into {2} rows as per Payment Terms").format(
_(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows)
),
alert=True,
)
outstanding_invoices_after_split += split_rows
continue
# If not an invoice or no payment terms template, add as it is
outstanding_invoices_after_split.append(entry)
return outstanding_invoices_after_split
def get_currency_data(outstanding_invoices: list, company: str = None) -> dict:
"""Get currency and conversion data for a list of invoices."""
exc_rates = frappe._dict()
company_currency = ( company_currency = (
frappe.db.get_value("Company", company, "default_currency") if company else None frappe.db.get_value("Company", company, "default_currency") if company else None
) )
exc_rates = frappe._dict()
for doctype in ["Sales Invoice", "Purchase Invoice"]: for doctype in ["Sales Invoice", "Purchase Invoice"]:
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype] invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
for x in frappe.db.get_all( for x in frappe.db.get_all(
@ -1732,72 +1747,54 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company):
company_currency=company_currency, company_currency=company_currency,
) )
for idx, d in enumerate(outstanding_invoices): return exc_rates
if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
payment_term_template = frappe.db.get_value(
d.voucher_type, d.voucher_no, "payment_terms_template" def get_split_invoice_rows(invoice: dict, payment_term_template: str, exc_rates: dict) -> list:
"""Split invoice based on its payment schedule table."""
split_rows = []
allocate_payment_based_on_payment_terms = frappe.db.get_value(
"Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
)
if not allocate_payment_based_on_payment_terms:
return [invoice]
payment_schedule = frappe.get_all(
"Payment Schedule", filters={"parent": invoice.voucher_no}, fields=["*"], order_by="due_date"
)
for payment_term in payment_schedule:
if not payment_term.outstanding > 0.1:
continue
doc_details = exc_rates.get(payment_term.parent, None)
is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
doc_details.party_account_currency != doc_details.company_currency
)
payment_term_outstanding = flt(payment_term.outstanding)
if not is_multi_currency_acc:
payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
split_rows.append(
frappe._dict(
{
"due_date": invoice.due_date,
"currency": invoice.currency,
"voucher_no": invoice.voucher_no,
"voucher_type": invoice.voucher_type,
"posting_date": invoice.posting_date,
"invoice_amount": flt(invoice.invoice_amount),
"outstanding_amount": payment_term_outstanding
if payment_term_outstanding
else invoice.outstanding_amount,
"payment_term_outstanding": payment_term_outstanding,
"payment_amount": payment_term.payment_amount,
"payment_term": payment_term.payment_term,
}
) )
if payment_term_template: )
allocate_payment_based_on_payment_terms = frappe.get_cached_value(
"Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
)
if allocate_payment_based_on_payment_terms:
payment_schedule = frappe.get_all(
"Payment Schedule", filters={"parent": d.voucher_no}, fields=["*"]
)
for payment_term in payment_schedule: return split_rows
if payment_term.outstanding > 0.1:
doc_details = exc_rates.get(payment_term.parent, None)
is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
doc_details.party_account_currency != doc_details.company_currency
)
payment_term_outstanding = flt(payment_term.outstanding)
if not is_multi_currency_acc:
payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
invoice_ref_based_on_payment_terms.setdefault(idx, [])
invoice_ref_based_on_payment_terms[idx].append(
frappe._dict(
{
"due_date": d.due_date,
"currency": d.currency,
"voucher_no": d.voucher_no,
"voucher_type": d.voucher_type,
"posting_date": d.posting_date,
"invoice_amount": flt(d.invoice_amount),
"outstanding_amount": payment_term_outstanding
if payment_term_outstanding
else d.outstanding_amount,
"payment_term_outstanding": payment_term_outstanding,
"payment_amount": payment_term.payment_amount,
"payment_term": payment_term.payment_term,
"account": d.account,
}
)
)
outstanding_invoices_after_split = []
if invoice_ref_based_on_payment_terms:
for idx, ref in invoice_ref_based_on_payment_terms.items():
voucher_no = ref[0]["voucher_no"]
voucher_type = ref[0]["voucher_type"]
frappe.msgprint(
_("Spliting {} {} into {} row(s) as per Payment Terms").format(
voucher_type, voucher_no, len(ref)
),
alert=True,
)
outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx]
existing_row = list(filter(lambda x: x.get("voucher_no") == voucher_no, outstanding_invoices))
index = outstanding_invoices.index(existing_row[0])
outstanding_invoices.pop(index)
outstanding_invoices_after_split += outstanding_invoices
return outstanding_invoices_after_split
def get_orders_to_be_billed( def get_orders_to_be_billed(
@ -2638,3 +2635,8 @@ def make_payment_order(source_name, target_doc=None):
) )
return doclist return doclist
@erpnext.allow_regional
def add_regional_gl_entries(gl_entries, doc):
return

View File

@ -6,10 +6,11 @@ import unittest
import frappe import frappe
from frappe import qb from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import flt, nowdate from frappe.utils import add_days, flt, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import ( from erpnext.accounts.doctype.payment_entry.payment_entry import (
InvalidPaymentEntry, InvalidPaymentEntry,
get_outstanding_reference_documents,
get_payment_entry, get_payment_entry,
get_reference_details, get_reference_details,
) )
@ -683,17 +684,6 @@ class TestPaymentEntry(FrappeTestCase):
self.validate_gl_entries(pe.name, expected_gle) self.validate_gl_entries(pe.name, expected_gle)
def test_payment_against_negative_sales_invoice(self): def test_payment_against_negative_sales_invoice(self):
pe1 = frappe.new_doc("Payment Entry")
pe1.payment_type = "Pay"
pe1.company = "_Test Company"
pe1.party_type = "Customer"
pe1.party = "_Test Customer"
pe1.paid_from = "_Test Cash - _TC"
pe1.paid_amount = 100
pe1.received_amount = 100
self.assertRaises(InvalidPaymentEntry, pe1.validate)
si1 = create_sales_invoice() si1 = create_sales_invoice()
# create full payment entry against si1 # create full payment entry against si1
@ -751,8 +741,6 @@ class TestPaymentEntry(FrappeTestCase):
# pay more than outstanding against si1 # pay more than outstanding against si1
pe3 = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Cash - _TC") pe3 = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Cash - _TC")
pe3.paid_amount = pe3.received_amount = 300
self.assertRaises(InvalidPaymentEntry, pe3.validate)
# pay negative outstanding against si1 # pay negative outstanding against si1
pe3.paid_to = "Debtors - _TC" pe3.paid_to = "Debtors - _TC"
@ -1262,6 +1250,130 @@ class TestPaymentEntry(FrappeTestCase):
so.reload() so.reload()
self.assertEqual(so.advance_paid, so.rounded_total) self.assertEqual(so.advance_paid, so.rounded_total)
def test_outstanding_invoices_api(self):
"""
Test if `get_outstanding_reference_documents` fetches invoices in the right order.
"""
customer = create_customer("Max Mustermann", "INR")
create_payment_terms_template()
# SI has an earlier due date and SI2 has a later due date
si = create_sales_invoice(
qty=1, rate=100, customer=customer, posting_date=add_days(nowdate(), -4)
)
si2 = create_sales_invoice(do_not_save=1, qty=1, rate=100, customer=customer)
si2.payment_terms_template = "Test Receivable Template"
si2.submit()
args = {
"posting_date": nowdate(),
"company": "_Test Company",
"party_type": "Customer",
"payment_type": "Pay",
"party": customer,
"party_account": "Debtors - _TC",
}
args.update(
{
"get_outstanding_invoices": True,
"from_posting_date": add_days(nowdate(), -4),
"to_posting_date": add_days(nowdate(), 2),
}
)
references = get_outstanding_reference_documents(args)
self.assertEqual(len(references), 3)
self.assertEqual(references[0].voucher_no, si.name)
self.assertEqual(references[1].voucher_no, si2.name)
self.assertEqual(references[2].voucher_no, si2.name)
self.assertEqual(references[1].payment_term, "Basic Amount Receivable")
self.assertEqual(references[2].payment_term, "Tax Receivable")
def test_receive_payment_from_payable_party_type(self):
"""
Checks GL entries generated while receiving payments from a Payable Party Type.
"""
pe = create_payment_entry(
party_type="Supplier",
party="_Test Supplier",
payment_type="Receive",
paid_from="Creditors - _TC",
paid_to="_Test Cash - _TC",
save=True,
submit=True,
)
self.voucher_no = pe.name
self.expected_gle = [
{"account": "Creditors - _TC", "debit": 0.0, "credit": 1000.0},
{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
]
self.check_gl_entries()
def test_payment_against_partial_return_invoice(self):
"""
Checks GL entries generated for partial return invoice payments.
"""
si = create_sales_invoice(qty=10, rate=10, customer="_Test Customer")
credit_note = create_sales_invoice(
qty=-4, rate=10, customer="_Test Customer", is_return=1, return_against=si.name
)
pe = create_payment_entry(
party_type="Customer",
party="_Test Customer",
payment_type="Receive",
paid_from="Debtors - _TC",
paid_to="_Test Cash - _TC",
)
pe.set(
"references",
[
{
"reference_doctype": "Sales Invoice",
"reference_name": si.name,
"due_date": si.get("due_date"),
"total_amount": si.grand_total,
"outstanding_amount": si.outstanding_amount,
"allocated_amount": si.outstanding_amount,
},
{
"reference_doctype": "Sales Invoice",
"reference_name": credit_note.name,
"due_date": credit_note.get("due_date"),
"total_amount": credit_note.grand_total,
"outstanding_amount": credit_note.outstanding_amount,
"allocated_amount": credit_note.outstanding_amount,
},
],
)
pe.save()
pe.submit()
self.assertEqual(pe.total_allocated_amount, 60)
self.assertEqual(pe.unallocated_amount, 940)
self.voucher_no = pe.name
self.expected_gle = [
{"account": "Debtors - _TC", "debit": 40.0, "credit": 0.0},
{"account": "Debtors - _TC", "debit": 0.0, "credit": 940.0},
{"account": "Debtors - _TC", "debit": 0.0, "credit": 100.0},
{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
]
self.check_gl_entries()
def check_gl_entries(self):
gle = frappe.qb.DocType("GL Entry")
gl_entries = (
frappe.qb.from_(gle)
.select(
gle.account,
gle.debit,
gle.credit,
)
.where((gle.voucher_no == self.voucher_no) & (gle.is_cancelled == 0))
.orderby(gle.account, gle.debit, gle.credit, order=frappe.qb.desc)
).run(as_dict=True)
for row in range(len(self.expected_gle)):
for field in ["account", "debit", "credit"]:
self.assertEqual(self.expected_gle[row][field], gl_entries[row][field])
def create_payment_entry(**args): def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry") payment_entry = frappe.new_doc("Payment Entry")
@ -1322,6 +1434,9 @@ def create_payment_terms_template():
def create_payment_terms_template_with_discount( def create_payment_terms_template_with_discount(
name=None, discount_type=None, discount=None, template_name=None name=None, discount_type=None, discount=None, template_name=None
): ):
"""
Create a Payment Terms Template with % or amount discount.
"""
create_payment_term(name or "30 Credit Days with 10% Discount") create_payment_term(name or "30 Credit Days with 10% Discount")
template_name = template_name or "Test Discount Template" template_name = template_name or "Test Discount Template"

View File

@ -212,9 +212,10 @@
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
"icon": "icon-resize-horizontal", "icon": "icon-resize-horizontal",
"is_virtual": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-08-15 05:35:50.109290", "modified": "2023-11-17 17:33:55.701726",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Reconciliation", "name": "Payment Reconciliation",
@ -239,6 +240,5 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": []
"track_changes": 1
} }

View File

@ -29,6 +29,58 @@ class PaymentReconciliation(Document):
self.accounting_dimension_filter_conditions = [] self.accounting_dimension_filter_conditions = []
self.ple_posting_date_filter = [] self.ple_posting_date_filter = []
def load_from_db(self):
# 'modified' attribute is required for `run_doc_method` to work properly.
doc_dict = frappe._dict(
{
"modified": None,
"company": None,
"party": None,
"party_type": None,
"receivable_payable_account": None,
"default_advance_account": None,
"from_invoice_date": None,
"to_invoice_date": None,
"invoice_limit": 50,
"from_payment_date": None,
"to_payment_date": None,
"payment_limit": 50,
"minimum_invoice_amount": None,
"minimum_payment_amount": None,
"maximum_invoice_amount": None,
"maximum_payment_amount": None,
"bank_cash_account": None,
"cost_center": None,
"payment_name": None,
"invoice_name": None,
}
)
super(Document, self).__init__(doc_dict)
def save(self):
return
@staticmethod
def get_list(args):
pass
@staticmethod
def get_count(args):
pass
@staticmethod
def get_stats(args):
pass
def db_insert(self, *args, **kwargs):
pass
def db_update(self, *args, **kwargs):
pass
def delete(self):
pass
@frappe.whitelist() @frappe.whitelist()
def get_unreconciled_entries(self): def get_unreconciled_entries(self):
self.get_nonreconciled_payment_entries() self.get_nonreconciled_payment_entries()

View File

@ -1137,6 +1137,40 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(pay.unallocated_amount, 1000) self.assertEqual(pay.unallocated_amount, 1000)
self.assertEqual(pay.difference_amount, 0) self.assertEqual(pay.difference_amount, 0)
def test_rounding_of_unallocated_amount(self):
self.supplier = "_Test Supplier USD"
pi = self.create_purchase_invoice(qty=1, rate=10, do_not_submit=True)
pi.supplier = self.supplier
pi.currency = "USD"
pi.conversion_rate = 80
pi.credit_to = self.creditors_usd
pi.save().submit()
pe = get_payment_entry(pi.doctype, pi.name)
pe.target_exchange_rate = 78.726500000
pe.received_amount = 26.75
pe.paid_amount = 2105.93
pe.references = []
pe.save().submit()
# unallocated_amount will have some rounding loss - 26.749950
self.assertNotEqual(pe.unallocated_amount, 26.75)
pr = frappe.get_doc("Payment Reconciliation")
pr.company = self.company
pr.party_type = "Supplier"
pr.party = self.supplier
pr.receivable_payable_account = self.creditors_usd
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
pr.get_unreconciled_entries()
invoices = [invoice.as_dict() for invoice in pr.invoices]
payments = [payment.as_dict() for payment in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
pr.reconcile()
def make_customer(customer_name, currency=None): def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name): if not frappe.db.exists("Customer", customer_name):

View File

@ -159,9 +159,10 @@
"label": "Difference Posting Date" "label": "Difference Posting Date"
} }
], ],
"is_virtual": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-10-23 10:44:56.066303", "modified": "2023-11-17 17:33:38.612615",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Reconciliation Allocation", "name": "Payment Reconciliation Allocation",

View File

@ -71,9 +71,10 @@
"label": "Exchange Rate" "label": "Exchange Rate"
} }
], ],
"is_virtual": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-11-08 18:18:02.502149", "modified": "2023-11-17 17:33:45.455166",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Reconciliation Invoice", "name": "Payment Reconciliation Invoice",

View File

@ -107,9 +107,10 @@
"options": "Cost Center" "options": "Cost Center"
} }
], ],
"is_virtual": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-09-03 07:43:29.965353", "modified": "2023-11-17 17:33:34.818530",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Reconciliation Payment", "name": "Payment Reconciliation Payment",

View File

@ -175,13 +175,6 @@ class PaymentRequest(Document):
if self.payment_url: if self.payment_url:
self.db_set("payment_url", self.payment_url) self.db_set("payment_url", self.payment_url)
if (
self.payment_url
or not self.payment_gateway_account
or (self.payment_gateway_account and self.payment_channel == "Phone")
):
self.db_set("status", "Initiated")
def get_payment_url(self): def get_payment_url(self):
if self.reference_doctype != "Fees": if self.reference_doctype != "Fees":
data = frappe.db.get_value( data = frappe.db.get_value(

View File

@ -18,6 +18,7 @@
"is_pos", "is_pos",
"is_return", "is_return",
"update_billed_amount_in_sales_order", "update_billed_amount_in_sales_order",
"update_billed_amount_in_delivery_note",
"column_break1", "column_break1",
"company", "company",
"posting_date", "posting_date",
@ -1550,12 +1551,19 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Amount Eligible for Commission", "label": "Amount Eligible for Commission",
"read_only": 1 "read_only": 1
},
{
"default": "1",
"depends_on": "eval: doc.is_return && doc.return_against",
"fieldname": "update_billed_amount_in_delivery_note",
"fieldtype": "Check",
"label": "Update Billed Amount in Delivery Note"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-06-03 16:23:41.083409", "modified": "2023-11-20 12:27:12.848149",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",

View File

@ -556,7 +556,7 @@ def get_stock_availability(item_code, warehouse):
return bin_qty - pos_sales_qty, is_stock_item return bin_qty - pos_sales_qty, is_stock_item
else: else:
is_stock_item = True is_stock_item = True
if frappe.db.exists("Product Bundle", item_code): if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}):
return get_bundle_availability(item_code, warehouse), is_stock_item return get_bundle_availability(item_code, warehouse), is_stock_item
else: else:
is_stock_item = False is_stock_item = False

View File

@ -6,7 +6,6 @@ import unittest
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import add_days, nowdate
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
@ -126,64 +125,70 @@ class TestPOSInvoice(unittest.TestCase):
self.assertEqual(inv.grand_total, 5474.0) self.assertEqual(inv.grand_total, 5474.0)
def test_tax_calculation_with_item_tax_template(self): def test_tax_calculation_with_item_tax_template(self):
import json inv = create_pos_invoice(qty=84, rate=4.6, do_not_save=1)
item_row = inv.get("items")[0]
from erpnext.stock.get_item_details import get_item_details add_items = [
(54, "_Test Account Excise Duty @ 12 - _TC"),
# set tax template in item (288, "_Test Account Excise Duty @ 15 - _TC"),
item = frappe.get_cached_doc("Item", "_Test Item") (144, "_Test Account Excise Duty @ 20 - _TC"),
item.set( (430, "_Test Item Tax Template 1 - _TC"),
"taxes",
[
{
"item_tax_template": "_Test Account Excise Duty @ 15 - _TC",
"valid_from": add_days(nowdate(), -5),
}
],
)
item.save()
# create POS invoice with item
pos_inv = create_pos_invoice(qty=84, rate=4.6, do_not_save=True)
item_details = get_item_details(
doc=pos_inv,
args={
"item_code": item.item_code,
"company": pos_inv.company,
"doctype": "POS Invoice",
"conversion_rate": 1.0,
},
)
tax_map = json.loads(item_details.item_tax_rate)
for tax in tax_map:
pos_inv.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": tax,
"rate": tax_map[tax],
"description": "Test",
"cost_center": "_Test Cost Center - _TC",
},
)
pos_inv.submit()
pos_inv.load_from_db()
# check if correct tax values are applied from tax template
self.assertEqual(pos_inv.net_total, 386.4)
expected_taxes = [
{
"tax_amount": 57.96,
"total": 444.36,
},
] ]
for qty, item_tax_template in add_items:
item_row_copy = copy.deepcopy(item_row)
item_row_copy.qty = qty
item_row_copy.item_tax_template = item_tax_template
inv.append("items", item_row_copy)
for i in range(len(expected_taxes)): inv.append(
for key in expected_taxes[i]: "taxes",
self.assertEqual(expected_taxes[i][key], pos_inv.get("taxes")[i].get(key)) {
"account_head": "_Test Account Excise Duty - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "Excise Duty",
"doctype": "Sales Taxes and Charges",
"rate": 11,
},
)
inv.append(
"taxes",
{
"account_head": "_Test Account Education Cess - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "Education Cess",
"doctype": "Sales Taxes and Charges",
"rate": 0,
},
)
inv.append(
"taxes",
{
"account_head": "_Test Account S&H Education Cess - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "S&H Education Cess",
"doctype": "Sales Taxes and Charges",
"rate": 3,
},
)
inv.insert()
self.assertEqual(pos_inv.get("base_total_taxes_and_charges"), 57.96) self.assertEqual(inv.net_total, 4600)
self.assertEqual(inv.get("taxes")[0].tax_amount, 502.41)
self.assertEqual(inv.get("taxes")[0].total, 5102.41)
self.assertEqual(inv.get("taxes")[1].tax_amount, 197.80)
self.assertEqual(inv.get("taxes")[1].total, 5300.21)
self.assertEqual(inv.get("taxes")[2].tax_amount, 375.36)
self.assertEqual(inv.get("taxes")[2].total, 5675.57)
self.assertEqual(inv.grand_total, 5675.57)
self.assertEqual(inv.rounding_adjustment, 0.43)
self.assertEqual(inv.rounded_total, 5676.0)
def test_tax_calculation_with_multiple_items_and_discount(self): def test_tax_calculation_with_multiple_items_and_discount(self):
inv = create_pos_invoice(qty=1, rate=75, do_not_save=True) inv = create_pos_invoice(qty=1, rate=75, do_not_save=True)

View File

@ -186,6 +186,7 @@
"label": "Image" "label": "Image"
}, },
{ {
"fetch_from": "item_code.image",
"fieldname": "image", "fieldname": "image",
"fieldtype": "Attach", "fieldtype": "Attach",
"hidden": 1, "hidden": 1,
@ -833,7 +834,7 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-03-12 13:36:40.160468", "modified": "2023-11-14 18:33:22.585715",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice Item", "name": "POS Invoice Item",

View File

@ -180,7 +180,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
} }
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1); this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm); erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
} }
unblock_invoice() { unblock_invoice() {

View File

@ -13,6 +13,7 @@ from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import ( from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
validate_docs_for_deferred_accounting, validate_docs_for_deferred_accounting,
validate_docs_for_voucher_types,
) )
from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
check_if_return_invoice_linked_with_payment_entry, check_if_return_invoice_linked_with_payment_entry,
@ -491,6 +492,7 @@ class PurchaseInvoice(BuyingController):
def validate_for_repost(self): def validate_for_repost(self):
self.validate_write_off_account() self.validate_write_off_account()
self.validate_expense_account() self.validate_expense_account()
validate_docs_for_voucher_types(["Purchase Invoice"])
validate_docs_for_deferred_accounting([], [self.name]) validate_docs_for_deferred_accounting([], [self.name])
def on_submit(self): def on_submit(self):
@ -525,7 +527,11 @@ class PurchaseInvoice(BuyingController):
if self.update_stock == 1: if self.update_stock == 1:
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.update_project() if (
frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction"
):
self.update_project()
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.update_advance_tax_references() self.update_advance_tax_references()
@ -1302,7 +1308,10 @@ class PurchaseInvoice(BuyingController):
if self.update_stock == 1: if self.update_stock == 1:
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.update_project() if (
frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction"
):
self.update_project()
self.db_set("status", "Cancelled") self.db_set("status", "Cancelled")
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
@ -1321,13 +1330,21 @@ class PurchaseInvoice(BuyingController):
self.update_advance_tax_references(cancel=1) self.update_advance_tax_references(cancel=1)
def update_project(self): def update_project(self):
project_list = [] projects = frappe._dict()
for d in self.items: for d in self.items:
if d.project and d.project not in project_list: if d.project:
project = frappe.get_doc("Project", d.project) if self.docstatus == 1:
project.update_purchase_costing() projects[d.project] = projects.get(d.project, 0) + d.base_net_amount
project.db_update() elif self.docstatus == 2:
project_list.append(d.project) projects[d.project] = projects.get(d.project, 0) - d.base_net_amount
pj = frappe.qb.DocType("Project")
for proj, value in projects.items():
res = (
frappe.qb.from_(pj).select(pj.total_purchase_cost).where(pj.name == proj).for_update().run()
)
current_purchase_cost = res and res[0][0] or 0
frappe.db.set_value("Project", proj, "total_purchase_cost", current_purchase_cost + value)
def validate_supplier_invoice(self): def validate_supplier_invoice(self):
if self.bill_date: if self.bill_date:

View File

@ -1783,9 +1783,14 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
set_advance_flag(company="_Test Company", flag=0, default_account="") set_advance_flag(company="_Test Company", flag=0, default_account="")
def test_gl_entries_for_standalone_debit_note(self): def test_gl_entries_for_standalone_debit_note(self):
make_purchase_invoice(qty=5, rate=500, update_stock=True) from erpnext.stock.doctype.item.test_item import make_item
returned_inv = make_purchase_invoice(qty=-5, rate=5, update_stock=True, is_return=True) item_code = make_item(properties={"is_stock_item": 1})
make_purchase_invoice(item_code=item_code, qty=5, rate=500, update_stock=True)
returned_inv = make_purchase_invoice(
item_code=item_code, qty=-5, rate=5, update_stock=True, is_return=True
)
# override the rate with valuation rate # override the rate with valuation rate
sle = frappe.get_all( sle = frappe.get_all(
@ -1795,7 +1800,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
)[0] )[0]
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty) rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
self.assertAlmostEqual(returned_inv.items[0].rate, rate) self.assertAlmostEqual(rate, 500)
def test_payment_allocation_for_payment_terms(self): def test_payment_allocation_for_payment_terms(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import ( from erpnext.buying.doctype.purchase_order.test_purchase_order import (
@ -1898,6 +1903,12 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
disable_dimension() disable_dimension()
def test_repost_accounting_entries(self): def test_repost_accounting_entries(self):
# update repost settings
settings = frappe.get_doc("Repost Accounting Ledger Settings")
if not [x for x in settings.allowed_types if x.document_type == "Purchase Invoice"]:
settings.append("allowed_types", {"document_type": "Purchase Invoice", "allowed": True})
settings.save()
pi = make_purchase_invoice( pi = make_purchase_invoice(
rate=1000, rate=1000,
price_list_rate=1000, price_list_rate=1000,

View File

@ -158,6 +158,7 @@
"width": "300px" "width": "300px"
}, },
{ {
"fetch_from": "item_code.image",
"fieldname": "image", "fieldname": "image",
"fieldtype": "Attach", "fieldtype": "Attach",
"hidden": 1, "hidden": 1,
@ -497,6 +498,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"allow_on_submit": 1,
"fieldname": "project", "fieldname": "project",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Project", "label": "Project",
@ -504,6 +506,7 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"allow_on_submit": 1,
"default": ":Company", "default": ":Company",
"depends_on": "eval:!doc.is_fixed_asset", "depends_on": "eval:!doc.is_fixed_asset",
"fieldname": "cost_center", "fieldname": "cost_center",
@ -915,7 +918,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-10-03 21:01:01.824892", "modified": "2023-11-14 18:33:48.547297",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@ -5,9 +5,7 @@ frappe.ui.form.on("Repost Accounting Ledger", {
setup: function(frm) { setup: function(frm) {
frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) { frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) {
return { return {
filters: { query: "erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.get_repost_allowed_types"
name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']],
}
} }
} }

View File

@ -10,9 +10,7 @@ from frappe.utils.data import comma_and
class RepostAccountingLedger(Document): class RepostAccountingLedger(Document):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(RepostAccountingLedger, self).__init__(*args, **kwargs) super(RepostAccountingLedger, self).__init__(*args, **kwargs)
self._allowed_types = set( self._allowed_types = get_allowed_types_from_settings()
["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"]
)
def validate(self): def validate(self):
self.validate_vouchers() self.validate_vouchers()
@ -53,15 +51,7 @@ class RepostAccountingLedger(Document):
def validate_vouchers(self): def validate_vouchers(self):
if self.vouchers: if self.vouchers:
# Validate voucher types validate_docs_for_voucher_types([x.voucher_type for x in self.vouchers])
voucher_types = set([x.voucher_type for x in self.vouchers])
if disallowed_types := voucher_types.difference(self._allowed_types):
frappe.throw(
_("{0} types are not allowed. Only {1} are.").format(
frappe.bold(comma_and(list(disallowed_types))),
frappe.bold(comma_and(list(self._allowed_types))),
)
)
def get_existing_ledger_entries(self): def get_existing_ledger_entries(self):
vouchers = [x.voucher_no for x in self.vouchers] vouchers = [x.voucher_no for x in self.vouchers]
@ -157,7 +147,7 @@ def start_repost(account_repost_doc=str) -> None:
doc.docstatus = 1 doc.docstatus = 1
doc.make_gl_entries() doc.make_gl_entries()
elif doc.doctype in ["Payment Entry", "Journal Entry"]: 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)
doc.make_gl_entries() doc.make_gl_entries()
@ -165,6 +155,15 @@ def start_repost(account_repost_doc=str) -> None:
frappe.db.commit() frappe.db.commit()
def get_allowed_types_from_settings():
return [
x.document_type
for x in frappe.db.get_all(
"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"]
)
]
def validate_docs_for_deferred_accounting(sales_docs, purchase_docs): def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
docs_with_deferred_revenue = frappe.db.get_all( docs_with_deferred_revenue = frappe.db.get_all(
"Sales Invoice Item", "Sales Invoice Item",
@ -186,3 +185,37 @@ def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
frappe.bold(comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue])) frappe.bold(comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue]))
) )
) )
def validate_docs_for_voucher_types(doc_voucher_types):
allowed_types = get_allowed_types_from_settings()
# Validate voucher types
voucher_types = set(doc_voucher_types)
if disallowed_types := voucher_types.difference(allowed_types):
message = "are" if len(disallowed_types) > 1 else "is"
frappe.throw(
_("{0} {1} not allowed to be reposted. Modify {2} to enable reposting.").format(
frappe.bold(comma_and(list(disallowed_types))),
message,
frappe.bold(
frappe.utils.get_link_to_form(
"Repost Accounting Ledger Settings", "Repost Accounting Ledger Settings"
)
),
)
)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters):
filters = {"allowed": True}
if txt:
filters.update({"document_type": ("like", f"%{txt}%")})
if allowed_types := frappe.db.get_all(
"Repost Allowed Types", filters=filters, fields=["distinct(document_type)"], as_list=1
):
return allowed_types
return []

View File

@ -20,10 +20,18 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
self.create_company() self.create_company()
self.create_customer() self.create_customer()
self.create_item() self.create_item()
self.update_repost_settings()
def teadDown(self): def teadDown(self):
frappe.db.rollback() frappe.db.rollback()
def update_repost_settings(self):
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
for x in allowed_types:
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
repost_settings.save()
def test_01_basic_functions(self): def test_01_basic_functions(self):
si = create_sales_invoice( si = create_sales_invoice(
item=self.item, item=self.item,

View File

@ -0,0 +1,8 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Repost Accounting Ledger Settings", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,46 @@
{
"actions": [],
"creation": "2023-11-07 09:57:20.619939",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"allowed_types"
],
"fields": [
{
"fieldname": "allowed_types",
"fieldtype": "Table",
"label": "Allowed Doctypes",
"options": "Repost Allowed Types"
}
],
"in_create": 1,
"issingle": 1,
"links": [],
"modified": "2023-11-07 14:24:13.321522",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Accounting Ledger Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Administrator",
"share": 1,
"write": 1
},
{
"read": 1,
"role": "System Manager",
"select": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class RepostAccountingLedgerSettings(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestRepostAccountingLedgerSettings(FrappeTestCase):
pass

View File

@ -0,0 +1,45 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2023-11-07 09:58:03.595382",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"column_break_sfzb",
"allowed"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Doctype",
"options": "DocType"
},
{
"default": "0",
"fieldname": "allowed",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Allowed"
},
{
"fieldname": "column_break_sfzb",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-11-07 10:01:39.217861",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Allowed Types",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class RepostAllowedTypes(Document):
pass

View File

@ -37,7 +37,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
super.onload(); super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payments", "Unreconcile Payment Entries"]; 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format // show debit_to in print format
@ -184,10 +184,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
} }
} }
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm); erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
} }
make_maintenance_schedule() { make_maintenance_schedule() {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule", method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",
@ -563,15 +562,6 @@ cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) {
} }
} }
// Income Account in Details Table
// --------------------------------
cur_frm.set_query("income_account", "items", function(doc) {
return{
query: "erpnext.controllers.queries.get_income_account",
filters: {'company': doc.company}
}
});
// Cost Center in Details Table // Cost Center in Details Table
// ----------------------------- // -----------------------------
cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function(doc) { cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function(doc) {
@ -666,6 +656,16 @@ frappe.ui.form.on('Sales Invoice', {
}; };
}); });
frm.set_query("income_account", "items", function() {
return{
query: "erpnext.controllers.queries.get_income_account",
filters: {
'company': frm.doc.company,
"disabled": 0
}
}
});
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Delivery Note': 'Delivery', 'Delivery Note': 'Delivery',
'Sales Invoice': 'Return / Credit Note', 'Sales Invoice': 'Return / Credit Note',

View File

@ -1615,7 +1615,8 @@
"hide_seconds": 1, "hide_seconds": 1,
"label": "Inter Company Invoice Reference", "label": "Inter Company Invoice Reference",
"options": "Purchase Invoice", "options": "Purchase Invoice",
"read_only": 1 "read_only": 1,
"search_index": 1
}, },
{ {
"fieldname": "customer_group", "fieldname": "customer_group",
@ -2156,7 +2157,7 @@
"label": "Use Company default Cost Center for Round off" "label": "Use Company default Cost Center for Round off"
}, },
{ {
"default": "0", "default": "1",
"depends_on": "eval: doc.is_return", "depends_on": "eval: doc.is_return",
"fieldname": "update_billed_amount_in_delivery_note", "fieldname": "update_billed_amount_in_delivery_note",
"fieldtype": "Check", "fieldtype": "Check",
@ -2173,7 +2174,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2023-11-03 14:39:38.012346", "modified": "2023-11-23 16:56:29.679499",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -17,6 +17,7 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
) )
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import ( from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
validate_docs_for_deferred_accounting, validate_docs_for_deferred_accounting,
validate_docs_for_voucher_types,
) )
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details, get_party_tax_withholding_details,
@ -172,6 +173,7 @@ class SalesInvoice(SellingController):
self.validate_write_off_account() self.validate_write_off_account()
self.validate_account_for_change_amount() self.validate_account_for_change_amount()
self.validate_income_account() self.validate_income_account()
validate_docs_for_voucher_types(["Sales Invoice"])
validate_docs_for_deferred_accounting([self.name], []) validate_docs_for_deferred_accounting([self.name], [])
def validate_fixed_asset(self): def validate_fixed_asset(self):
@ -395,7 +397,7 @@ class SalesInvoice(SellingController):
"Repost Payment Ledger Items", "Repost Payment Ledger Items",
"Repost Accounting Ledger", "Repost Accounting Ledger",
"Repost Accounting Ledger Items", "Repost Accounting Ledger Items",
"Unreconcile Payments", "Unreconcile Payment",
"Unreconcile Payment Entries", "Unreconcile Payment Entries",
"Payment Ledger Entry", "Payment Ledger Entry",
"Serial and Batch Bundle", "Serial and Batch Bundle",

View File

@ -516,72 +516,70 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(si.grand_total, 5474.0) self.assertEqual(si.grand_total, 5474.0)
def test_tax_calculation_with_item_tax_template(self): def test_tax_calculation_with_item_tax_template(self):
import json
from erpnext.stock.get_item_details import get_item_details
# set tax template in item
item = frappe.get_cached_doc("Item", "_Test Item")
item.set(
"taxes",
[
{
"item_tax_template": "_Test Item Tax Template 1 - _TC",
"valid_from": add_days(nowdate(), -5),
}
],
)
item.save()
# create sales invoice with item
si = create_sales_invoice(qty=84, rate=4.6, do_not_save=True) si = create_sales_invoice(qty=84, rate=4.6, do_not_save=True)
item_details = get_item_details( item_row = si.get("items")[0]
doc=si,
args={ add_items = [
"item_code": item.item_code, (54, "_Test Account Excise Duty @ 12 - _TC"),
"company": si.company, (288, "_Test Account Excise Duty @ 15 - _TC"),
"doctype": "Sales Invoice", (144, "_Test Account Excise Duty @ 20 - _TC"),
"conversion_rate": 1.0, (430, "_Test Item Tax Template 1 - _TC"),
]
for qty, item_tax_template in add_items:
item_row_copy = copy.deepcopy(item_row)
item_row_copy.qty = qty
item_row_copy.item_tax_template = item_tax_template
si.append("items", item_row_copy)
si.append(
"taxes",
{
"account_head": "_Test Account Excise Duty - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "Excise Duty",
"doctype": "Sales Taxes and Charges",
"rate": 11,
}, },
) )
tax_map = json.loads(item_details.item_tax_rate) si.append(
for tax in tax_map: "taxes",
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": tax,
"rate": tax_map[tax],
"description": "Test",
"cost_center": "_Test Cost Center - _TC",
},
)
si.submit()
si.load_from_db()
# check if correct tax values are applied from tax template
self.assertEqual(si.net_total, 386.4)
expected_taxes = [
{ {
"tax_amount": 19.32, "account_head": "_Test Account Education Cess - _TC",
"total": 405.72, "charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "Education Cess",
"doctype": "Sales Taxes and Charges",
"rate": 0,
}, },
)
si.append(
"taxes",
{ {
"tax_amount": 38.64, "account_head": "_Test Account S&H Education Cess - _TC",
"total": 444.36, "charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "S&H Education Cess",
"doctype": "Sales Taxes and Charges",
"rate": 3,
}, },
{ )
"tax_amount": 57.96, si.insert()
"total": 502.32,
},
]
for i in range(len(expected_taxes)): self.assertEqual(si.net_total, 4600)
for key in expected_taxes[i]:
self.assertEqual(expected_taxes[i][key], si.get("taxes")[i].get(key))
self.assertEqual(si.get("base_total_taxes_and_charges"), 115.92) self.assertEqual(si.get("taxes")[0].tax_amount, 502.41)
self.assertEqual(si.get("taxes")[0].total, 5102.41)
self.assertEqual(si.get("taxes")[1].tax_amount, 197.80)
self.assertEqual(si.get("taxes")[1].total, 5300.21)
self.assertEqual(si.get("taxes")[2].tax_amount, 375.36)
self.assertEqual(si.get("taxes")[2].total, 5675.57)
self.assertEqual(si.grand_total, 5675.57)
self.assertEqual(si.rounding_adjustment, 0.43)
self.assertEqual(si.rounded_total, 5676.0)
def test_tax_calculation_with_multiple_items_and_discount(self): def test_tax_calculation_with_multiple_items_and_discount(self):
si = create_sales_invoice(qty=1, rate=75, do_not_save=True) si = create_sales_invoice(qty=1, rate=75, do_not_save=True)
@ -791,6 +789,28 @@ class TestSalesInvoice(FrappeTestCase):
w = self.make() w = self.make()
self.assertEqual(w.outstanding_amount, w.base_rounded_total) self.assertEqual(w.outstanding_amount, w.base_rounded_total)
def test_rounded_total_with_cash_discount(self):
si = frappe.copy_doc(test_records[2])
item = copy.deepcopy(si.get("items")[0])
item.update(
{
"qty": 1,
"rate": 14960.66,
}
)
si.set("items", [item])
si.set("taxes", [])
si.apply_discount_on = "Grand Total"
si.is_cash_or_non_trade_discount = 1
si.discount_amount = 1
si.insert()
self.assertEqual(si.grand_total, 14959.66)
self.assertEqual(si.rounded_total, 14960)
self.assertEqual(si.rounding_adjustment, 0.34)
def test_payment(self): def test_payment(self):
w = self.make() w = self.make()

View File

@ -167,6 +167,7 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"fetch_from": "item_code.image",
"fieldname": "image", "fieldname": "image",
"fieldtype": "Attach", "fieldtype": "Attach",
"hidden": 1, "hidden": 1,
@ -901,7 +902,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-07-26 12:53:22.404057", "modified": "2023-11-14 18:34:10.479329",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@ -10,7 +10,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_customer() self.create_customer()
@ -73,7 +73,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
unreconcile = frappe.get_doc( unreconcile = frappe.get_doc(
{ {
"doctype": "Unreconcile Payments", "doctype": "Unreconcile Payment",
"company": self.company, "company": self.company,
"voucher_type": pe.doctype, "voucher_type": pe.doctype,
"voucher_no": pe.name, "voucher_no": pe.name,
@ -138,7 +138,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
unreconcile = frappe.get_doc( unreconcile = frappe.get_doc(
{ {
"doctype": "Unreconcile Payments", "doctype": "Unreconcile Payment",
"company": self.company, "company": self.company,
"voucher_type": pe2.doctype, "voucher_type": pe2.doctype,
"voucher_no": pe2.name, "voucher_no": pe2.name,
@ -196,7 +196,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
unreconcile = frappe.get_doc( unreconcile = frappe.get_doc(
{ {
"doctype": "Unreconcile Payments", "doctype": "Unreconcile Payment",
"company": self.company, "company": self.company,
"voucher_type": pe.doctype, "voucher_type": pe.doctype,
"voucher_no": pe.name, "voucher_no": pe.name,
@ -281,7 +281,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
unreconcile = frappe.get_doc( unreconcile = frappe.get_doc(
{ {
"doctype": "Unreconcile Payments", "doctype": "Unreconcile Payment",
"company": self.company, "company": self.company,
"voucher_type": pe2.doctype, "voucher_type": pe2.doctype,
"voucher_no": pe2.name, "voucher_no": pe2.name,

View File

@ -1,7 +1,7 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Unreconcile Payments", { frappe.ui.form.on("Unreconcile Payment", {
refresh(frm) { refresh(frm) {
frm.set_query("voucher_type", function() { frm.set_query("voucher_type", function() {
return { return {

View File

@ -21,7 +21,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Amended From", "label": "Amended From",
"no_copy": 1, "no_copy": 1,
"options": "Unreconcile Payments", "options": "Unreconcile Payment",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@ -61,7 +61,7 @@
"modified": "2023-08-28 17:42:50.261377", "modified": "2023-08-28 17:42:50.261377",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Unreconcile Payments", "name": "Unreconcile Payment",
"naming_rule": "Expression", "naming_rule": "Expression",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [

View File

@ -15,7 +15,7 @@ from erpnext.accounts.utils import (
) )
class UnreconcilePayments(Document): class UnreconcilePayment(Document):
def validate(self): def validate(self):
self.supported_types = ["Payment Entry", "Journal Entry"] self.supported_types = ["Payment Entry", "Journal Entry"]
if not self.voucher_type in self.supported_types: if not self.voucher_type in self.supported_types:
@ -142,7 +142,7 @@ def create_unreconcile_doc_for_selection(selections=None):
selections = frappe.json.loads(selections) selections = frappe.json.loads(selections)
# assuming each row is a unique voucher # assuming each row is a unique voucher
for row in selections: for row in selections:
unrecon = frappe.new_doc("Unreconcile Payments") unrecon = frappe.new_doc("Unreconcile Payment")
unrecon.company = row.get("company") unrecon.company = row.get("company")
unrecon.voucher_type = row.get("voucher_type") unrecon.voucher_type = row.get("voucher_type")
unrecon.voucher_no = row.get("voucher_no") unrecon.voucher_no = row.get("voucher_no")

View File

@ -31,7 +31,12 @@ from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
from erpnext.utilities.regional import temporary_flag from erpnext.utilities.regional import temporary_flag
PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"} PURCHASE_TRANSACTION_TYPES = {
"Supplier Quotation",
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice",
}
SALES_TRANSACTION_TYPES = { SALES_TRANSACTION_TYPES = {
"Quotation", "Quotation",
"Sales Order", "Sales Order",
@ -231,7 +236,9 @@ def set_address_details(
if shipping_address: if shipping_address:
party_details.update( party_details.update(
shipping_address=shipping_address, shipping_address=shipping_address,
shipping_address_display=render_address(shipping_address), shipping_address_display=render_address(
shipping_address, check_permissions=not ignore_permissions
),
**get_fetch_values(doctype, "shipping_address", shipping_address) **get_fetch_values(doctype, "shipping_address", shipping_address)
) )

View File

@ -144,6 +144,16 @@ frappe.query_reports["Accounts Payable"] = {
"label": __("Show Future Payments"), "label": __("Show Future Payments"),
"fieldtype": "Check", "fieldtype": "Check",
}, },
{
"fieldname": "in_party_currency",
"label": __("In Party Currency"),
"fieldtype": "Check",
},
{
"fieldname": "for_revaluation_journals",
"label": __("Revaluation Journals"),
"fieldtype": "Check",
},
{ {
"fieldname": "ignore_accounts", "fieldname": "ignore_accounts",
"label": __("Group by Voucher"), "label": __("Group by Voucher"),

View File

@ -40,6 +40,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"range2": 60, "range2": 60,
"range3": 90, "range3": 90,
"range4": 120, "range4": 120,
"in_party_currency": 1,
} }
data = execute(filters) data = execute(filters)

View File

@ -110,6 +110,11 @@ frappe.query_reports["Accounts Payable Summary"] = {
"fieldname":"based_on_payment_terms", "fieldname":"based_on_payment_terms",
"label": __("Based On Payment Terms"), "label": __("Based On Payment Terms"),
"fieldtype": "Check", "fieldtype": "Check",
},
{
"fieldname": "for_revaluation_journals",
"label": __("Revaluation Journals"),
"fieldtype": "Check",
} }
], ],

View File

@ -114,10 +114,13 @@ frappe.query_reports["Accounts Receivable"] = {
"reqd": 1 "reqd": 1
}, },
{ {
"fieldname": "customer_group", "fieldname":"customer_group",
"label": __("Customer Group"), "label": __("Customer Group"),
"fieldtype": "Link", "fieldtype": "MultiSelectList",
"options": "Customer Group" "options": "Customer Group",
get_data: function(txt) {
return frappe.db.get_link_options('Customer Group', txt);
}
}, },
{ {
"fieldname": "payment_terms_template", "fieldname": "payment_terms_template",
@ -173,12 +176,23 @@ frappe.query_reports["Accounts Receivable"] = {
"label": __("Show Remarks"), "label": __("Show Remarks"),
"fieldtype": "Check", "fieldtype": "Check",
}, },
{
"fieldname": "in_party_currency",
"label": __("In Party Currency"),
"fieldtype": "Check",
},
{
"fieldname": "for_revaluation_journals",
"label": __("Revaluation Journals"),
"fieldtype": "Check",
},
{ {
"fieldname": "ignore_accounts", "fieldname": "ignore_accounts",
"label": __("Group by Voucher"), "label": __("Group by Voucher"),
"fieldtype": "Check", "fieldtype": "Check",
} }
], ],
"formatter": function(value, row, column, data, default_formatter) { "formatter": function(value, row, column, data, default_formatter) {

View File

@ -7,14 +7,14 @@ from collections import OrderedDict
import frappe import frappe
from frappe import _, qb, scrub from frappe import _, qb, scrub
from frappe.query_builder import Criterion from frappe.query_builder import Criterion
from frappe.query_builder.functions import Date, Sum from frappe.query_builder.functions import Date, Substring, Sum
from frappe.utils import cint, cstr, flt, getdate, nowdate from frappe.utils import cint, cstr, flt, getdate, nowdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
get_dimension_with_children, get_dimension_with_children,
) )
from erpnext.accounts.utils import get_currency_precision from erpnext.accounts.utils import get_currency_precision, get_party_types_from_account_type
# This report gives a summary of all Outstanding Invoices considering the following # This report gives a summary of all Outstanding Invoices considering the following
@ -28,8 +28,8 @@ from erpnext.accounts.utils import get_currency_precision
# 6. Configurable Ageing Groups (0-30, 30-60 etc) can be set via filters # 6. Configurable Ageing Groups (0-30, 30-60 etc) can be set via filters
# 7. For overpayment against an invoice with payment terms, there will be an additional row # 7. For overpayment against an invoice with payment terms, there will be an additional row
# 8. Invoice details like Sales Persons, Delivery Notes are also fetched comma separated # 8. Invoice details like Sales Persons, Delivery Notes are also fetched comma separated
# 9. Report amounts are in "Party Currency" if party is selected, or company currency for multi-party # 9. Report amounts are in party currency if in_party_currency is selected, otherwise company currency
# 10. This reports is based on all GL Entries that are made against account_type "Receivable" or "Payable" # 10. This report is based on Payment Ledger Entries
def execute(filters=None): def execute(filters=None):
@ -72,9 +72,7 @@ class ReceivablePayableReport(object):
self.currency_precision = get_currency_precision() or 2 self.currency_precision = get_currency_precision() or 2
self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit" self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit"
self.account_type = self.filters.account_type self.account_type = self.filters.account_type
self.party_type = frappe.db.get_all( self.party_type = get_party_types_from_account_type(self.account_type)
"Party Type", {"account_type": self.account_type}, pluck="name"
)
self.party_details = {} self.party_details = {}
self.invoices = set() self.invoices = set()
self.skip_total_row = 0 self.skip_total_row = 0
@ -84,6 +82,9 @@ class ReceivablePayableReport(object):
self.total_row_map = {} self.total_row_map = {}
self.skip_total_row = 1 self.skip_total_row = 1
if self.filters.get("in_party_currency"):
self.skip_total_row = 1
def get_data(self): def get_data(self):
self.get_ple_entries() self.get_ple_entries()
self.get_sales_invoices_or_customers_based_on_sales_person() self.get_sales_invoices_or_customers_based_on_sales_person()
@ -117,7 +118,7 @@ class ReceivablePayableReport(object):
for ple in self.ple_entries: for ple in self.ple_entries:
# get the balance object for voucher_type # get the balance object for voucher_type
if self.filters.get("ingore_accounts"): if self.filters.get("ignore_accounts"):
key = (ple.voucher_type, ple.voucher_no, ple.party) key = (ple.voucher_type, ple.voucher_no, ple.party)
else: else:
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party) key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
@ -145,7 +146,7 @@ class ReceivablePayableReport(object):
if self.filters.get("group_by_party"): if self.filters.get("group_by_party"):
self.init_subtotal_row(ple.party) self.init_subtotal_row(ple.party)
if self.filters.get("group_by_party"): if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"):
self.init_subtotal_row("Total") self.init_subtotal_row("Total")
def get_invoices(self, ple): def get_invoices(self, ple):
@ -188,7 +189,7 @@ class ReceivablePayableReport(object):
): ):
return return
if self.filters.get("ingore_accounts"): if self.filters.get("ignore_accounts"):
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
else: else:
key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party) key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)
@ -200,7 +201,7 @@ class ReceivablePayableReport(object):
if ple.against_voucher_no in self.return_entries: if ple.against_voucher_no in self.return_entries:
return_against = self.return_entries.get(ple.against_voucher_no) return_against = self.return_entries.get(ple.against_voucher_no)
if return_against: if return_against:
if self.filters.get("ingore_accounts"): if self.filters.get("ignore_accounts"):
key = (ple.against_voucher_type, return_against, ple.party) key = (ple.against_voucher_type, return_against, ple.party)
else: else:
key = (ple.account, ple.against_voucher_type, return_against, ple.party) key = (ple.account, ple.against_voucher_type, return_against, ple.party)
@ -209,7 +210,7 @@ class ReceivablePayableReport(object):
if not row: if not row:
# no invoice, this is an invoice / stand-alone payment / credit note # no invoice, this is an invoice / stand-alone payment / credit note
if self.filters.get("ingore_accounts"): if self.filters.get("ignore_accounts"):
row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party)) row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
else: else:
row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party)) row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party))
@ -224,8 +225,7 @@ class ReceivablePayableReport(object):
if not row: if not row:
return return
# amount in "Party Currency", if its supplied. If not, amount in company currency if self.filters.get("in_party_currency"):
if self.filters.get("party_type") and self.filters.get("party"):
amount = ple.amount_in_account_currency amount = ple.amount_in_account_currency
else: else:
amount = ple.amount amount = ple.amount
@ -256,8 +256,10 @@ class ReceivablePayableReport(object):
def update_sub_total_row(self, row, party): def update_sub_total_row(self, row, party):
total_row = self.total_row_map.get(party) total_row = self.total_row_map.get(party)
for field in self.get_currency_fields(): if total_row:
total_row[field] += row.get(field, 0.0) for field in self.get_currency_fields():
total_row[field] += row.get(field, 0.0)
total_row["currency"] = row.get("currency", "")
def append_subtotal_row(self, party): def append_subtotal_row(self, party):
sub_total_row = self.total_row_map.get(party) sub_total_row = self.total_row_map.get(party)
@ -281,11 +283,20 @@ class ReceivablePayableReport(object):
row.invoice_grand_total = row.invoiced row.invoice_grand_total = row.invoiced
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and ( must_consider = False
(abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision) if self.filters.get("for_revaluation_journals"):
or (row.voucher_no in self.err_journals) if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) or (
): (abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
):
must_consider = True
else:
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
(abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
or (row.voucher_no in self.err_journals)
):
must_consider = True
if must_consider:
# non-zero oustanding, we must consider this row # non-zero oustanding, we must consider this row
if self.is_invoice(row) and self.filters.based_on_payment_terms: if self.is_invoice(row) and self.filters.based_on_payment_terms:
@ -309,7 +320,7 @@ class ReceivablePayableReport(object):
if self.filters.get("group_by_party"): if self.filters.get("group_by_party"):
self.append_subtotal_row(self.previous_party) self.append_subtotal_row(self.previous_party)
if self.data: if self.data:
self.data.append(self.total_row_map.get("Total")) self.data.append(self.total_row_map.get("Total", {}))
def append_row(self, row): def append_row(self, row):
self.allocate_future_payments(row) self.allocate_future_payments(row)
@ -440,7 +451,7 @@ class ReceivablePayableReport(object):
party_details = self.get_party_details(row.party) or {} party_details = self.get_party_details(row.party) or {}
row.update(party_details) row.update(party_details)
if self.filters.get("party_type") and self.filters.get("party"): if self.filters.get("in_party_currency"):
row.currency = row.account_currency row.currency = row.account_currency
else: else:
row.currency = self.company_currency row.currency = self.company_currency
@ -753,7 +764,12 @@ class ReceivablePayableReport(object):
) )
if self.filters.get("show_remarks"): if self.filters.get("show_remarks"):
query = query.select(ple.remarks) if remarks_length := frappe.db.get_single_value(
"Accounts Settings", "receivable_payable_remarks_length"
):
query = query.select(Substring(ple.remarks, 1, remarks_length).as_("remarks"))
else:
query = query.select(ple.remarks)
if self.filters.get("group_by_party"): if self.filters.get("group_by_party"):
query = query.orderby(self.ple.party, self.ple.posting_date) query = query.orderby(self.ple.party, self.ple.posting_date)
@ -840,7 +856,13 @@ class ReceivablePayableReport(object):
self.customer = qb.DocType("Customer") self.customer = qb.DocType("Customer")
if self.filters.get("customer_group"): if self.filters.get("customer_group"):
self.get_hierarchical_filters("Customer Group", "customer_group") groups = get_customer_group_with_children(self.filters.customer_group)
customers = (
qb.from_(self.customer)
.select(self.customer.name)
.where(self.customer["customer_group"].isin(groups))
)
self.qb_selection_filter.append(self.ple.party.isin(customers))
if self.filters.get("territory"): if self.filters.get("territory"):
self.get_hierarchical_filters("Territory", "territory") self.get_hierarchical_filters("Territory", "territory")
@ -1132,3 +1154,19 @@ class ReceivablePayableReport(object):
.run() .run()
) )
self.err_journals = [x[0] for x in results] if results else [] self.err_journals = [x[0] for x in results] if results else []
def get_customer_group_with_children(customer_groups):
if not isinstance(customer_groups, list):
customer_groups = [d.strip() for d in customer_groups.strip().split(",") if d]
all_customer_groups = []
for d in customer_groups:
if frappe.db.exists("Customer Group", d):
lft, rgt = frappe.db.get_value("Customer Group", d, ["lft", "rgt"])
children = frappe.get_all("Customer Group", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
all_customer_groups += [c.name for c in children]
else:
frappe.throw(_("Customer Group: {0} does not exist").format(d))
return list(set(all_customer_groups))

View File

@ -475,6 +475,30 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
report = execute(filters)[1] report = execute(filters)[1]
self.assertEqual(len(report), 0) self.assertEqual(len(report), 0)
def test_multi_customer_group_filter(self):
si = self.create_sales_invoice()
cus_group = frappe.db.get_value("Customer", self.customer, "customer_group")
# Create a list of customer groups, e.g., ["Group1", "Group2"]
cus_groups_list = [cus_group, "_Test Customer Group 1"]
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"customer_group": cus_groups_list, # Use the list of customer groups
}
report = execute(filters)[1]
# Assert that the report contains data for the specified customer groups
self.assertTrue(len(report) > 0)
for row in report:
# Assert that the customer group of each row is in the list of customer groups
self.assertIn(row.customer_group, cus_groups_list)
def test_party_account_filter(self): def test_party_account_filter(self):
si1 = self.create_sales_invoice() si1 = self.create_sales_invoice()
self.customer2 = ( self.customer2 = (
@ -557,6 +581,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"range2": 60, "range2": 60,
"range3": 90, "range3": 90,
"range4": 120, "range4": 120,
"in_party_currency": 1,
} }
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)

View File

@ -139,6 +139,11 @@ frappe.query_reports["Accounts Receivable Summary"] = {
"label": __("Show GL Balance"), "label": __("Show GL Balance"),
"fieldtype": "Check", "fieldtype": "Check",
}, },
{
"fieldname": "for_revaluation_journals",
"label": __("Revaluation Journals"),
"fieldtype": "Check",
}
], ],
onload: function(report) { onload: function(report) {

View File

@ -8,6 +8,7 @@ from frappe.utils import cint, flt
from erpnext.accounts.party import get_partywise_advanced_payment_amount from erpnext.accounts.party import get_partywise_advanced_payment_amount
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
from erpnext.accounts.utils import get_party_types_from_account_type
def execute(filters=None): def execute(filters=None):
@ -22,9 +23,7 @@ def execute(filters=None):
class AccountsReceivableSummary(ReceivablePayableReport): class AccountsReceivableSummary(ReceivablePayableReport):
def run(self, args): def run(self, args):
self.account_type = args.get("account_type") self.account_type = args.get("account_type")
self.party_type = frappe.db.get_all( self.party_type = get_party_types_from_account_type(self.account_type)
"Party Type", {"account_type": self.account_type}, pluck="name"
)
self.party_naming_by = frappe.db.get_value( self.party_naming_by = frappe.db.get_value(
args.get("naming_by")[0], None, args.get("naming_by")[1] args.get("naming_by")[0], None, args.get("naming_by")[1]
) )

View File

@ -31,6 +31,18 @@ frappe.query_reports["Asset Depreciation Ledger"] = {
"fieldtype": "Link", "fieldtype": "Link",
"options": "Asset" "options": "Asset"
}, },
{
"fieldname":"asset_category",
"label": __("Asset Category"),
"fieldtype": "Link",
"options": "Asset Category"
},
{
"fieldname":"cost_center",
"label": __("Cost Center"),
"fieldtype": "Link",
"options": "Cost Center"
},
{ {
"fieldname":"finance_book", "fieldname":"finance_book",
"label": __("Finance Book"), "label": __("Finance Book"),
@ -38,10 +50,10 @@ frappe.query_reports["Asset Depreciation Ledger"] = {
"options": "Finance Book" "options": "Finance Book"
}, },
{ {
"fieldname":"asset_category", "fieldname": "include_default_book_assets",
"label": __("Asset Category"), "label": __("Include Default FB Assets"),
"fieldtype": "Link", "fieldtype": "Check",
"options": "Asset Category" "default": 1
} },
] ]
} }

View File

@ -1,15 +1,15 @@
{ {
"add_total_row": 1, "add_total_row": 0,
"columns": [], "columns": [],
"creation": "2016-04-08 14:49:58.133098", "creation": "2016-04-08 14:49:58.133098",
"disabled": 0, "disabled": 0,
"docstatus": 0, "docstatus": 0,
"doctype": "Report", "doctype": "Report",
"filters": [], "filters": [],
"idx": 2, "idx": 6,
"is_standard": "Yes", "is_standard": "Yes",
"letterhead": null, "letterhead": null,
"modified": "2023-07-26 21:05:33.554778", "modified": "2023-11-08 20:17:05.774211",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Asset Depreciation Ledger", "name": "Asset Depreciation Ledger",

View File

@ -4,7 +4,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import cstr, flt
def execute(filters=None): def execute(filters=None):
@ -32,7 +32,6 @@ def get_data(filters):
filters_data.append(["against_voucher", "=", filters.get("asset")]) filters_data.append(["against_voucher", "=", filters.get("asset")])
if filters.get("asset_category"): if filters.get("asset_category"):
assets = frappe.db.sql_list( assets = frappe.db.sql_list(
"""select name from tabAsset """select name from tabAsset
where asset_category = %s and docstatus=1""", where asset_category = %s and docstatus=1""",
@ -41,12 +40,27 @@ def get_data(filters):
filters_data.append(["against_voucher", "in", assets]) filters_data.append(["against_voucher", "in", assets])
if filters.get("finance_book"): company_fb = frappe.get_cached_value("Company", filters.get("company"), "default_finance_book")
filters_data.append(["finance_book", "in", ["", filters.get("finance_book")]])
if filters.get("include_default_book_assets") and company_fb:
if filters.get("finance_book") and cstr(filters.get("finance_book")) != cstr(company_fb):
frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Assets'"))
else:
finance_book = company_fb
elif filters.get("finance_book"):
finance_book = filters.get("finance_book")
else:
finance_book = None
if finance_book:
or_filters_data = [["finance_book", "in", ["", finance_book]], ["finance_book", "is", "not set"]]
else:
or_filters_data = [["finance_book", "in", [""]], ["finance_book", "is", "not set"]]
gl_entries = frappe.get_all( gl_entries = frappe.get_all(
"GL Entry", "GL Entry",
filters=filters_data, filters=filters_data,
or_filters=or_filters_data,
fields=["against_voucher", "debit_in_account_currency as debit", "voucher_no", "posting_date"], fields=["against_voucher", "debit_in_account_currency as debit", "voucher_no", "posting_date"],
order_by="against_voucher, posting_date", order_by="against_voucher, posting_date",
) )
@ -61,7 +75,9 @@ def get_data(filters):
asset_data = assets_details.get(d.against_voucher) asset_data = assets_details.get(d.against_voucher)
if asset_data: if asset_data:
if not asset_data.get("accumulated_depreciation_amount"): if not asset_data.get("accumulated_depreciation_amount"):
asset_data.accumulated_depreciation_amount = d.debit asset_data.accumulated_depreciation_amount = d.debit + asset_data.get(
"opening_accumulated_depreciation"
)
else: else:
asset_data.accumulated_depreciation_amount += d.debit asset_data.accumulated_depreciation_amount += d.debit
@ -70,7 +86,7 @@ def get_data(filters):
{ {
"depreciation_amount": d.debit, "depreciation_amount": d.debit,
"depreciation_date": d.posting_date, "depreciation_date": d.posting_date,
"amount_after_depreciation": ( "value_after_depreciation": (
flt(row.gross_purchase_amount) - flt(row.accumulated_depreciation_amount) flt(row.gross_purchase_amount) - flt(row.accumulated_depreciation_amount)
), ),
"depreciation_entry": d.voucher_no, "depreciation_entry": d.voucher_no,
@ -88,10 +104,12 @@ def get_assets_details(assets):
fields = [ fields = [
"name as asset", "name as asset",
"gross_purchase_amount", "gross_purchase_amount",
"opening_accumulated_depreciation",
"asset_category", "asset_category",
"status", "status",
"depreciation_method", "depreciation_method",
"purchase_date", "purchase_date",
"cost_center",
] ]
for d in frappe.get_all("Asset", fields=fields, filters={"name": ("in", assets)}): for d in frappe.get_all("Asset", fields=fields, filters={"name": ("in", assets)}):
@ -121,6 +139,12 @@ def get_columns():
"fieldtype": "Currency", "fieldtype": "Currency",
"width": 120, "width": 120,
}, },
{
"label": _("Opening Accumulated Depreciation"),
"fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency",
"width": 140,
},
{ {
"label": _("Depreciation Amount"), "label": _("Depreciation Amount"),
"fieldname": "depreciation_amount", "fieldname": "depreciation_amount",
@ -134,8 +158,8 @@ def get_columns():
"width": 210, "width": 210,
}, },
{ {
"label": _("Amount After Depreciation"), "label": _("Value After Depreciation"),
"fieldname": "amount_after_depreciation", "fieldname": "value_after_depreciation",
"fieldtype": "Currency", "fieldtype": "Currency",
"width": 180, "width": 180,
}, },
@ -153,12 +177,13 @@ def get_columns():
"options": "Asset Category", "options": "Asset Category",
"width": 120, "width": 120,
}, },
{"label": _("Current Status"), "fieldname": "status", "fieldtype": "Data", "width": 120},
{ {
"label": _("Depreciation Method"), "label": _("Cost Center"),
"fieldname": "depreciation_method", "fieldtype": "Link",
"fieldtype": "Data", "fieldname": "cost_center",
"width": 130, "options": "Cost Center",
"width": 100,
}, },
{"label": _("Current Status"), "fieldname": "status", "fieldtype": "Data", "width": 120},
{"label": _("Purchase Date"), "fieldname": "purchase_date", "fieldtype": "Date", "width": 120}, {"label": _("Purchase Date"), "fieldname": "purchase_date", "fieldtype": "Date", "width": 120},
] ]

View File

@ -17,7 +17,7 @@ frappe.query_reports["Balance Sheet"]["filters"].push({
frappe.query_reports["Balance Sheet"]["filters"].push({ frappe.query_reports["Balance Sheet"]["filters"].push({
fieldname: "include_default_book_entries", fieldname: "include_default_book_entries",
label: __("Include Default Book Entries"), label: __("Include Default FB Entries"),
fieldtype: "Check", fieldtype: "Check",
default: 1, default: 1,
}); });

View File

@ -17,7 +17,7 @@ frappe.query_reports["Cash Flow"]["filters"].splice(8, 1);
frappe.query_reports["Cash Flow"]["filters"].push( frappe.query_reports["Cash Flow"]["filters"].push(
{ {
"fieldname": "include_default_book_entries", "fieldname": "include_default_book_entries",
"label": __("Include Default Book Entries"), "label": __("Include Default FB Entries"),
"fieldtype": "Check", "fieldtype": "Check",
"default": 1 "default": 1
} }

View File

@ -104,7 +104,7 @@ frappe.query_reports["Consolidated Financial Statement"] = {
}, },
{ {
"fieldname": "include_default_book_entries", "fieldname": "include_default_book_entries",
"label": __("Include Default Book Entries"), "label": __("Include Default FB Entries"),
"fieldtype": "Check", "fieldtype": "Check",
"default": 1 "default": 1
}, },

View File

@ -561,9 +561,7 @@ def apply_additional_conditions(doctype, query, from_date, ignore_closing_entrie
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book") company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb): if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
frappe.throw( frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'"))
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
)
query = query.where( query = query.where(
(gl_entry.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""])) (gl_entry.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))

View File

@ -175,7 +175,7 @@ frappe.query_reports["General Ledger"] = {
}, },
{ {
"fieldname": "include_default_book_entries", "fieldname": "include_default_book_entries",
"label": __("Include Default Book Entries"), "label": __("Include Default FB Entries"),
"fieldtype": "Check", "fieldtype": "Check",
"default": 1 "default": 1
}, },

View File

@ -164,7 +164,12 @@ def get_gl_entries(filters, accounting_dimensions):
credit_in_account_currency """ credit_in_account_currency """
if filters.get("show_remarks"): if filters.get("show_remarks"):
select_fields += """,remarks""" if remarks_length := frappe.db.get_single_value(
"Accounts Settings", "general_ledger_remarks_length"
):
select_fields += f",substr(remarks, 1, {remarks_length}) as 'remarks'"
else:
select_fields += """,remarks"""
order_by_statement = "order by posting_date, account, creation" order_by_statement = "order by posting_date, account, creation"
@ -259,9 +264,7 @@ def get_conditions(filters):
if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr( if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr(
filters.get("company_fb") filters.get("company_fb")
): ):
frappe.throw( frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'"))
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
)
else: else:
conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)") conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
else: else:

View File

@ -184,6 +184,16 @@ def get_columns(filters):
"width": 180, "width": 180,
} }
) )
else:
columns.append(
{
"label": _(filters.get("party_type")),
"fieldname": "party",
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 180,
}
)
columns.extend( columns.extend(
[ [
@ -316,7 +326,7 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
if not tds_accounts: if not tds_accounts:
frappe.throw( frappe.throw(
_("No {0} Accounts found for this company.").format(frappe.bold("Tax Withholding")), _("No {0} Accounts found for this company.").format(frappe.bold("Tax Withholding")),
title="Accounts Missing Error", title=_("Accounts Missing Error"),
) )
gle = frappe.qb.DocType("GL Entry") gle = frappe.qb.DocType("GL Entry")
query = ( query = (

View File

@ -95,7 +95,7 @@ frappe.query_reports["Trial Balance"] = {
}, },
{ {
"fieldname": "include_default_book_entries", "fieldname": "include_default_book_entries",
"label": __("Include Default Book Entries"), "label": __("Include Default FB Entries"),
"fieldtype": "Check", "fieldtype": "Check",
"default": 1 "default": 1
}, },

View File

@ -275,9 +275,7 @@ def get_opening_balance(
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book") company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb): if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
frappe.throw( frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'"))
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
)
opening_balance = opening_balance.where( opening_balance = opening_balance.where(
(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""])) (closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))

View File

@ -53,6 +53,9 @@ GL_REPOSTING_CHUNK = 100
def get_fiscal_year( def get_fiscal_year(
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False
): ):
if isinstance(boolean, str):
boolean = frappe.json.loads(boolean)
fiscal_years = get_fiscal_years( fiscal_years = get_fiscal_years(
date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean
) )
@ -180,6 +183,7 @@ def get_balance_on(
cost_center=None, cost_center=None,
ignore_account_permission=False, ignore_account_permission=False,
account_type=None, account_type=None,
start_date=None,
): ):
if not account and frappe.form_dict.get("account"): if not account and frappe.form_dict.get("account"):
account = frappe.form_dict.get("account") account = frappe.form_dict.get("account")
@ -193,6 +197,8 @@ def get_balance_on(
cost_center = frappe.form_dict.get("cost_center") cost_center = frappe.form_dict.get("cost_center")
cond = ["is_cancelled=0"] cond = ["is_cancelled=0"]
if start_date:
cond.append("posting_date >= %s" % frappe.db.escape(cstr(start_date)))
if date: if date:
cond.append("posting_date <= %s" % frappe.db.escape(cstr(date))) cond.append("posting_date <= %s" % frappe.db.escape(cstr(date)))
else: else:
@ -1831,6 +1837,28 @@ class QueryPaymentLedger(object):
Table("outstanding").amount_in_account_currency >= self.max_outstanding Table("outstanding").amount_in_account_currency >= self.max_outstanding
) )
if self.limit and self.get_invoices:
outstanding_vouchers = (
qb.from_(ple)
.select(
ple.against_voucher_no.as_("voucher_no"),
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
)
.where(ple.delinked == 0)
.where(Criterion.all(filter_on_against_voucher_no))
.where(Criterion.all(self.common_filter))
.groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party)
.orderby(ple.posting_date, ple.voucher_no)
.having(qb.Field("amount_in_account_currency") > 0)
.limit(self.limit)
.run()
)
if outstanding_vouchers:
filter_on_voucher_no.append(ple.voucher_no.isin([x[0] for x in outstanding_vouchers]))
filter_on_against_voucher_no.append(
ple.against_voucher_no.isin([x[0] for x in outstanding_vouchers])
)
# build query for voucher amount # build query for voucher amount
query_voucher_amount = ( query_voucher_amount = (
qb.from_(ple) qb.from_(ple)
@ -2047,3 +2075,7 @@ def create_gain_loss_journal(
journal_entry.save() journal_entry.save()
journal_entry.submit() journal_entry.submit()
return journal_entry.name return journal_entry.name
def get_party_types_from_account_type(account_type):
return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name")

View File

@ -481,11 +481,11 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval.doc.asset_quantity", "default": "1",
"fieldname": "asset_quantity", "fieldname": "asset_quantity",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Asset Quantity", "label": "Asset Quantity",
"read_only": 1 "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
}, },
{ {
"fieldname": "depr_entry_posting_status", "fieldname": "depr_entry_posting_status",
@ -572,7 +572,7 @@
"link_fieldname": "target_asset" "link_fieldname": "target_asset"
} }
], ],
"modified": "2023-10-27 17:03:46.629617", "modified": "2023-11-20 20:57:37.010467",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@ -46,12 +46,28 @@ class Asset(AccountsController):
self.validate_item() self.validate_item()
self.validate_cost_center() self.validate_cost_center()
self.set_missing_values() self.set_missing_values()
self.validate_finance_books()
if not self.split_from:
self.prepare_depreciation_data()
update_draft_asset_depr_schedules(self)
self.validate_gross_and_purchase_amount() self.validate_gross_and_purchase_amount()
self.validate_expected_value_after_useful_life() self.validate_expected_value_after_useful_life()
self.validate_finance_books()
if not self.split_from:
self.prepare_depreciation_data()
if self.calculate_depreciation:
update_draft_asset_depr_schedules(self)
if frappe.db.exists("Asset", self.name):
asset_depr_schedules_names = make_draft_asset_depr_schedules_if_not_present(self)
if asset_depr_schedules_names:
asset_depr_schedules_links = get_comma_separated_links(
asset_depr_schedules_names, "Asset Depreciation Schedule"
)
frappe.msgprint(
_(
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
).format(asset_depr_schedules_links)
)
self.status = self.get_status() self.status = self.get_status()
@ -61,17 +77,7 @@ class Asset(AccountsController):
if not self.booked_fixed_asset and self.validate_make_gl_entry(): if not self.booked_fixed_asset and self.validate_make_gl_entry():
self.make_gl_entries() self.make_gl_entries()
if self.calculate_depreciation and not self.split_from: if self.calculate_depreciation and not self.split_from:
asset_depr_schedules_names = make_draft_asset_depr_schedules_if_not_present(self)
convert_draft_asset_depr_schedules_into_active(self) convert_draft_asset_depr_schedules_into_active(self)
if asset_depr_schedules_names:
asset_depr_schedules_links = get_comma_separated_links(
asset_depr_schedules_names, "Asset Depreciation Schedule"
)
frappe.msgprint(
_(
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
).format(asset_depr_schedules_links)
)
self.set_status() self.set_status()
add_asset_activity(self.name, _("Asset submitted")) add_asset_activity(self.name, _("Asset submitted"))
@ -827,6 +833,7 @@ def get_item_details(item_code, asset_category, gross_purchase_amount):
"expected_value_after_useful_life": flt(gross_purchase_amount) "expected_value_after_useful_life": flt(gross_purchase_amount)
* flt(d.salvage_value_percentage / 100), * flt(d.salvage_value_percentage / 100),
"depreciation_start_date": d.depreciation_start_date or nowdate(), "depreciation_start_date": d.depreciation_start_date or nowdate(),
"rate_of_depreciation": d.rate_of_depreciation,
} }
) )

View File

@ -509,6 +509,9 @@ def restore_asset(asset_name):
def depreciate_asset(asset_doc, date, notes): def depreciate_asset(asset_doc, date, notes):
if not asset_doc.calculate_depreciation:
return
asset_doc.flags.ignore_validate_update_after_submit = True asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones( make_new_active_asset_depr_schedules_and_cancel_current_ones(
@ -521,6 +524,9 @@ def depreciate_asset(asset_doc, date, notes):
def reset_depreciation_schedule(asset_doc, date, notes): def reset_depreciation_schedule(asset_doc, date, notes):
if not asset_doc.calculate_depreciation:
return
asset_doc.flags.ignore_validate_update_after_submit = True asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones( make_new_active_asset_depr_schedules_and_cancel_current_ones(

View File

@ -52,7 +52,7 @@ frappe.query_reports["Fixed Asset Register"] = {
}, },
{ {
"fieldname": "include_default_book_assets", "fieldname": "include_default_book_assets",
"label": __("Include Default Book Assets"), "label": __("Include Default FB Assets"),
"fieldtype": "Check", "fieldtype": "Check",
"default": 1 "default": 1
}, },

View File

@ -223,7 +223,7 @@ def get_assets_linked_to_fb(filters):
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book") company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb): if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
frappe.throw(_("To use a different finance book, please uncheck 'Include Default Book Assets'")) frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Assets'"))
query = query.where( query = query.where(
(afb.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""])) (afb.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))

View File

@ -1,30 +1,21 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Bulk Transaction Log', { frappe.ui.form.on("Bulk Transaction Log", {
refresh(frm) {
refresh: function(frm) { frm.add_custom_button(__('Succeeded Entries'), function() {
frm.disable_save(); frappe.set_route('List', 'Bulk Transaction Log Detail', {'date': frm.doc.date, 'transaction_status': "Success"});
frm.add_custom_button(__('Retry Failed Transactions'), ()=>{ }, __("View"));
frappe.confirm(__("Retry Failing Transactions ?"), ()=>{ frm.add_custom_button(__('Failed Entries'), function() {
query(frm, 1); frappe.set_route('List', 'Bulk Transaction Log Detail', {'date': frm.doc.date, 'transaction_status': "Failed"});
} }, __("View"));
); if (frm.doc.failed) {
}); frm.add_custom_button(__('Retry Failed Transactions'), function() {
} frappe.call({
method: "erpnext.utilities.bulk_transaction.retry",
args: {date: frm.doc.date}
});
});
}
},
}); });
function query(frm) {
frappe.call({
method: "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction",
args: {
log_date: frm.doc.log_date
}
}).then((r) => {
if (r.message === "No Failed Records") {
frappe.show_alert(__(r.message), 5);
} else {
frappe.show_alert(__("Retrying Failed Transactions"), 5);
}
});
}

View File

@ -1,31 +1,64 @@
{ {
"actions": [], "actions": [],
"allow_rename": 1, "allow_copy": 1,
"creation": "2021-11-30 13:41:16.343827", "creation": "2023-11-09 20:14:45.139593",
"default_view": "List",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"log_date", "date",
"logger_data" "column_break_bsan",
"log_entries",
"section_break_mdmv",
"succeeded",
"column_break_qryp",
"failed"
], ],
"fields": [ "fields": [
{ {
"fieldname": "log_date", "fieldname": "date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Log Date", "in_list_view": 1,
"in_standard_filter": 1,
"label": "Date",
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "logger_data", "fieldname": "log_entries",
"fieldtype": "Table", "fieldtype": "Int",
"label": "Logger Data", "in_list_view": 1,
"options": "Bulk Transaction Log Detail" "label": "Log Entries",
"read_only": 1
},
{
"fieldname": "column_break_bsan",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_mdmv",
"fieldtype": "Section Break"
},
{
"fieldname": "succeeded",
"fieldtype": "Int",
"label": "Succeeded",
"read_only": 1
},
{
"fieldname": "column_break_qryp",
"fieldtype": "Column Break"
},
{
"fieldname": "failed",
"fieldtype": "Int",
"label": "Failed",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "in_create": 1,
"is_virtual": 1,
"links": [], "links": [],
"modified": "2022-02-03 17:23:02.935325", "modified": "2023-11-11 04:52:49.347376",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Bulk Transaction", "module": "Bulk Transaction",
"name": "Bulk Transaction Log", "name": "Bulk Transaction Log",
@ -47,5 +80,5 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "title_field": "date"
} }

View File

@ -1,67 +1,112 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from datetime import date
import frappe import frappe
from frappe import qb
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.functions import Count
from erpnext.utilities.bulk_transaction import task, update_logger from frappe.utils import cint
from pypika import Order
class BulkTransactionLog(Document): class BulkTransactionLog(Document):
pass def db_insert(self, *args, **kwargs):
pass
def load_from_db(self):
log_detail = qb.DocType("Bulk Transaction Log Detail")
@frappe.whitelist() has_records = frappe.db.sql(
def retry_failing_transaction(log_date=None): f"select exists (select * from `tabBulk Transaction Log Detail` where date = '{self.name}');"
if not log_date: )[0][0]
log_date = str(date.today()) if not has_records:
btp = frappe.qb.DocType("Bulk Transaction Log Detail") raise frappe.DoesNotExistError
data = (
frappe.qb.from_(btp)
.select(btp.transaction_name, btp.from_doctype, btp.to_doctype)
.distinct()
.where(btp.retried != 1)
.where(btp.transaction_status == "Failed")
.where(btp.date == log_date)
).run(as_dict=True)
if data: succeeded_logs = (
if len(data) > 10: qb.from_(log_detail)
frappe.enqueue(job, queue="long", job_name="bulk_retry", data=data, log_date=log_date) .select(Count(log_detail.date).as_("count"))
else: .where((log_detail.date == self.name) & (log_detail.transaction_status == "Success"))
job(data, log_date) .run()
else: )[0][0] or 0
return "No Failed Records" failed_logs = (
qb.from_(log_detail)
.select(Count(log_detail.date).as_("count"))
.where((log_detail.date == self.name) & (log_detail.transaction_status == "Failed"))
.run()
)[0][0] or 0
total_logs = succeeded_logs + failed_logs
transaction_log = frappe._dict(
{
"date": self.name,
"count": total_logs,
"succeeded": succeeded_logs,
"failed": failed_logs,
}
)
super(Document, self).__init__(serialize_transaction_log(transaction_log))
@staticmethod
def get_list(args):
filter_date = parse_list_filters(args)
limit = cint(args.get("page_length")) or 20
log_detail = qb.DocType("Bulk Transaction Log Detail")
def job(data, log_date): dates_query = (
for d in data: qb.from_(log_detail)
failed = [] .select(log_detail.date)
try: .distinct()
frappe.db.savepoint("before_creation_of_record") .orderby(log_detail.date, order=Order.desc)
task(d.transaction_name, d.from_doctype, d.to_doctype) .limit(limit)
except Exception as e: )
frappe.db.rollback(save_point="before_creation_of_record") if filter_date:
failed.append(e) dates_query = dates_query.where(log_detail.date == filter_date)
update_logger( dates = dates_query.run()
d.transaction_name,
e, transaction_logs = []
d.from_doctype, if dates:
d.to_doctype, transaction_logs_query = (
status="Failed", qb.from_(log_detail)
log_date=log_date, .select(log_detail.date.as_("date"), Count(log_detail.date).as_("count"))
restarted=1, .where(log_detail.date.isin(dates))
.orderby(log_detail.date, order=Order.desc)
.groupby(log_detail.date)
.limit(limit)
) )
transaction_logs = transaction_logs_query.run(as_dict=True)
if not failed: return [serialize_transaction_log(x) for x in transaction_logs]
update_logger(
d.transaction_name, @staticmethod
None, def get_count(args):
d.from_doctype, pass
d.to_doctype,
status="Success", @staticmethod
log_date=log_date, def get_stats(args):
restarted=1, pass
)
def db_update(self, *args, **kwargs):
pass
def delete(self):
pass
def serialize_transaction_log(data):
return frappe._dict(
name=data.date,
date=data.date,
log_entries=data.count,
succeeded=data.succeeded,
failed=data.failed,
)
def parse_list_filters(args):
# parse date filter
filter_date = None
for fil in args.get("filters"):
if isinstance(fil, list):
for elem in fil:
if elem == "date":
filter_date = fil[3]
return filter_date

View File

@ -1,79 +1,9 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest # import frappe
from datetime import date from frappe.tests.utils import FrappeTestCase
import frappe
from erpnext.utilities.bulk_transaction import transaction_processing
class TestBulkTransactionLog(unittest.TestCase): class TestBulkTransactionLog(FrappeTestCase):
def setUp(self): pass
create_company()
create_customer()
create_item()
def test_entry_in_log(self):
so_name = create_so()
transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")
doc = frappe.get_doc("Bulk Transaction Log", str(date.today()))
for d in doc.get("logger_data"):
if d.transaction_name == so_name:
self.assertEqual(d.transaction_name, so_name)
self.assertEqual(d.transaction_status, "Success")
self.assertEqual(d.from_doctype, "Sales Order")
self.assertEqual(d.to_doctype, "Sales Invoice")
self.assertEqual(d.retried, 0)
def create_company():
if not frappe.db.exists("Company", "_Test Company"):
frappe.get_doc(
{
"doctype": "Company",
"company_name": "_Test Company",
"country": "India",
"default_currency": "INR",
}
).insert()
def create_customer():
if not frappe.db.exists("Customer", "Bulk Customer"):
frappe.get_doc({"doctype": "Customer", "customer_name": "Bulk Customer"}).insert()
def create_item():
if not frappe.db.exists("Item", "MK"):
frappe.get_doc(
{
"doctype": "Item",
"item_code": "MK",
"item_name": "Milk",
"description": "Milk",
"item_group": "Products",
}
).insert()
def create_so(intent=None):
so = frappe.new_doc("Sales Order")
so.customer = "Bulk Customer"
so.company = "_Test Company"
so.transaction_date = date.today()
so.set_warehouse = "Finished Goods - _TC"
so.append(
"items",
{
"item_code": "MK",
"delivery_date": date.today(),
"qty": 10,
"rate": 80,
},
)
so.insert()
so.submit()
return so.name

View File

@ -0,0 +1,8 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Bulk Transaction Log Detail", {
// refresh(frm) {
// },
// });

View File

@ -6,12 +6,12 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"from_doctype",
"transaction_name", "transaction_name",
"date", "date",
"time", "time",
"transaction_status", "transaction_status",
"error_description", "error_description",
"from_doctype",
"to_doctype", "to_doctype",
"retried" "retried"
], ],
@ -20,8 +20,11 @@
"fieldname": "transaction_name", "fieldname": "transaction_name",
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1,
"label": "Name", "label": "Name",
"options": "from_doctype" "options": "from_doctype",
"read_only": 1,
"search_index": 1
}, },
{ {
"fieldname": "transaction_status", "fieldname": "transaction_status",
@ -39,9 +42,11 @@
{ {
"fieldname": "from_doctype", "fieldname": "from_doctype",
"fieldtype": "Link", "fieldtype": "Link",
"in_standard_filter": 1,
"label": "From Doctype", "label": "From Doctype",
"options": "DocType", "options": "DocType",
"read_only": 1 "read_only": 1,
"search_index": 1
}, },
{ {
"fieldname": "to_doctype", "fieldname": "to_doctype",
@ -54,8 +59,10 @@
"fieldname": "date", "fieldname": "date",
"fieldtype": "Date", "fieldtype": "Date",
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1,
"label": "Date ", "label": "Date ",
"read_only": 1 "read_only": 1,
"search_index": 1
}, },
{ {
"fieldname": "time", "fieldname": "time",
@ -66,19 +73,33 @@
{ {
"fieldname": "retried", "fieldname": "retried",
"fieldtype": "Int", "fieldtype": "Int",
"in_list_view": 1,
"label": "Retried", "label": "Retried",
"read_only": 1 "read_only": 1
} }
], ],
"in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1,
"links": [], "links": [],
"modified": "2022-02-03 19:57:31.650359", "modified": "2023-11-10 11:44:10.758342",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Bulk Transaction", "module": "Bulk Transaction",
"name": "Bulk Transaction Log Detail", "name": "Bulk Transaction Log Detail",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],

View File

@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestBulkTransactionLogDetail(FrappeTestCase):
pass

View File

@ -17,6 +17,7 @@
"po_required", "po_required",
"pr_required", "pr_required",
"blanket_order_allowance", "blanket_order_allowance",
"project_update_frequency",
"column_break_12", "column_break_12",
"maintain_same_rate", "maintain_same_rate",
"set_landed_cost_based_on_purchase_invoice_rate", "set_landed_cost_based_on_purchase_invoice_rate",
@ -172,6 +173,14 @@
"fieldname": "blanket_order_allowance", "fieldname": "blanket_order_allowance",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Blanket Order Allowance (%)" "label": "Blanket Order Allowance (%)"
},
{
"default": "Each Transaction",
"description": "How often should Project be updated of Total Purchase Cost ?",
"fieldname": "project_update_frequency",
"fieldtype": "Select",
"label": "Update frequency of Project",
"options": "Each Transaction\nManual"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@ -179,7 +188,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-10-25 14:03:32.520418", "modified": "2023-11-24 10:55:51.287327",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View File

@ -189,6 +189,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"fetch_from": "item_code.image",
"fieldname": "image", "fieldname": "image",
"fieldtype": "Attach", "fieldtype": "Attach",
"hidden": 1, "hidden": 1,
@ -470,6 +471,7 @@
"fieldname": "material_request", "fieldname": "material_request",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Material Request", "label": "Material Request",
"mandatory_depends_on": "eval: doc.material_request_item",
"no_copy": 1, "no_copy": 1,
"oldfieldname": "prevdoc_docname", "oldfieldname": "prevdoc_docname",
"oldfieldtype": "Link", "oldfieldtype": "Link",
@ -485,6 +487,7 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Material Request Item", "label": "Material Request Item",
"mandatory_depends_on": "eval: doc.material_request",
"no_copy": 1, "no_copy": 1,
"oldfieldname": "prevdoc_detail_docname", "oldfieldname": "prevdoc_detail_docname",
"oldfieldtype": "Data", "oldfieldtype": "Data",
@ -914,7 +917,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-10-27 15:50:42.655573", "modified": "2023-11-14 18:34:27.267382",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",

View File

@ -9,6 +9,8 @@
"field_order": [ "field_order": [
"naming_series", "naming_series",
"company", "company",
"billing_address",
"billing_address_display",
"vendor", "vendor",
"column_break1", "column_break1",
"transaction_date", "transaction_date",
@ -292,13 +294,25 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Send Document Print", "label": "Send Document Print",
"print_hide": 1 "print_hide": 1
},
{
"fieldname": "billing_address",
"fieldtype": "Link",
"label": "Company Billing Address",
"options": "Address"
},
{
"fieldname": "billing_address_display",
"fieldtype": "Small Text",
"label": "Billing Address Details",
"read_only": 1
} }
], ],
"icon": "fa fa-shopping-cart", "icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-08-09 12:20:26.850623", "modified": "2023-11-06 12:45:28.898706",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Request for Quotation", "name": "Request for Quotation",

View File

@ -87,6 +87,7 @@
"width": "300px" "width": "300px"
}, },
{ {
"fetch_from": "item_code.image",
"fieldname": "image", "fieldname": "image",
"fieldtype": "Attach", "fieldtype": "Attach",
"hidden": 1, "hidden": 1,
@ -260,13 +261,15 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-09-24 17:26:46.276934", "modified": "2023-11-14 18:34:48.327224",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Request for Quotation Item", "name": "Request for Quotation Item",
"naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -165,16 +165,17 @@ class Supplier(TransactionBase):
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters): def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
supplier = filters.get("supplier") supplier = filters.get("supplier")
return frappe.db.sql( contact = frappe.qb.DocType("Contact")
""" dynamic_link = frappe.qb.DocType("Dynamic Link")
SELECT
`tabContact`.name from `tabContact`, return (
`tabDynamic Link` frappe.qb.from_(contact)
WHERE .join(dynamic_link)
`tabContact`.name = `tabDynamic Link`.parent .on(contact.name == dynamic_link.parent)
and `tabDynamic Link`.link_name = %(supplier)s .select(contact.name, contact.email_id)
and `tabDynamic Link`.link_doctype = 'Supplier' .where(
and `tabContact`.name like %(txt)s (dynamic_link.link_name == supplier)
""", & (dynamic_link.link_doctype == "Supplier")
{"supplier": supplier, "txt": "%%%s%%" % txt}, & (contact.name.like("%{0}%".format(txt)))
) )
).run(as_dict=False)

View File

@ -20,6 +20,10 @@
"valid_till", "valid_till",
"quotation_number", "quotation_number",
"amended_from", "amended_from",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"currency_and_price_list", "currency_and_price_list",
"currency", "currency",
"conversion_rate", "conversion_rate",
@ -79,6 +83,7 @@
"pricing_rule_details", "pricing_rule_details",
"pricing_rules", "pricing_rules",
"address_and_contact_tab", "address_and_contact_tab",
"supplier_address_section",
"supplier_address", "supplier_address",
"address_display", "address_display",
"column_break_72", "column_break_72",
@ -86,6 +91,14 @@
"contact_display", "contact_display",
"contact_mobile", "contact_mobile",
"contact_email", "contact_email",
"shipping_address_section",
"shipping_address",
"column_break_zjaq",
"shipping_address_display",
"company_billing_address_section",
"billing_address",
"column_break_gcth",
"billing_address_display",
"terms_tab", "terms_tab",
"tc_name", "tc_name",
"terms", "terms",
@ -838,6 +851,76 @@
"fieldname": "named_place", "fieldname": "named_place",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Named Place" "label": "Named Place"
},
{
"fieldname": "shipping_address",
"fieldtype": "Link",
"label": "Shipping Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "column_break_zjaq",
"fieldtype": "Column Break"
},
{
"fieldname": "shipping_address_display",
"fieldtype": "Small Text",
"label": "Shipping Address Details",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "shipping_address_section",
"fieldtype": "Section Break",
"label": "Shipping Address"
},
{
"fieldname": "supplier_address_section",
"fieldtype": "Section Break",
"label": "Supplier Address"
},
{
"fieldname": "company_billing_address_section",
"fieldtype": "Section Break",
"label": "Company Billing Address"
},
{
"fieldname": "billing_address",
"fieldtype": "Link",
"label": "Company Billing Address",
"options": "Address"
},
{
"fieldname": "column_break_gcth",
"fieldtype": "Column Break"
},
{
"fieldname": "billing_address_display",
"fieldtype": "Small Text",
"label": "Billing Address Details",
"read_only": 1
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
} }
], ],
"icon": "fa fa-shopping-cart", "icon": "fa fa-shopping-cart",
@ -845,7 +928,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-06-03 16:20:15.880114", "modified": "2023-11-17 12:34:30.083077",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier Quotation", "name": "Supplier Quotation",

View File

@ -68,6 +68,8 @@
"column_break_15", "column_break_15",
"manufacturer_part_no", "manufacturer_part_no",
"ad_sec_break", "ad_sec_break",
"cost_center",
"dimension_col_break",
"project", "project",
"section_break_44", "section_break_44",
"page_break" "page_break"
@ -553,19 +555,31 @@
"fieldname": "expected_delivery_date", "fieldname": "expected_delivery_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Expected Delivery Date" "label": "Expected Delivery Date"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-10-19 12:36:26.913211", "modified": "2023-11-17 12:25:26.235367",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier Quotation Item", "name": "Supplier Quotation Item",
"naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -239,7 +239,7 @@ class AccountsController(TransactionBase):
references_map.setdefault(x.parent, []).append(x.name) references_map.setdefault(x.parent, []).append(x.name)
for doc, rows in references_map.items(): for doc, rows in references_map.items():
unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc) unreconcile_doc = frappe.get_doc("Unreconcile Payment", doc)
for row in rows: for row in rows:
unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0]) unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0])
@ -248,9 +248,9 @@ class AccountsController(TransactionBase):
unreconcile_doc.save(ignore_permissions=True) unreconcile_doc.save(ignore_permissions=True)
# delete docs upon parent doc deletion # delete docs upon parent doc deletion
unreconcile_docs = frappe.db.get_all("Unreconcile Payments", filters={"voucher_no": self.name}) unreconcile_docs = frappe.db.get_all("Unreconcile Payment", filters={"voucher_no": self.name})
for x in unreconcile_docs: for x in unreconcile_docs:
_doc = frappe.get_doc("Unreconcile Payments", x.name) _doc = frappe.get_doc("Unreconcile Payment", x.name)
if _doc.docstatus == 1: if _doc.docstatus == 1:
_doc.cancel() _doc.cancel()
_doc.delete() _doc.delete()

View File

@ -105,26 +105,26 @@ class BuyingController(SubcontractingController):
def set_rate_for_standalone_debit_note(self): def set_rate_for_standalone_debit_note(self):
if self.get("is_return") and self.get("update_stock") and not self.return_against: if self.get("is_return") and self.get("update_stock") and not self.return_against:
for row in self.items: for row in self.items:
if row.rate <= 0:
# override the rate with valuation rate
row.rate = get_incoming_rate(
{
"item_code": row.item_code,
"warehouse": row.warehouse,
"posting_date": self.get("posting_date"),
"posting_time": self.get("posting_time"),
"qty": row.qty,
"serial_and_batch_bundle": row.get("serial_and_batch_bundle"),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
},
raise_error_if_no_rate=False,
)
# override the rate with valuation rate row.discount_percentage = 0.0
row.rate = get_incoming_rate( row.discount_amount = 0.0
{ row.margin_rate_or_amount = 0.0
"item_code": row.item_code,
"warehouse": row.warehouse,
"posting_date": self.get("posting_date"),
"posting_time": self.get("posting_time"),
"qty": row.qty,
"serial_and_batch_bundle": row.get("serial_and_batch_bundle"),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
},
raise_error_if_no_rate=False,
)
row.discount_percentage = 0.0
row.discount_amount = 0.0
row.margin_rate_or_amount = 0.0
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate) super(BuyingController, self).set_missing_values(for_validate)
@ -365,7 +365,7 @@ class BuyingController(SubcontractingController):
{ {
"item_code": d.item_code, "item_code": d.item_code,
"warehouse": d.get("from_warehouse"), "warehouse": d.get("from_warehouse"),
"posting_date": self.get("posting_date") or self.get("transation_date"), "posting_date": self.get("posting_date") or self.get("transaction_date"),
"posting_time": posting_time, "posting_time": posting_time,
"qty": -1 * flt(d.get("stock_qty")), "qty": -1 * flt(d.get("stock_qty")),
"serial_and_batch_bundle": d.get("serial_and_batch_bundle"), "serial_and_batch_bundle": d.get("serial_and_batch_bundle"),
@ -758,7 +758,7 @@ class BuyingController(SubcontractingController):
"calculate_depreciation": 0, "calculate_depreciation": 0,
"purchase_receipt_amount": purchase_amount, "purchase_receipt_amount": purchase_amount,
"gross_purchase_amount": purchase_amount, "gross_purchase_amount": purchase_amount,
"asset_quantity": row.qty if is_grouped_asset else 0, "asset_quantity": row.qty if is_grouped_asset else 1,
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None, "purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
"purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None, "purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None,
} }

View File

@ -611,6 +611,8 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
if filters.get("company"): if filters.get("company"):
condition += "and tabAccount.company = %(company)s" condition += "and tabAccount.company = %(company)s"
condition += f"and tabAccount.disabled = {filters.get('disabled', 0)}"
return frappe.db.sql( return frappe.db.sql(
"""select tabAccount.name from `tabAccount` """select tabAccount.name from `tabAccount`
where (tabAccount.report_type = "Profit and Loss" where (tabAccount.report_type = "Profit and Loss"

View File

@ -356,6 +356,7 @@ def make_return_doc(
if doc.doctype == "Sales Invoice" or doc.doctype == "POS Invoice": if doc.doctype == "Sales Invoice" or doc.doctype == "POS Invoice":
doc.consolidated_invoice = "" doc.consolidated_invoice = ""
doc.set("payments", []) doc.set("payments", [])
doc.update_billed_amount_in_delivery_note = True
for data in source.payments: for data in source.payments:
paid_amount = 0.00 paid_amount = 0.00
base_paid_amount = 0.00 base_paid_amount = 0.00

View File

@ -350,11 +350,12 @@ class SellingController(StockController):
return il return il
def has_product_bundle(self, item_code): def has_product_bundle(self, item_code):
return frappe.db.sql( product_bundle = frappe.qb.DocType("Product Bundle")
"""select name from `tabProduct Bundle` return (
where new_item_code=%s and docstatus != 2""", frappe.qb.from_(product_bundle)
item_code, .select(product_bundle.name)
) .where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0))
).run()
def get_already_delivered_qty(self, current_docname, so, so_detail): def get_already_delivered_qty(self, current_docname, so, so_detail):
delivered_via_dn = frappe.db.sql( delivered_via_dn = frappe.db.sql(

View File

@ -626,6 +626,18 @@ class SubcontractingController(StockController):
(row.item_code, row.get(self.subcontract_data.order_field)) (row.item_code, row.get(self.subcontract_data.order_field))
] -= row.qty ] -= row.qty
def __set_rate_for_serial_and_batch_bundle(self):
if self.doctype != "Subcontracting Receipt":
return
for row in self.get(self.raw_material_table):
if not row.get("serial_and_batch_bundle"):
continue
row.rate = frappe.get_cached_value(
"Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"
)
def __modify_serial_and_batch_bundle(self): def __modify_serial_and_batch_bundle(self):
if self.is_new(): if self.is_new():
return return
@ -681,6 +693,7 @@ class SubcontractingController(StockController):
self.__remove_changed_rows() self.__remove_changed_rows()
self.__set_supplied_items() self.__set_supplied_items()
self.__modify_serial_and_batch_bundle() self.__modify_serial_and_batch_bundle()
self.__set_rate_for_serial_and_batch_bundle()
def __validate_batch_no(self, row, key): def __validate_batch_no(self, row, key):
if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get( if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get(

View File

@ -54,6 +54,7 @@ class calculate_taxes_and_totals(object):
if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"): if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"):
self.doc.grand_total -= self.doc.discount_amount self.doc.grand_total -= self.doc.discount_amount
self.doc.base_grand_total -= self.doc.base_discount_amount self.doc.base_grand_total -= self.doc.base_discount_amount
self.doc.rounding_adjustment = self.doc.base_rounding_adjustment = 0.0
self.set_rounded_total() self.set_rounded_total()
self.calculate_shipping_charges() self.calculate_shipping_charges()

View File

@ -7,6 +7,8 @@ from frappe.contacts.address_and_contact import (
delete_contact_and_address, delete_contact_and_address,
load_address_and_contact, load_address_and_contact,
) )
from frappe.contacts.doctype.address.address import get_default_address
from frappe.contacts.doctype.contact.contact import get_default_contact
from frappe.email.inbox import link_communication_to_document from frappe.email.inbox import link_communication_to_document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address
@ -251,6 +253,13 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False):
target.customer_group = frappe.db.get_default("Customer Group") target.customer_group = frappe.db.get_default("Customer Group")
address = get_default_address("Lead", source.name)
contact = get_default_contact("Lead", source.name)
if address:
target.customer_primary_address = address
if contact:
target.customer_primary_contact = contact
doclist = get_mapped_doc( doclist = get_mapped_doc(
"Lead", "Lead",
source_name, source_name,

View File

@ -103,6 +103,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"fetch_from": "item_code.image",
"fieldname": "image", "fieldname": "image",
"fieldtype": "Attach", "fieldtype": "Attach",
"hidden": 1, "hidden": 1,
@ -165,7 +166,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-07-30 16:39:09.775720", "modified": "2023-11-14 18:35:30.887278",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Opportunity Item", "name": "Opportunity Item",
@ -173,5 +174,6 @@
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -421,7 +421,7 @@ scheduler_events = {
"hourly_long": [ "hourly_long": [
"erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process", "erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries", "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction", "erpnext.utilities.bulk_transaction.retry",
], ],
"daily": [ "daily": [
"erpnext.support.doctype.issue.issue.auto_close_tickets", "erpnext.support.doctype.issue.issue.auto_close_tickets",
@ -539,6 +539,8 @@ accounting_dimension_doctypes = [
"Subcontracting Receipt", "Subcontracting Receipt",
"Subcontracting Receipt Item", "Subcontracting Receipt Item",
"Account Closing Balance", "Account Closing Balance",
"Supplier Quotation",
"Supplier Quotation Item",
] ]
get_matching_queries = ( get_matching_queries = (

Some files were not shown because too many files have changed in this diff Show More