Compare commits

..

1 Commits

Author SHA1 Message Date
RitvikSardana
3714b795d6 fix: POS opening issue because of Product Bundle 2023-09-18 17:22:02 +05:30
423 changed files with 55943 additions and 11720 deletions

View File

@@ -3,7 +3,7 @@ import inspect
import frappe
__version__ = "14.55.2"
__version__ = "14.34.3"
def get_default_company(user=None):

View File

@@ -37,7 +37,6 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date):
}
)
cle.flags.ignore_permissions = True
cle.flags.ignore_links = True
cle.submit()

View File

@@ -301,30 +301,3 @@ def get_dimensions(with_cost_center_and_project=False):
default_dimensions_map[dimension.company][dimension.fieldname] = dimension.default_dimension
return dimension_filters, default_dimensions_map
def create_accounting_dimensions_for_doctype(doctype):
accounting_dimensions = frappe.db.get_all(
"Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"]
)
if not accounting_dimensions:
return
for d in accounting_dimensions:
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
if field:
continue
df = {
"fieldname": d.fieldname,
"label": d.label,
"fieldtype": "Link",
"options": d.document_type,
"insert_after": "accounting_dimensions_section",
}
create_custom_field(doctype, df, ignore_validate=True)
frappe.clear_cache(doctype=doctype)

View File

@@ -68,12 +68,7 @@
"enable_party_matching",
"enable_fuzzy_matching",
"tab_break_dpet",
"show_balance_in_coa",
"reports_tab",
"remarks_section",
"general_ledger_remarks_length",
"column_break_lvjk",
"receivable_payable_remarks_length"
"show_balance_in_coa"
],
"fields": [
{
@@ -434,34 +429,6 @@
"fieldname": "show_balance_in_coa",
"fieldtype": "Check",
"label": "Show Balances in Chart Of Accounts"
},
{
"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",
@@ -469,7 +436,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-11-20 09:37:47.650347",
"modified": "2023-07-27 15:05:34.000264",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -17,7 +17,6 @@ from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_s
get_entries,
)
from erpnext.accounts.utils import get_balance_on
from erpnext.setup.utils import get_exchange_rate
class BankReconciliationTool(Document):
@@ -130,7 +129,7 @@ def create_journal_entry_bts(
bank_transaction = frappe.db.get_values(
"Bank Transaction",
bank_transaction_name,
fieldname=["name", "deposit", "withdrawal", "bank_account", "currency"],
fieldname=["name", "deposit", "withdrawal", "bank_account"],
as_dict=True,
)[0]
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
@@ -144,94 +143,29 @@ def create_journal_entry_bts(
)
company = frappe.get_value("Account", company_account, "company")
company_default_currency = frappe.get_cached_value("Company", company, "default_currency")
company_account_currency = frappe.get_cached_value("Account", company_account, "account_currency")
second_account_currency = frappe.get_cached_value("Account", second_account, "account_currency")
# determine if multi-currency Journal or not
is_multi_currency = (
True
if company_default_currency != company_account_currency
or company_default_currency != second_account_currency
or company_default_currency != bank_transaction.currency
else False
)
accounts = []
second_account_dict = {
"account": second_account,
"account_currency": second_account_currency,
"credit_in_account_currency": bank_transaction.deposit,
"debit_in_account_currency": bank_transaction.withdrawal,
"party_type": party_type,
"party": party,
"cost_center": get_default_cost_center(company),
}
# Multi Currency?
accounts.append(
{
"account": second_account,
"credit_in_account_currency": bank_transaction.deposit,
"debit_in_account_currency": bank_transaction.withdrawal,
"party_type": party_type,
"party": party,
"cost_center": get_default_cost_center(company),
}
)
company_account_dict = {
"account": company_account,
"account_currency": company_account_currency,
"bank_account": bank_transaction.bank_account,
"credit_in_account_currency": bank_transaction.withdrawal,
"debit_in_account_currency": bank_transaction.deposit,
"cost_center": get_default_cost_center(company),
}
# convert transaction amount to company currency
if is_multi_currency:
exc_rate = get_exchange_rate(bank_transaction.currency, company_default_currency, posting_date)
withdrawal_in_company_currency = flt(exc_rate * abs(bank_transaction.withdrawal))
deposit_in_company_currency = flt(exc_rate * abs(bank_transaction.deposit))
else:
withdrawal_in_company_currency = bank_transaction.withdrawal
deposit_in_company_currency = bank_transaction.deposit
# if second account is of foreign currency, convert and set debit and credit fields.
if second_account_currency != company_default_currency:
exc_rate = get_exchange_rate(second_account_currency, company_default_currency, posting_date)
second_account_dict.update(
{
"exchange_rate": exc_rate,
"credit": deposit_in_company_currency,
"debit": withdrawal_in_company_currency,
"credit_in_account_currency": flt(deposit_in_company_currency / exc_rate) or 0,
"debit_in_account_currency": flt(withdrawal_in_company_currency / exc_rate) or 0,
}
)
else:
second_account_dict.update(
{
"exchange_rate": 1,
"credit": deposit_in_company_currency,
"debit": withdrawal_in_company_currency,
"credit_in_account_currency": deposit_in_company_currency,
"debit_in_account_currency": withdrawal_in_company_currency,
}
)
# if company account is of foreign currency, convert and set debit and credit fields.
if company_account_currency != company_default_currency:
exc_rate = get_exchange_rate(company_account_currency, company_default_currency, posting_date)
company_account_dict.update(
{
"exchange_rate": exc_rate,
"credit": withdrawal_in_company_currency,
"debit": deposit_in_company_currency,
}
)
else:
company_account_dict.update(
{
"exchange_rate": 1,
"credit": withdrawal_in_company_currency,
"debit": deposit_in_company_currency,
"credit_in_account_currency": withdrawal_in_company_currency,
"debit_in_account_currency": deposit_in_company_currency,
}
)
accounts.append(second_account_dict)
accounts.append(company_account_dict)
accounts.append(
{
"account": company_account,
"bank_account": bank_transaction.bank_account,
"credit_in_account_currency": bank_transaction.withdrawal,
"debit_in_account_currency": bank_transaction.deposit,
"cost_center": get_default_cost_center(company),
}
)
journal_entry_dict = {
"voucher_type": entry_type,
@@ -241,9 +175,6 @@ def create_journal_entry_bts(
"cheque_no": reference_number,
"mode_of_payment": mode_of_payment,
}
if is_multi_currency:
journal_entry_dict.update({"multi_currency": True})
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.update(journal_entry_dict)
journal_entry.set("accounts", accounts)

View File

@@ -2,16 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("Bank Statement Import", {
onload(frm) {
frm.set_query("bank_account", function (doc) {
return {
filters: {
company: doc.company,
},
};
});
},
setup(frm) {
frappe.realtime.on("data_import_refresh", ({ data_import }) => {
frm.import_in_progress = false;
@@ -362,11 +352,10 @@ frappe.ui.form.on("Bank Statement Import", {
export_errored_rows(frm) {
open_url_post(
"/api/method/erpnext.accounts.doctype.bank_statement_import.bank_statement_import.download_errored_template",
"/api/method/frappe.core.doctype.data_import.data_import.download_errored_template",
{
data_import_name: frm.doc.name,
},
true
}
);
},

View File

@@ -112,8 +112,7 @@ class AutoMatchbyPartyNameDescription:
for party in parties:
filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
field = party.lower() + "_name"
names = frappe.get_all(party, filters=filters, fields=[f"{field} as party_name", "name"])
names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name")
for field in ["bank_party_name", "description"]:
if not self.get(field):
@@ -132,11 +131,7 @@ class AutoMatchbyPartyNameDescription:
def fuzzy_search_and_return_result(self, party, names, field) -> Union[Tuple, None]:
skip = False
result = process.extract(
query=self.get(field),
choices={row.get("name"): row.get("party_name") for row in names},
scorer=fuzz.token_set_ratio,
)
result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio)
party_name, skip = self.process_fuzzy_result(result)
if not party_name:
@@ -154,14 +149,14 @@ class AutoMatchbyPartyNameDescription:
Returns: Result, Skip (whether or not to discontinue matching)
"""
SCORE, PARTY_ID, CUTOFF = 1, 2, 80
PARTY, SCORE, CUTOFF = 0, 1, 80
if not result or not len(result):
return None, False
first_result = result[0]
if len(result) == 1:
return (first_result[PARTY_ID] if first_result[SCORE] > CUTOFF else None), True
return (first_result[PARTY] if first_result[SCORE] > CUTOFF else None), True
second_result = result[1]
if first_result[SCORE] > CUTOFF:
@@ -170,7 +165,7 @@ class AutoMatchbyPartyNameDescription:
if first_result[SCORE] == second_result[SCORE]:
return None, True
return first_result[PARTY_ID], True
return first_result[PARTY], True
else:
return None, False

View File

@@ -89,6 +89,7 @@ class BankTransaction(StatusUpdater):
- 0 > a: Error: already over-allocated
- clear means: set the latest transaction date as clearance date
"""
gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
remaining_amount = self.unallocated_amount
for payment_entry in self.payment_entries:
if payment_entry.allocated_amount == 0.0:

View File

@@ -53,18 +53,10 @@ frappe.ui.form.on('Chart of Accounts Importer', {
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() {
let data = d.get_values();
var data = d.get_values();
if (!data.template_type) {
frappe.throw(__('Please select <b>Template Type</b> to download template'));
@@ -74,8 +66,7 @@ frappe.ui.form.on('Chart of Accounts Importer', {
'/api/method/erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.download_template',
{
file_type: data.file_type,
template_type: data.template_type,
company: data.company
template_type: data.template_type
}
);

View File

@@ -8,7 +8,6 @@ from functools import reduce
import frappe
from frappe import _
from frappe.desk.form.linked_with import get_linked_fields
from frappe.model.document import Document
from frappe.utils import cint, cstr
from frappe.utils.csvutils import UnicodeWriter
@@ -113,7 +112,7 @@ def generate_data_from_csv(file_doc, as_dict=False):
if as_dict:
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
else:
if not row[1] and len(row) > 1:
if not row[1]:
row[1] = row[0]
row[3] = row[2]
data.append(row)
@@ -295,8 +294,10 @@ def build_response_as_excel(writer):
@frappe.whitelist()
def download_template(file_type, template_type, company):
writer = get_template(template_type, company)
def download_template(file_type, template_type):
data = frappe._dict(frappe.local.form_dict)
writer = get_template(template_type)
if file_type == "CSV":
# download csv file
@@ -307,7 +308,8 @@ def download_template(file_type, template_type, company):
build_response_as_excel(writer)
def get_template(template_type, company):
def get_template(template_type):
fields = [
"Account Name",
"Parent Account",
@@ -333,17 +335,34 @@ def get_template(template_type, company):
["", "", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
)
else:
writer = get_sample_template(writer, company)
writer = get_sample_template(writer)
return writer
def get_sample_template(writer, company):
currency = frappe.db.get_value("Company", company, "default_currency")
with open(os.path.join(os.path.dirname(__file__), "coa_sample_template.csv"), "r") as f:
for row in f:
row = row.strip().split(",") + [currency]
writer.writerow(row)
def get_sample_template(writer):
template = [
["Application Of Funds(Assets)", "", "", "", 1, "", "Asset"],
["Sources Of Funds(Liabilities)", "", "", "", 1, "", "Liability"],
["Equity", "", "", "", 1, "", "Equity"],
["Expenses", "", "", "", 1, "", "Expense"],
["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
@@ -434,11 +453,14 @@ def get_mandatory_account_types():
def unset_existing_data(company):
# remove accounts data from company
linked = frappe.db.sql(
'''select fieldname from tabDocField
where fieldtype="Link" and options="Account" and parent="Company"''',
as_dict=True,
)
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}
# remove accounts data from company
update_values = {d.fieldname: "" for d in linked}
frappe.db.set_value("Company", company, update_values, update_values)
# remove accounts data from various doctypes

View File

@@ -1,17 +0,0 @@
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

@@ -6,10 +6,8 @@
"engine": "InnoDB",
"field_order": [
"api_details_section",
"disabled",
"service_provider",
"api_endpoint",
"access_key",
"url",
"column_break_3",
"help",
@@ -79,24 +77,12 @@
"label": "Service Provider",
"options": "frankfurter.app\nexchangerate.host\nCustom",
"reqd": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"depends_on": "eval:doc.service_provider == 'exchangerate.host';",
"fieldname": "access_key",
"fieldtype": "Data",
"label": "Access Key"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-10-04 15:30:25.333860",
"modified": "2022-01-10 15:51:14.521174",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",

View File

@@ -18,21 +18,11 @@ class CurrencyExchangeSettings(Document):
def set_parameters_and_result(self):
if self.service_provider == "exchangerate.host":
if not self.access_key:
frappe.throw(
_("Access Key is required for Service Provider: {0}").format(
frappe.bold(self.service_provider)
)
)
self.set("result_key", [])
self.set("req_params", [])
self.api_endpoint = "https://api.exchangerate.host/convert"
self.append("result_key", {"key": "result"})
self.append("req_params", {"key": "access_key", "value": self.access_key})
self.append("req_params", {"key": "amount", "value": "1"})
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
self.append("req_params", {"key": "from", "value": "{from_currency}"})
self.append("req_params", {"key": "to", "value": "{to_currency}"})

View File

@@ -192,7 +192,7 @@ class ExchangeRateRevaluation(Document):
# round off balance based on currency precision
# and consider debit-credit difference allowance
currency_precision = get_currency_precision()
rounding_loss_allowance = float(rounding_loss_allowance)
rounding_loss_allowance = float(rounding_loss_allowance) or 0.05
for acc in account_details:
acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision)
if abs(acc.balance_in_account_currency) <= rounding_loss_allowance:

View File

@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", {
setup: function(frm) {
frm.add_fetch("bank_account", "account", "account");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"];
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger', 'Asset', 'Asset Movement', 'Repost Accounting Ledger'];
},
refresh: function(frm) {
@@ -50,18 +50,8 @@ frappe.ui.form.on("Journal Entry", {
frm.trigger("make_inter_company_journal_entry");
}, __('Make'));
}
},
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
},
before_save: function(frm) {
if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) {
let payment_entry_references = frm.doc.accounts.filter(elem => (elem.reference_type == "Payment Entry"));
if (payment_entry_references.length > 0) {
let rows = payment_entry_references.map(x => "#"+x.idx);
frappe.throw(__("Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually.", [frappe.utils.comma_and(rows)]));
}
}
},
make_inter_company_journal_entry: function(frm) {
var d = new frappe.ui.Dialog({
title: __("Select Company"),

View File

@@ -548,16 +548,8 @@
"icon": "fa fa-file-text",
"idx": 176,
"is_submittable": 1,
"links": [
{
"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",
"links": [],
"modified": "2023-08-10 14:32:22.366895",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -98,8 +98,6 @@ class JournalEntry(AccountsController):
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
"Unreconcile Payment",
"Unreconcile Payment Entries",
)
self.make_gl_entries(1)
self.update_advance_paid()
@@ -498,7 +496,7 @@ class JournalEntry(AccountsController):
).format(d.reference_name, d.account)
)
else:
dr_or_cr = "debit" if flt(d.credit) > 0 else "credit"
dr_or_cr = "debit" if d.credit > 0 else "credit"
valid = False
for jvd in against_entries:
if flt(jvd[dr_or_cr]) > 0:

View File

@@ -203,8 +203,7 @@
"fieldtype": "Select",
"label": "Reference Type",
"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",
"search_index": 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"
},
{
"fieldname": "reference_name",
@@ -212,8 +211,7 @@
"in_list_view": 1,
"label": "Reference Name",
"no_copy": 1,
"options": "reference_type",
"search_index": 1
"options": "reference_type"
},
{
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])",
@@ -280,14 +278,13 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Reference Detail No",
"no_copy": 1,
"search_index": 1
"no_copy": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-11-23 11:44:25.841187",
"modified": "2023-06-16 14:11:13.507807",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
frappe.ui.form.on('Payment Entry', {
onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries'];
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger'];
if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
@@ -152,13 +152,6 @@ frappe.ui.form.on('Payment Entry', {
frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm);
frm.events.show_general_ledger(frm);
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() {
frappe.set_route("List", "Journal Entry", {"voucher_type": "Exchange Gain Or Loss", "reference_name": frm.doc.name});
}, __('Actions'));
}
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
},
validate_company: (frm) => {
@@ -829,6 +822,7 @@ frappe.ui.form.on('Payment Entry', {
else
total_negative_outstanding += Math.abs(flt(row.outstanding_amount));
})
var allocated_negative_outstanding = 0;
if (
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
@@ -843,7 +837,6 @@ frappe.ui.form.on('Payment Entry', {
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
} 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(total_negative_outstanding == 0) {
frappe.msgprint(

View File

@@ -739,16 +739,8 @@
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [
{
"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",
"links": [],
"modified": "2023-06-19 11:38:04.387219",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
@@ -794,4 +786,4 @@
"states": [],
"title_field": "title",
"track_changes": 1
}
}

View File

@@ -9,8 +9,6 @@ import frappe
from frappe import ValidationError, _, qb, scrub, throw
from frappe.utils import cint, comma_or, flt, getdate, nowdate
from frappe.utils.data import comma_and, fmt_money
from pypika import Case
from pypika.functions import Coalesce, Sum
import erpnext
from erpnext.accounts.doctype.bank_account.bank_account import (
@@ -109,8 +107,6 @@ class PaymentEntry(AccountsController):
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
"Unreconcile Payment",
"Unreconcile Payment Entries",
)
super(PaymentEntry, self).on_cancel()
self.make_gl_entries(cancel=1)
@@ -231,18 +227,16 @@ class PaymentEntry(AccountsController):
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
latest = latest.get(d.payment_term) or latest.get(None)
# The reference has already been fully paid
if not latest:
frappe.throw(
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
)
# The reference has already been partly paid
elif (
latest.outstanding_amount < latest.invoice_amount
and flt(d.outstanding_amount, d.precision("outstanding_amount"))
!= flt(latest.outstanding_amount, d.precision("outstanding_amount"))
and d.payment_term == ""
):
elif latest.outstanding_amount < latest.invoice_amount and flt(
d.outstanding_amount, d.precision("outstanding_amount")
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
frappe.throw(
_(
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
@@ -915,11 +909,8 @@ class PaymentEntry(AccountsController):
):
return
total_negative_outstanding = flt(
sum(
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
),
self.references[0].precision("outstanding_amount") if self.references else None,
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
@@ -1554,43 +1545,13 @@ def get_outstanding_reference_documents(args):
return data
def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list:
"""Split a list of invoices based on their payment terms."""
exc_rates = get_currency_data(outstanding_invoices, company)
def split_invoices_based_on_payment_terms(outstanding_invoices, company):
invoice_ref_based_on_payment_terms = {}
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
if len(split_rows) > 1:
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 = (
frappe.db.get_value("Company", company, "default_currency") if company else None
)
exc_rates = frappe._dict()
for doctype in ["Sales Invoice", "Purchase Invoice"]:
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
for x in frappe.db.get_all(
@@ -1605,54 +1566,72 @@ def get_currency_data(outstanding_invoices: list, company: str = None) -> dict:
company_currency=company_currency,
)
return exc_rates
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,
}
for idx, d in enumerate(outstanding_invoices):
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"
)
)
if payment_term_template:
allocate_payment_based_on_payment_terms = frappe.db.get_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=["*"]
)
return split_rows
for payment_term in payment_schedule:
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": flt(d.outstanding_amount),
"payment_term_outstanding": payment_term_outstanding,
"allocated_amount": payment_term_outstanding
if payment_term_outstanding
else d.outstanding_amount,
"payment_amount": payment_term.payment_amount,
"payment_term": payment_term.payment_term,
}
)
)
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(
@@ -1856,24 +1835,18 @@ def get_company_defaults(company):
def get_outstanding_on_journal_entry(name):
gl = frappe.qb.DocType("GL Entry")
res = (
frappe.qb.from_(gl)
.select(
Case()
.when(
gl.party_type == "Customer",
Coalesce(Sum(gl.debit_in_account_currency - gl.credit_in_account_currency), 0),
)
.else_(Coalesce(Sum(gl.credit_in_account_currency - gl.debit_in_account_currency), 0))
.as_("outstanding_amount")
)
.where(
(Coalesce(gl.party_type, "") != "")
& (gl.is_cancelled == 0)
& ((gl.voucher_no == name) | (gl.against_voucher == name))
)
).run(as_dict=True)
res = frappe.db.sql(
"SELECT "
'CASE WHEN party_type IN ("Customer") '
"THEN ifnull(sum(debit_in_account_currency - credit_in_account_currency), 0) "
"ELSE ifnull(sum(credit_in_account_currency - debit_in_account_currency), 0) "
"END as outstanding_amount "
"FROM `tabGL Entry` WHERE (voucher_no=%s OR against_voucher=%s) "
"AND party_type IS NOT NULL "
'AND party_type != ""',
(name, name),
as_dict=1,
)
outstanding_amount = res[0].get("outstanding_amount", 0) if res else 0

View File

@@ -1,17 +1,15 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import json
import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, nowdate
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import (
InvalidPaymentEntry,
get_outstanding_reference_documents,
get_payment_entry,
get_reference_details,
)
@@ -23,7 +21,6 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
create_sales_invoice,
create_sales_invoice_against_cost_center,
)
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.setup.doctype.employee.test_employee import make_employee
@@ -1222,115 +1219,6 @@ class TestPaymentEntry(FrappeTestCase):
so.reload()
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_partial_cancel_for_payment_entry(self):
si = create_sales_invoice()
pe = get_payment_entry(si.doctype, si.name)
pe.save()
pe.submit()
# Additional GL Entry
tax_amount = 10
reference_row = pe.references[0]
gl_args = {
"party_type": pe.party_type,
"party": pe.party,
"against_voucher_type": reference_row.reference_doctype,
"against_voucher": reference_row.reference_name,
"voucher_detail_no": reference_row.name,
}
gl_dicts = []
gl_dicts.extend(
[
pe.get_gl_dict(
{
"account": pe.paid_to,
"credit": tax_amount,
"credit_in_account_currency": tax_amount,
**gl_args,
}
),
pe.get_gl_dict(
{
"account": pe.paid_from,
"debit": tax_amount,
"debit_in_account_currency": tax_amount,
**gl_args,
}
),
]
)
make_gl_entries(gl_dicts)
# Assert PLEs Before
self.assertPLEntries(
pe,
[
{"amount": -100.0, "against_voucher_no": si.name},
{"amount": 10.0, "against_voucher_no": si.name},
],
)
# Partially cancel Payment Entry
make_reverse_gl_entries(gl_dicts, partial_cancel=True)
self.assertPLEntries(pe, [{"amount": -100.0, "against_voucher_no": si.name}])
def assertPLEntries(self, payment_doc, expected_pl_entries):
pl_entries = frappe.get_all(
"Payment Ledger Entry",
filters={
"voucher_type": payment_doc.doctype,
"voucher_no": payment_doc.name,
"delinked": 0,
},
fields=["amount", "against_voucher_no"],
)
out_str = json.dumps(sorted(pl_entries, key=json.dumps))
expected_out_str = json.dumps(sorted(expected_pl_entries, key=json.dumps))
self.assertEqual(out_str, expected_out_str)
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")
@@ -1391,9 +1279,6 @@ def create_payment_terms_template():
def create_payment_terms_template_with_discount(
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")
template_name = template_name or "Test Discount Template"

View File

@@ -13,7 +13,6 @@
"party_type",
"party",
"due_date",
"voucher_detail_no",
"cost_center",
"finance_book",
"voucher_type",
@@ -30,8 +29,7 @@
{
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date",
"search_index": 1
"label": "Posting Date"
},
{
"fieldname": "account_type",
@@ -65,8 +63,7 @@
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Voucher Type",
"options": "DocType",
"search_index": 1
"options": "DocType"
},
{
"fieldname": "voucher_no",
@@ -74,16 +71,14 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Voucher No",
"options": "voucher_type",
"search_index": 1
"options": "voucher_type"
},
{
"fieldname": "against_voucher_type",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Against Voucher Type",
"options": "DocType",
"search_index": 1
"options": "DocType"
},
{
"fieldname": "against_voucher_no",
@@ -91,8 +86,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Against Voucher No",
"options": "against_voucher_type",
"search_index": 1
"options": "against_voucher_type"
},
{
"fieldname": "amount",
@@ -148,18 +142,12 @@
"fieldname": "remarks",
"fieldtype": "Text",
"label": "Remarks"
},
{
"fieldname": "voucher_detail_no",
"fieldtype": "Data",
"label": "Voucher Detail No",
"search_index": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-11-08 10:53:10.664896",
"modified": "2022-08-22 15:32:56.629430",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Ledger Entry",

View File

@@ -216,7 +216,6 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
this.data = [];
const dialog = new frappe.ui.Dialog({
title: __("Select Difference Account"),
size: 'extra-large',
fields: [
{
fieldname: "allocation",
@@ -240,13 +239,6 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
in_list_view: 1,
read_only: 1
}, {
fieldtype:'Date',
fieldname:"gain_loss_posting_date",
label: __("Posting Date"),
in_list_view: 1,
reqd: 1,
}, {
fieldtype:'Link',
options: 'Account',
in_list_view: 1,
@@ -280,9 +272,6 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
args.forEach(d => {
frappe.model.set_value("Payment Reconciliation Allocation", d.docname,
"difference_account", d.difference_account);
frappe.model.set_value("Payment Reconciliation Allocation", d.docname,
"gain_loss_posting_date", d.gain_loss_posting_date);
});
this.reconcile_payment_entries();
@@ -298,7 +287,6 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
'reference_name': d.reference_name,
'difference_amount': d.difference_amount,
'difference_account': d.difference_account,
'gain_loss_posting_date': d.gain_loss_posting_date
});
}
});

View File

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

View File

@@ -19,7 +19,7 @@ from erpnext.accounts.utils import (
get_outstanding_invoices,
reconcile_against_document,
)
from erpnext.controllers.accounts_controller import get_advance_payment_entries_for_regional
from erpnext.controllers.accounts_controller import get_advance_payment_entries
class PaymentReconciliation(Document):
@@ -29,58 +29,6 @@ class PaymentReconciliation(Document):
self.accounting_dimension_filter_conditions = []
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()
def get_unreconciled_entries(self):
self.get_nonreconciled_payment_entries()
@@ -111,8 +59,10 @@ class PaymentReconciliation(Document):
def get_payment_entries(self):
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
condition = self.get_conditions(get_payments=True)
if self.payment_name:
condition += "name like '%%{0}%%'".format(self.payment_name)
payment_entries = get_advance_payment_entries_for_regional(
payment_entries = get_advance_payment_entries(
self.party_type,
self.party,
self.receivable_payable_account,
@@ -120,7 +70,6 @@ class PaymentReconciliation(Document):
against_all_orders=True,
limit=self.payment_limit,
condition=condition,
payment_name=self.payment_name,
)
return payment_entries
@@ -144,8 +93,6 @@ class PaymentReconciliation(Document):
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
)
limit = f"limit {self.payment_limit}" if self.payment_limit else " "
# nosemgrep
journal_entries = frappe.db.sql(
"""
@@ -169,13 +116,11 @@ class PaymentReconciliation(Document):
ELSE {bank_account_condition}
END)
order by t1.posting_date
{limit}
""".format(
**{
"dr_or_cr": dr_or_cr,
"bank_account_condition": bank_account_condition,
"condition": condition,
"limit": limit,
}
),
{
@@ -201,7 +146,7 @@ class PaymentReconciliation(Document):
if self.payment_name:
conditions.append(doc.name.like(f"%{self.payment_name}%"))
self.return_invoices_query = (
self.return_invoices = (
qb.from_(doc)
.select(
ConstantColumn(voucher_type).as_("voucher_type"),
@@ -209,11 +154,8 @@ class PaymentReconciliation(Document):
doc.return_against,
)
.where(Criterion.all(conditions))
.run(as_dict=True)
)
if self.payment_limit:
self.return_invoices_query = self.return_invoices_query.limit(self.payment_limit)
self.return_invoices = self.return_invoices_query.run(as_dict=True)
def get_dr_or_cr_notes(self):
@@ -373,7 +315,6 @@ class PaymentReconciliation(Document):
res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"])
res.difference_account = default_exchange_gain_loss_account
res.exchange_rate = inv.get("exchange_rate")
res.update({"gain_loss_posting_date": pay.get("posting_date")})
if pay.get("amount") == 0:
entries.append(res)
@@ -409,7 +350,6 @@ class PaymentReconciliation(Document):
)
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
adjust_allocations_for_taxes(self)
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
@@ -480,7 +420,6 @@ class PaymentReconciliation(Document):
"allocated_amount": flt(row.get("allocated_amount")),
"difference_amount": flt(row.get("difference_amount")),
"difference_account": row.get("difference_account"),
"difference_posting_date": row.get("gain_loss_posting_date"),
"cost_center": row.get("cost_center"),
}
)
@@ -538,27 +477,6 @@ class PaymentReconciliation(Document):
invoice_exchange_map.update(purchase_invoice_map)
journals = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Journal Entry"
]
journals.extend(
[d.get("reference_name") for d in payments if d.get("reference_type") == "Journal Entry"]
)
if journals:
journals = list(set(journals))
journals_map = frappe._dict(
frappe.db.get_all(
"Journal Entry Account",
filters={"parent": ("in", journals), "account": ("in", [self.receivable_payable_account])},
fields=[
"parent as `name`",
"exchange_rate",
],
as_list=1,
)
)
invoice_exchange_map.update(journals_map)
return invoice_exchange_map
def validate_allocation(self):
@@ -732,8 +650,3 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
None,
inv.cost_center,
)
@erpnext.allow_regional
def adjust_allocations_for_taxes(doc):
pass

View File

@@ -14,7 +14,6 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.stock.doctype.item.test_item import create_item
test_dependencies = ["Item"]
@@ -86,44 +85,26 @@ class TestPaymentReconciliation(FrappeTestCase):
self.customer5 = make_customer("_Test PR Customer 5", "EUR")
def create_account(self):
accounts = [
{
"attribute": "debtors_eur",
"account_name": "Debtors EUR",
"parent_account": "Accounts Receivable - _PR",
"account_currency": "EUR",
"account_type": "Receivable",
},
{
"attribute": "creditors_usd",
"account_name": "Payable USD",
"parent_account": "Accounts Payable - _PR",
"account_currency": "USD",
"account_type": "Payable",
},
]
for x in accounts:
x = frappe._dict(x)
if not frappe.db.get_value(
"Account", filters={"account_name": x.account_name, "company": self.company}
):
acc = frappe.new_doc("Account")
acc.account_name = x.account_name
acc.parent_account = x.parent_account
acc.company = self.company
acc.account_currency = x.account_currency
acc.account_type = x.account_type
acc.insert()
else:
name = frappe.db.get_value(
"Account",
filters={"account_name": x.account_name, "company": self.company},
fieldname="name",
pluck=True,
)
acc = frappe.get_doc("Account", name)
setattr(self, x.attribute, acc.name)
account_name = "Debtors EUR"
if not frappe.db.get_value(
"Account", filters={"account_name": account_name, "company": self.company}
):
acc = frappe.new_doc("Account")
acc.account_name = account_name
acc.parent_account = "Accounts Receivable - _PR"
acc.company = self.company
acc.account_currency = "EUR"
acc.account_type = "Receivable"
acc.insert()
else:
name = frappe.db.get_value(
"Account",
filters={"account_name": account_name, "company": self.company},
fieldname="name",
pluck=True,
)
acc = frappe.get_doc("Account", name)
self.debtors_eur = acc.name
def create_sales_invoice(
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
@@ -170,64 +151,6 @@ class TestPaymentReconciliation(FrappeTestCase):
payment.posting_date = posting_date
return payment
def create_purchase_invoice(
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
):
"""
Helper function to populate default values in sales invoice
"""
pinv = make_purchase_invoice(
qty=qty,
rate=rate,
company=self.company,
customer=self.supplier,
item_code=self.item,
item_name=self.item,
cost_center=self.cost_center,
warehouse=self.warehouse,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
update_stock=0,
currency="INR",
is_pos=0,
is_return=0,
return_against=None,
income_account=self.income_account,
expense_account=self.expense_account,
do_not_save=do_not_save,
do_not_submit=do_not_submit,
)
return pinv
def create_purchase_order(
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
):
"""
Helper function to populate default values in sales invoice
"""
pord = create_purchase_order(
qty=qty,
rate=rate,
company=self.company,
customer=self.supplier,
item_code=self.item,
item_name=self.item,
cost_center=self.cost_center,
warehouse=self.warehouse,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
update_stock=0,
currency="INR",
is_pos=0,
is_return=0,
return_against=None,
income_account=self.income_account,
expense_account=self.expense_account,
do_not_save=do_not_save,
do_not_submit=do_not_submit,
)
return pord
def clear_old_entries(self):
doctype_list = [
"GL Entry",
@@ -240,11 +163,13 @@ class TestPaymentReconciliation(FrappeTestCase):
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def create_payment_reconciliation(self, party_is_customer=True):
def create_payment_reconciliation(self):
pr = frappe.new_doc("Payment Reconciliation")
pr.company = self.company
pr.party_type = "Customer" if party_is_customer else "Supplier"
pr.party = self.customer if party_is_customer else self.supplier
pr.party_type = (
self.party_type if hasattr(self, "party_type") and self.party_type else "Customer"
)
pr.party = self.customer
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
return pr
@@ -981,13 +906,9 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(pr.allocation[0].difference_amount, 0)
def test_reconciliation_purchase_invoice_against_return(self):
self.supplier = "_Test Supplier USD"
pi = self.create_purchase_invoice(qty=5, rate=50, do_not_submit=True)
pi.supplier = self.supplier
pi.currency = "USD"
pi.conversion_rate = 50
pi.credit_to = self.creditors_usd
pi.save().submit()
pi = make_purchase_invoice(
supplier="_Test Supplier USD", currency="USD", conversion_rate=50
).submit()
pi_return = frappe.get_doc(pi.as_dict())
pi_return.name = None
@@ -997,12 +918,11 @@ class TestPaymentReconciliation(FrappeTestCase):
pi_return.items[0].qty = -pi_return.items[0].qty
pi_return.submit()
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()
self.company = "_Test Company"
self.party_type = "Supplier"
self.customer = "_Test Supplier USD"
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
invoices = []
@@ -1011,7 +931,6 @@ class TestPaymentReconciliation(FrappeTestCase):
if invoice.invoice_number == pi.name:
invoices.append(invoice.as_dict())
break
for payment in pr.payments:
if payment.reference_name == pi_return.name:
payments.append(payment.as_dict())
@@ -1022,155 +941,6 @@ class TestPaymentReconciliation(FrappeTestCase):
# Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit.
pr.reconcile()
def test_reconciliation_from_purchase_order_to_multiple_invoices(self):
"""
Reconciling advance payment from PO/SO to multiple invoices should not cause overallocation
"""
self.supplier = "_Test Supplier"
pi1 = self.create_purchase_invoice(qty=10, rate=100)
pi2 = self.create_purchase_invoice(qty=10, rate=100)
po = self.create_purchase_order(qty=20, rate=100)
pay = get_payment_entry(po.doctype, po.name)
# Overpay Puchase Order
pay.paid_amount = 3000
pay.save().submit()
# assert total allocated and unallocated before reconciliation
self.assertEqual(
(
pay.references[0].reference_doctype,
pay.references[0].reference_name,
pay.references[0].allocated_amount,
),
(po.doctype, po.name, 2000),
)
self.assertEqual(pay.total_allocated_amount, 2000)
self.assertEqual(pay.unallocated_amount, 1000)
self.assertEqual(pay.difference_amount, 0)
pr = self.create_payment_reconciliation(party_is_customer=False)
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 2)
self.assertEqual(len(pr.payments), 2)
for x in pr.payments:
self.assertEqual((x.reference_type, x.reference_name), (pay.doctype, pay.name))
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# partial allocation on pi1 and full allocate on pi2
pr.allocation[0].allocated_amount = 100
pr.reconcile()
# assert references and total allocated and unallocated amount
pay.reload()
self.assertEqual(len(pay.references), 3)
self.assertEqual(
(
pay.references[0].reference_doctype,
pay.references[0].reference_name,
pay.references[0].allocated_amount,
),
(po.doctype, po.name, 900),
)
self.assertEqual(
(
pay.references[1].reference_doctype,
pay.references[1].reference_name,
pay.references[1].allocated_amount,
),
(pi1.doctype, pi1.name, 100),
)
self.assertEqual(
(
pay.references[2].reference_doctype,
pay.references[2].reference_name,
pay.references[2].allocated_amount,
),
(pi2.doctype, pi2.name, 1000),
)
self.assertEqual(pay.total_allocated_amount, 2000)
self.assertEqual(pay.unallocated_amount, 1000)
self.assertEqual(pay.difference_amount, 0)
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 2)
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
# assert references and total allocated and unallocated amount
pay.reload()
self.assertEqual(len(pay.references), 3)
# PO references should be removed now
self.assertEqual(
(
pay.references[0].reference_doctype,
pay.references[0].reference_name,
pay.references[0].allocated_amount,
),
(pi1.doctype, pi1.name, 100),
)
self.assertEqual(
(
pay.references[1].reference_doctype,
pay.references[1].reference_name,
pay.references[1].allocated_amount,
),
(pi2.doctype, pi2.name, 1000),
)
self.assertEqual(
(
pay.references[2].reference_doctype,
pay.references[2].reference_name,
pay.references[2].allocated_amount,
),
(pi1.doctype, pi1.name, 900),
)
self.assertEqual(pay.total_allocated_amount, 2000)
self.assertEqual(pay.unallocated_amount, 1000)
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):
if not frappe.db.exists("Customer", customer_name):

View File

@@ -19,7 +19,6 @@
"is_advance",
"section_break_5",
"difference_amount",
"gain_loss_posting_date",
"column_break_7",
"difference_account",
"exchange_rate",
@@ -152,17 +151,11 @@
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "gain_loss_posting_date",
"fieldtype": "Date",
"label": "Difference Posting Date"
}
],
"is_virtual": 1,
"istable": 1,
"links": [],
"modified": "2023-11-17 17:33:38.612615",
"modified": "2023-09-03 07:52:33.684217",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Allocation",

View File

@@ -6,6 +6,4 @@ from frappe.model.document import Document
class PaymentReconciliationAllocation(Document):
@staticmethod
def get_list(args):
pass
pass

View File

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

View File

@@ -6,6 +6,4 @@ from frappe.model.document import Document
class PaymentReconciliationInvoice(Document):
@staticmethod
def get_list(args):
pass
pass

View File

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

View File

@@ -6,6 +6,4 @@ from frappe.model.document import Document
class PaymentReconciliationPayment(Document):
@staticmethod
def get_list(args):
pass
pass

View File

@@ -249,7 +249,7 @@ class PaymentRequest(Document):
if (
party_account_currency == ref_doc.company_currency and party_account_currency != self.currency
):
party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
party_amount = ref_doc.base_grand_total
else:
party_amount = self.grand_total

View File

@@ -8,7 +8,6 @@
"transaction_date",
"posting_date",
"fiscal_year",
"year_start_date",
"amended_from",
"company",
"column_break1",
@@ -101,22 +100,16 @@
"fieldtype": "Text",
"label": "Error Message",
"read_only": 1
},
{
"fieldname": "year_start_date",
"fieldtype": "Date",
"label": "Year Start Date"
}
],
"icon": "fa fa-file-text",
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-09-11 20:19:11.810533",
"modified": "2022-07-20 14:51:04.714154",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Period Closing Voucher",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -151,6 +144,5 @@
"search_fields": "posting_date, fiscal_year",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "closing_account_head"
}

View File

@@ -33,7 +33,7 @@ class PeriodClosingVoucher(AccountsController):
def on_cancel(self):
self.validate_future_closing_vouchers()
self.db_set("gle_processing_status", "In Progress")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
gle_count = frappe.db.count(
"GL Entry",
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
@@ -95,23 +95,15 @@ class PeriodClosingVoucher(AccountsController):
self.check_if_previous_year_closed()
pcv = frappe.qb.DocType("Period Closing Voucher")
existing_entry = (
frappe.qb.from_(pcv)
.select(pcv.name)
.where(
(pcv.posting_date >= self.posting_date)
& (pcv.fiscal_year == self.fiscal_year)
& (pcv.docstatus == 1)
& (pcv.company == self.company)
)
.run()
pce = frappe.db.sql(
"""select name from `tabPeriod Closing Voucher`
where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""",
(self.posting_date, self.fiscal_year, self.company),
)
if existing_entry and existing_entry[0][0]:
if pce and pce[0][0]:
frappe.throw(
_("Another Period Closing Entry {0} has been made after {1}").format(
existing_entry[0][0], self.posting_date
pce[0][0], self.posting_date
)
)
@@ -138,27 +130,18 @@ class PeriodClosingVoucher(AccountsController):
frappe.enqueue(
process_gl_entries,
gl_entries=gl_entries,
voucher_name=self.name,
timeout=3000,
)
frappe.enqueue(
process_closing_entries,
gl_entries=gl_entries,
closing_entries=closing_entries,
voucher_name=self.name,
company=self.company,
closing_date=self.posting_date,
timeout=3000,
queue="long",
)
frappe.msgprint(
_("The GL Entries will be processed in the background, it can take a few minutes."),
alert=True,
)
else:
process_gl_entries(gl_entries, self.name)
process_closing_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
process_gl_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
def get_grouped_gl_entries(self, get_opening_entries=False):
closing_entries = []
@@ -339,12 +322,17 @@ class PeriodClosingVoucher(AccountsController):
return query.run(as_dict=1)
def process_gl_entries(gl_entries, voucher_name):
def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries,
)
from erpnext.accounts.general_ledger import make_gl_entries
try:
if gl_entries:
make_gl_entries(gl_entries, merge_entries=False)
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
@@ -352,19 +340,6 @@ def process_gl_entries(gl_entries, voucher_name):
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
def process_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries,
)
try:
if gl_entries + closing_entries:
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
def make_reverse_gl_entries(voucher_type, voucher_no):
from erpnext.accounts.general_ledger import make_reverse_gl_entries

View File

@@ -10,7 +10,7 @@ from frappe.utils import add_months, today
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.utils import get_fiscal_year
from erpnext.accounts.utils import get_fiscal_year, now
class TestPeriodClosingVoucher(unittest.TestCase):

View File

@@ -18,7 +18,6 @@
"is_pos",
"is_return",
"update_billed_amount_in_sales_order",
"update_billed_amount_in_delivery_note",
"column_break1",
"company",
"posting_date",
@@ -1550,19 +1549,12 @@
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"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",
"is_submittable": 1,
"links": [],
"modified": "2023-11-20 12:27:12.848149",
"modified": "2022-09-30 03:49:50.455199",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",

View File

@@ -1,7 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors
# For license information, please see license.txt
import collections
import frappe
@@ -45,6 +43,7 @@ class POSInvoice(SalesInvoice):
self.validate_debit_to_acc()
self.validate_write_off_account()
self.validate_change_amount()
self.validate_duplicate_serial_and_batch_no()
self.validate_change_account()
self.validate_item_cost_centers()
self.validate_warehouse()
@@ -57,7 +56,6 @@ class POSInvoice(SalesInvoice):
self.validate_payment_amount()
self.validate_loyalty_transaction()
self.validate_company_with_pos_company()
self.validate_duplicate_serial_no()
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
@@ -158,18 +156,27 @@ class POSInvoice(SalesInvoice):
title=_("Item Unavailable"),
)
def validate_duplicate_serial_no(self):
def validate_duplicate_serial_and_batch_no(self):
serial_nos = []
batch_nos = []
for row in self.get("items"):
if row.serial_no:
serial_nos = row.serial_no.split("\n")
if row.batch_no and not row.serial_no:
batch_nos.append(row.batch_no)
if serial_nos:
for key, value in collections.Counter(serial_nos).items():
if value > 1:
frappe.throw(_("Duplicate Serial No {0} found").format("key"))
if batch_nos:
for key, value in collections.Counter(batch_nos).items():
if value > 1:
frappe.throw(_("Duplicate Batch No {0} found").format("key"))
def validate_pos_reserved_batch_qty(self, item):
filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no": item.batch_no}
@@ -518,7 +525,7 @@ class POSInvoice(SalesInvoice):
selling_price_list = (
customer_price_list or customer_group_price_list or profile.get("selling_price_list")
)
if customer_currency and customer_currency != profile.get("currency"):
if customer_currency != profile.get("currency"):
self.set("currency", customer_currency)
else:
@@ -659,7 +666,7 @@ def get_stock_availability(item_code, warehouse):
return bin_qty - pos_sales_qty, is_stock_item
else:
is_stock_item = True
if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}):
if frappe.db.exists("Product Bundle", item_code):
return get_bundle_availability(item_code, warehouse), is_stock_item
else:
is_stock_item = False

View File

@@ -464,37 +464,6 @@ class TestPOSInvoice(unittest.TestCase):
pos2.insert()
self.assertRaises(frappe.ValidationError, pos2.submit)
def test_pos_invoice_with_duplicate_serial_no(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
se = make_serialized_item(
company="_Test Company",
target_warehouse="Stores - _TC",
cost_center="Main - _TC",
expense_account="Cost of Goods Sold - _TC",
)
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
pos = create_pos_invoice(
company="_Test Company",
debit_to="Debtors - _TC",
account_for_change_amount="Cash - _TC",
warehouse="Stores - _TC",
income_account="Sales - _TC",
expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC",
item=se.get("items")[0].item_code,
rate=1000,
qty=2,
do_not_save=1,
)
pos.get("items")[0].has_serial_no = 1
pos.get("items")[0].serial_no = serial_nos[0] + "\n" + serial_nos[0]
self.assertRaises(frappe.ValidationError, pos.submit)
def test_invalid_serial_no_validation(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item

View File

@@ -185,7 +185,6 @@
"label": "Image"
},
{
"fetch_from": "item_code.image",
"fieldname": "image",
"fieldtype": "Attach",
"hidden": 1,
@@ -822,7 +821,7 @@
],
"istable": 1,
"links": [],
"modified": "2023-11-14 18:33:22.585715",
"modified": "2022-11-02 12:52:39.125295",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",
@@ -832,4 +831,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -110,7 +110,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-11-02 11:32:12.254018",
"modified": "2023-04-21 17:36:26.642617",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation Log",
@@ -125,19 +125,7 @@
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"role": "System Manager",
"share": 1,
"write": 1
}

View File

@@ -15,7 +15,6 @@
"group_by",
"cost_center",
"territory",
"ignore_exchange_rate_revaluation_journals",
"column_break_14",
"to_date",
"finance_book",
@@ -375,16 +374,10 @@
"fieldname": "pdf_name",
"fieldtype": "Data",
"label": "PDF Name"
},
{
"default": "0",
"fieldname": "ignore_exchange_rate_revaluation_journals",
"fieldtype": "Check",
"label": "Ignore Exchange Rate Revaluation Journals"
}
],
"links": [],
"modified": "2023-12-18 12:20:08.965120",
"modified": "2023-08-28 12:59:53.071334",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -47,35 +47,9 @@ class ProcessStatementOfAccounts(Document):
def get_report_pdf(doc, consolidated=True):
statement_dict = get_statement_dict(doc)
if not bool(statement_dict):
return False
elif consolidated:
delimiter = '<div style="page-break-before: always;"></div>' if doc.include_break else ""
result = delimiter.join(list(statement_dict.values()))
return get_pdf(result, {"orientation": doc.orientation})
else:
for customer, statement_html in statement_dict.items():
statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation})
return statement_dict
def get_statement_dict(doc, get_statement_dict=False):
statement_dict = {}
ageing = ""
err_journals = None
if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals:
err_journals = frappe.db.get_all(
"Journal Entry",
filters={
"company": doc.company,
"docstatus": 1,
"voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
},
as_list=True,
)
for entry in doc.customers:
if doc.include_ageing:
ageing = set_ageing(doc, entry)
@@ -88,11 +62,8 @@ def get_statement_dict(doc, get_statement_dict=False):
)
filters = get_common_filters(doc)
if err_journals:
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
if doc.report == "General Ledger":
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
col, res = get_soa(filters)
for x in [0, -2, -1]:
res[x]["account"] = res[x]["account"].replace("'", "")
@@ -105,11 +76,17 @@ def get_statement_dict(doc, get_statement_dict=False):
if not res:
continue
statement_dict[entry.customer] = (
[res, ageing] if get_statement_dict else get_html(doc, filters, entry, col, res, ageing)
)
statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing)
return statement_dict
if not bool(statement_dict):
return False
elif consolidated:
result = "".join(list(statement_dict.values()))
return get_pdf(result, {"orientation": doc.orientation})
else:
for customer, statement_html in statement_dict.items():
statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation})
return statement_dict
def set_ageing(doc, entry):
@@ -122,8 +99,7 @@ def set_ageing(doc, entry):
"range2": 60,
"range3": 90,
"range4": 120,
"party_type": "Customer",
"party": [entry.customer],
"customer": entry.customer,
}
)
col1, ageing = get_ageing(ageing_filters)
@@ -166,8 +142,7 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency):
def get_ar_filters(doc, entry):
return {
"report_date": doc.posting_date if doc.posting_date else None,
"party_type": "Customer",
"party": [entry.customer],
"customer": entry.customer,
"customer_name": entry.customer_name if entry.customer_name else None,
"payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None,
"sales_partner": doc.sales_partner if doc.sales_partner else None,

View File

@@ -4,107 +4,39 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, getdate, today
from erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts import (
get_statement_dict,
send_emails,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
class TestProcessStatementOfAccounts(AccountsTestMixin, FrappeTestCase):
class TestProcessStatementOfAccounts(unittest.TestCase):
def setUp(self):
self.create_company()
self.create_customer()
self.create_customer(customer_name="Other Customer")
self.clear_old_entries()
self.si = create_sales_invoice()
create_sales_invoice(customer="Other Customer")
def test_process_soa_for_gl(self):
"""Tests the utils for Statement of Accounts(General Ledger)"""
process_soa = create_process_soa(
name="_Test Process SOA for GL",
customers=[{"customer": "_Test Customer"}, {"customer": "Other Customer"}],
)
statement_dict = get_statement_dict(process_soa, get_statement_dict=True)
# Checks if the statements are filtered based on the Customer
self.assertIn("Other Customer", statement_dict)
self.assertIn("_Test Customer", statement_dict)
# Checks if the correct number of receivable entries exist
# 3 rows for opening and closing and 1 row for SI
receivable_entries = statement_dict["_Test Customer"][0]
self.assertEqual(len(receivable_entries), 4)
# Checks the amount for the receivable entry
self.assertEqual(receivable_entries[1].voucher_no, self.si.name)
self.assertEqual(receivable_entries[1].balance, 100)
def test_process_soa_for_ar(self):
"""Tests the utils for Statement of Accounts(Accounts Receivable)"""
process_soa = create_process_soa(name="_Test Process SOA for AR", report="Accounts Receivable")
statement_dict = get_statement_dict(process_soa, get_statement_dict=True)
# Checks if the statements are filtered based on the Customer
self.assertNotIn("Other Customer", statement_dict)
self.assertIn("_Test Customer", statement_dict)
# Checks if the correct number of receivable entries exist
receivable_entries = statement_dict["_Test Customer"][0]
self.assertEqual(len(receivable_entries), 1)
# Checks the amount for the receivable entry
self.assertEqual(receivable_entries[0].voucher_no, self.si.name)
self.assertEqual(receivable_entries[0].total_due, 100)
# Checks the ageing summary for AR
ageing_summary = statement_dict["_Test Customer"][1][0]
expected_summary = frappe._dict(
range1=100,
range2=0,
range3=0,
range4=0,
range5=0,
)
self.check_ageing_summary(ageing_summary, expected_summary)
self.process_soa = create_process_soa()
def test_auto_email_for_process_soa_ar(self):
process_soa = create_process_soa(
name="_Test Process SOA", enable_auto_email=1, report="Accounts Receivable"
)
send_emails(process_soa.name, from_scheduler=True)
process_soa.load_from_db()
self.assertEqual(process_soa.posting_date, getdate(add_days(today(), 7)))
def check_ageing_summary(self, ageing, expected_ageing):
for age_range in expected_ageing:
self.assertEqual(expected_ageing[age_range], ageing.get(age_range))
send_emails(self.process_soa.name, from_scheduler=True)
self.process_soa.load_from_db()
self.assertEqual(self.process_soa.posting_date, getdate(add_days(today(), 7)))
def tearDown(self):
frappe.db.rollback()
frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA")
def create_process_soa(**args):
args = frappe._dict(args)
frappe.delete_doc_if_exists("Process Statement Of Accounts", args.name)
def create_process_soa():
frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA")
process_soa = frappe.new_doc("Process Statement Of Accounts")
soa_dict = frappe._dict(
name=args.name,
company=args.company or "_Test Company",
customers=args.customers or [{"customer": "_Test Customer"}],
enable_auto_email=1 if args.enable_auto_email else 0,
frequency=args.frequency or "Weekly",
report=args.report or "General Ledger",
from_date=args.from_date or getdate(today()),
to_date=args.to_date or getdate(today()),
posting_date=args.posting_date or getdate(today()),
include_ageing=1,
)
soa_dict = {
"name": "Test Process SOA",
"company": "_Test Company",
}
process_soa.update(soa_dict)
process_soa.set("customers", [{"customer": "_Test Customer"}])
process_soa.enable_auto_email = 1
process_soa.frequency = "Weekly"
process_soa.report = "Accounts Receivable"
process_soa.save()
return process_soa

View File

@@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
super.onload();
// Ignore linked advances
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"];
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"];
if(!this.frm.doc.__islocal) {
// show credit_to in print format
@@ -59,25 +59,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
this.show_stock_ledger();
}
if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) {
this.frm.set_intro(__("Accounting entries for this invoice need to be reposted. Please click on 'Repost' button to update."));
this.frm.add_custom_button(__('Repost Accounting Entries'),
() => {
this.frm.call({
doc: this.frm.doc,
method: 'repost_accounting_entries',
freeze: true,
freeze_message: __('Reposting...'),
callback: (r) => {
if (!r.exc) {
frappe.msgprint(__('Accounting Entries are reposted.'));
me.frm.refresh();
}
}
});
}).removeClass('btn-default').addClass('btn-warning');
}
if(!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0){
if(doc.on_hold) {
this.frm.add_custom_button(
@@ -181,7 +162,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
}
unblock_invoice() {
@@ -480,12 +460,6 @@ cur_frm.set_query("expense_account", "items", function(doc) {
}
});
cur_frm.set_query("wip_composite_asset", "items", function() {
return {
filters: {'is_composite_asset': 1, 'docstatus': 0 }
}
});
cur_frm.cscript.expense_account = function(doc, cdt, cdn){
var d = locals[cdt][cdn];
if(d.idx == 1 && d.expense_account){

View File

@@ -36,7 +36,6 @@
"currency_and_price_list",
"currency",
"conversion_rate",
"use_transaction_date_exchange_rate",
"column_break2",
"buying_price_list",
"price_list_currency",
@@ -167,7 +166,6 @@
"against_expense_account",
"column_break_63",
"unrealized_profit_loss_account",
"repost_required",
"subscription_section",
"auto_repeat",
"update_auto_repeat_reference",
@@ -192,7 +190,8 @@
"inter_company_invoice_reference",
"is_old_subcontracting_flow",
"remarks",
"connections_tab"
"connections_tab",
"column_break_38"
],
"fields": [
{
@@ -383,8 +382,7 @@
"label": "Supplier Invoice No",
"oldfieldname": "bill_no",
"oldfieldtype": "Data",
"print_hide": 1,
"search_index": 1
"print_hide": 1
},
{
"fieldname": "column_break_15",
@@ -407,8 +405,7 @@
"no_copy": 1,
"options": "Purchase Invoice",
"print_hide": 1,
"read_only": 1,
"search_index": 1
"read_only": 1
},
{
"fieldname": "section_addresses",
@@ -990,7 +987,6 @@
"print_hide": 1
},
{
"allow_on_submit": 1,
"fieldname": "cash_bank_account",
"fieldtype": "Link",
"label": "Cash/Bank Account",
@@ -1054,7 +1050,6 @@
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"depends_on": "eval:flt(doc.write_off_amount)!=0",
"fieldname": "write_off_account",
"fieldtype": "Link",
@@ -1218,7 +1213,6 @@
"read_only": 1
},
{
"allow_on_submit": 1,
"default": "No",
"fieldname": "is_opening",
"fieldtype": "Select",
@@ -1351,7 +1345,6 @@
"options": "Project"
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.is_internal_supplier",
"description": "Unrealized Profit/Loss account for intra-company transfers",
"fieldname": "unrealized_profit_loss_account",
@@ -1384,7 +1377,6 @@
"depends_on": "eval:doc.is_subcontracted",
"fieldname": "supplier_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Supplier Warehouse",
"no_copy": 1,
"options": "Warehouse",
@@ -1502,6 +1494,10 @@
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_38",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_50",
"fieldtype": "Column Break"
@@ -1572,29 +1568,13 @@
"fieldname": "use_company_roundoff_cost_center",
"fieldtype": "Check",
"label": "Use Company Default Round Off Cost Center"
},
{
"default": "0",
"fieldname": "repost_required",
"fieldtype": "Check",
"hidden": 1,
"label": "Repost Required",
"options": "Account",
"read_only": 1
},
{
"default": "0",
"fieldname": "use_transaction_date_exchange_rate",
"fieldtype": "Check",
"label": "Use Transaction Date Exchange Rate",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2023-11-03 15:47:30.319200",
"modified": "2023-07-04 17:23:59.145031",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -11,10 +11,6 @@ from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate,
import erpnext
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.repost_accounting_ledger.repost_accounting_ledger import (
validate_docs_for_deferred_accounting,
validate_docs_for_voucher_types,
)
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
check_if_return_invoice_linked_with_payment_entry,
get_total_in_party_account_currency,
@@ -34,7 +30,7 @@ from erpnext.accounts.general_ledger import (
)
from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.accounts_controller import validate_account_head
@@ -285,6 +281,9 @@ class PurchaseInvoice(BuyingController):
# in case of auto inventory accounting,
# expense account is always "Stock Received But Not Billed" for a stock item
# except opening entry, drop-ship entry and fixed asset items
if item.item_code:
asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category")
if (
auto_accounting_for_stock
and item.item_code in stock_items
@@ -351,26 +350,22 @@ class PurchaseInvoice(BuyingController):
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
elif item.is_fixed_asset and item.pr_detail:
if not asset_received_but_not_billed:
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
item.expense_account = asset_received_but_not_billed
elif item.is_fixed_asset:
account_type = (
"capital_work_in_progress_account"
if is_cwip_accounting_enabled(item.asset_category)
else "fixed_asset_account"
)
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
asset_category_account = get_asset_category_account(
account_type, item=item.item_code, company=self.company
"fixed_asset_account", item=item.item_code, company=self.company
)
if not asset_category_account:
form_link = get_link_to_form("Asset Category", item.asset_category)
form_link = get_link_to_form("Asset Category", asset_category)
throw(
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
title=_("Missing Account"),
)
item.expense_account = asset_category_account
elif item.is_fixed_asset and item.pr_detail:
if not asset_received_but_not_billed:
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
item.expense_account = asset_received_but_not_billed
elif not item.expense_account and for_validate:
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
@@ -492,12 +487,6 @@ class PurchaseInvoice(BuyingController):
_("Stock cannot be updated against Purchase Receipt {0}").format(item.purchase_receipt)
)
def validate_for_repost(self):
self.validate_write_off_account()
self.validate_expense_account()
validate_docs_for_voucher_types(["Purchase Invoice"])
validate_docs_for_deferred_accounting([], [self.name])
def on_submit(self):
super(PurchaseInvoice, self).on_submit()
@@ -534,29 +523,12 @@ class PurchaseInvoice(BuyingController):
if self.update_stock == 1:
self.repost_future_sle_and_gle()
if (
frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction"
):
self.update_project()
self.update_project()
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.update_advance_tax_references()
self.process_common_party_accounting()
def on_update_after_submit(self):
if hasattr(self, "repost_required"):
fields_to_check = [
"cash_bank_account",
"write_off_account",
"unrealized_profit_loss_account",
]
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
if self.needs_repost:
self.validate_for_repost()
self.db_set("repost_required", self.needs_repost)
def make_gl_entries(self, gl_entries=None, from_repost=False):
if not gl_entries:
gl_entries = self.get_gl_entries()
@@ -598,11 +570,12 @@ class PurchaseInvoice(BuyingController):
def get_gl_entries(self, warehouse_account=None):
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if self.auto_accounting_for_stock:
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
else:
self.stock_received_but_not_billed = None
self.expenses_included_in_valuation = None
self.negative_expense_to_be_booked = 0.0
gl_entries = []
@@ -611,6 +584,9 @@ class PurchaseInvoice(BuyingController):
self.make_item_gl_entries(gl_entries)
self.make_precision_loss_gl_entry(gl_entries)
if self.check_asset_cwip_enabled():
self.get_asset_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
@@ -714,11 +690,7 @@ class PurchaseInvoice(BuyingController):
if item.item_code:
asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category")
if (
self.update_stock
and self.auto_accounting_for_stock
and (item.item_code in stock_items or item.is_fixed_asset)
):
if self.update_stock and self.auto_accounting_for_stock and item.item_code in stock_items:
# warehouse account
warehouse_debit_amount = self.make_stock_adjustment_entry(
gl_entries, item, voucher_wise_stock_value, account_currency
@@ -833,7 +805,9 @@ class PurchaseInvoice(BuyingController):
)
)
else:
elif not item.is_fixed_asset or (
item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category)
):
expense_account = (
item.expense_account
if (not item.enable_deferred_expense or self.is_return)
@@ -926,6 +900,40 @@ class PurchaseInvoice(BuyingController):
)
)
# If asset is bought through this document and not linked to PR
if self.update_stock and item.landed_cost_voucher_amount:
expenses_included_in_asset_valuation = self.get_company_default(
"expenses_included_in_asset_valuation"
)
# Amount added through landed-cost-voucher
gl_entries.append(
self.get_gl_dict(
{
"account": expenses_included_in_asset_valuation,
"against": expense_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(item.landed_cost_voucher_amount),
"project": item.project or self.project,
},
item=item,
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": expense_account,
"against": expenses_included_in_asset_valuation,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": flt(item.landed_cost_voucher_amount),
"project": item.project or self.project,
},
item=item,
)
)
# update gross amount of asset bought through this document
assets = frappe.db.get_all(
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
@@ -950,17 +958,11 @@ class PurchaseInvoice(BuyingController):
(item.purchase_receipt, valuation_tax_accounts),
)
stock_rbnb = (
self.get_company_default("asset_received_but_not_billed")
if item.is_fixed_asset
else self.stock_received_but_not_billed
)
if not negative_expense_booked_in_pr:
gl_entries.append(
self.get_gl_dict(
{
"account": stock_rbnb,
"account": self.stock_received_but_not_billed,
"against": self.supplier,
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
"remarks": self.remarks or _("Accounting Entry for Stock"),
@@ -975,12 +977,156 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount, item.precision("item_tax_amount")
)
assets = frappe.db.get_all(
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
)
for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
def get_asset_gl_entry(self, gl_entries):
arbnb_account = None
eiiav_account = None
asset_eiiav_currency = None
for item in self.get("items"):
if item.is_fixed_asset:
asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate)
base_asset_amount = flt(item.base_net_amount + item.item_tax_amount)
item_exp_acc_type = frappe.db.get_value("Account", item.expense_account, "account_type")
if not item.expense_account or item_exp_acc_type not in [
"Asset Received But Not Billed",
"Fixed Asset",
]:
if not arbnb_account:
arbnb_account = self.get_company_default("asset_received_but_not_billed")
item.expense_account = arbnb_account
if not self.update_stock:
arbnb_currency = get_account_currency(item.expense_account)
gl_entries.append(
self.get_gl_dict(
{
"account": item.expense_account,
"against": self.supplier,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"debit": base_asset_amount,
"debit_in_account_currency": (
base_asset_amount if arbnb_currency == self.company_currency else asset_amount
),
"cost_center": item.cost_center,
"project": item.project or self.project,
},
item=item,
)
)
if item.item_tax_amount:
if not eiiav_account or not asset_eiiav_currency:
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
asset_eiiav_currency = get_account_currency(eiiav_account)
gl_entries.append(
self.get_gl_dict(
{
"account": eiiav_account,
"against": self.supplier,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"cost_center": item.cost_center,
"project": item.project or self.project,
"credit": item.item_tax_amount,
"credit_in_account_currency": (
item.item_tax_amount
if asset_eiiav_currency == self.company_currency
else item.item_tax_amount / self.conversion_rate
),
},
item=item,
)
)
else:
cwip_account = get_asset_account(
"capital_work_in_progress_account", asset_category=item.asset_category, company=self.company
)
cwip_account_currency = get_account_currency(cwip_account)
gl_entries.append(
self.get_gl_dict(
{
"account": cwip_account,
"against": self.supplier,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"debit": base_asset_amount,
"debit_in_account_currency": (
base_asset_amount if cwip_account_currency == self.company_currency else asset_amount
),
"cost_center": self.cost_center,
"project": item.project or self.project,
},
item=item,
)
)
if item.item_tax_amount and not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
if not eiiav_account or not asset_eiiav_currency:
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
asset_eiiav_currency = get_account_currency(eiiav_account)
gl_entries.append(
self.get_gl_dict(
{
"account": eiiav_account,
"against": self.supplier,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"cost_center": item.cost_center,
"credit": item.item_tax_amount,
"project": item.project or self.project,
"credit_in_account_currency": (
item.item_tax_amount
if asset_eiiav_currency == self.company_currency
else item.item_tax_amount / self.conversion_rate
),
},
item=item,
)
)
# Assets are bought through this document then it will be linked to this document
if flt(item.landed_cost_voucher_amount):
if not eiiav_account:
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
gl_entries.append(
self.get_gl_dict(
{
"account": eiiav_account,
"against": cwip_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(item.landed_cost_voucher_amount),
"project": item.project or self.project,
},
item=item,
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": cwip_account,
"against": eiiav_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": flt(item.landed_cost_voucher_amount),
"project": item.project or self.project,
},
item=item,
)
)
# update gross amount of assets bought through this document
assets = frappe.db.get_all(
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
)
for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
return gl_entries
def make_stock_adjustment_entry(
self, gl_entries, item, voucher_wise_stock_value, account_currency
@@ -1275,10 +1421,7 @@ class PurchaseInvoice(BuyingController):
if self.update_stock == 1:
self.repost_future_sle_and_gle()
if (
frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction"
):
self.update_project()
self.update_project()
self.db_set("status", "Cancelled")
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
@@ -1290,29 +1433,19 @@ class PurchaseInvoice(BuyingController):
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
"Unreconcile Payment",
"Unreconcile Payment Entries",
"Payment Ledger Entry",
"Tax Withheld Vouchers",
)
self.update_advance_tax_references(cancel=1)
def update_project(self):
projects = frappe._dict()
project_list = []
for d in self.items:
if d.project:
if self.docstatus == 1:
projects[d.project] = projects.get(d.project, 0) + d.base_net_amount
elif self.docstatus == 2:
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)
if d.project and d.project not in project_list:
project = frappe.get_doc("Project", d.project)
project.update_purchase_costing()
project.db_update()
project_list.append(d.project)
def validate_supplier_invoice(self):
if self.bill_date:
@@ -1695,7 +1828,6 @@ def make_purchase_receipt(source_name, target_doc=None):
"po_detail": "purchase_order_item",
"material_request": "material_request",
"material_request_item": "material_request_item",
"wip_composite_asset": "wip_composite_asset",
},
"postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty),
@@ -1705,4 +1837,6 @@ def make_purchase_receipt(source_name, target_doc=None):
target_doc,
)
doc.set_onload("ignore_price_list", True)
return doc

View File

@@ -5,7 +5,7 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.tests.utils import change_settings
from frappe.utils import add_days, cint, flt, getdate, nowdate, today
import erpnext
@@ -33,7 +33,7 @@ test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Templ
test_ignore = ["Serial No"]
class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
@classmethod
def setUpClass(self):
unlink_payment_on_cancel_of_invoice()
@@ -43,9 +43,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
def tearDownClass(self):
unlink_payment_on_cancel_of_invoice(0)
def tearDown(self):
frappe.db.rollback()
def test_purchase_invoice_received_qty(self):
"""
1. Test if received qty is validated against accepted + rejected
@@ -420,7 +417,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertEqual(tax.tax_amount, expected_values[i][1])
self.assertEqual(tax.total, expected_values[i][2])
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_purchase_invoice_with_advance(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
test_records as jv_test_records,
@@ -475,7 +471,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
)
)
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_invoice_with_advance_and_multi_payment_terms(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
test_records as jv_test_records,
@@ -1214,7 +1209,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
acc_settings.submit_journal_entriessubmit_journal_entries = 0
acc_settings.save()
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_gain_loss_with_advance_entry(self):
unlink_enabled = frappe.db.get_value(
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
@@ -1417,7 +1411,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
)
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_purchase_invoice_advance_taxes(self):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@@ -1718,14 +1711,9 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertTrue(return_pi.docstatus == 1)
def test_gl_entries_for_standalone_debit_note(self):
from erpnext.stock.doctype.item.test_item import make_item
make_purchase_invoice(qty=5, rate=500, update_stock=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
)
returned_inv = make_purchase_invoice(qty=-5, rate=5, update_stock=True, is_return=True)
# override the rate with valuation rate
sle = frappe.get_all(
@@ -1735,7 +1723,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
)[0]
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
self.assertAlmostEqual(rate, 500)
self.assertAlmostEqual(returned_inv.items[0].rate, rate)
def test_payment_allocation_for_payment_terms(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
@@ -1808,6 +1796,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
pi = make_purchase_invoice(
company="_Test Company",
customer="_Test Supplier",
do_not_save=True,
do_not_submit=True,
rate=1000,
@@ -1837,62 +1826,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
clear_dimension_defaults("Branch")
disable_dimension()
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(
rate=1000,
price_list_rate=1000,
qty=1,
)
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate()],
["Creditors - _TC", 0.0, 1000, nowdate()],
]
check_gl_entries(self, pi.name, expected_gle, nowdate())
pi.items[0].expense_account = "Service - _TC"
pi.save()
pi.load_from_db()
self.assertTrue(pi.repost_required)
pi.repost_accounting_entries()
expected_gle = [
["Creditors - _TC", 0.0, 1000, nowdate()],
["Service - _TC", 1000, 0.0, nowdate()],
]
check_gl_entries(self, pi.name, expected_gle, nowdate())
pi.load_from_db()
self.assertFalse(pi.repost_required)
def test_default_cost_center_for_purchase(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
for c_center in ["_Test Cost Center Selling", "_Test Cost Center Buying"]:
create_cost_center(cost_center_name=c_center)
item = create_item(
"_Test Cost Center Item For Purchase",
is_stock_item=1,
buying_cost_center="_Test Cost Center Buying - _TC",
selling_cost_center="_Test Cost Center Selling - _TC",
)
pi = make_purchase_invoice(
item=item.name, qty=1, rate=1000, update_stock=True, do_not_submit=True, cost_center=""
)
pi.items[0].cost_center = ""
pi.set_missing_values()
pi.calculate_taxes_and_totals()
pi.save()
self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC")
def check_gl_entries(
doc,

View File

@@ -75,7 +75,6 @@
"manufacturer_part_no",
"accounting",
"expense_account",
"wip_composite_asset",
"col_break5",
"is_fixed_asset",
"asset_location",
@@ -156,7 +155,6 @@
"width": "300px"
},
{
"fetch_from": "item_code.image",
"fieldname": "image",
"fieldtype": "Attach",
"hidden": 1,
@@ -286,7 +284,6 @@
"oldfieldname": "import_rate",
"oldfieldtype": "Currency",
"options": "currency",
"read_only_depends_on": "eval: (!parent.is_return && doc.purchase_receipt && doc.pr_detail)",
"reqd": 1
},
{
@@ -470,7 +467,6 @@
"label": "Accounting"
},
{
"allow_on_submit": 1,
"fieldname": "expense_account",
"fieldtype": "Link",
"label": "Expense Head",
@@ -493,7 +489,6 @@
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
@@ -501,7 +496,6 @@
"print_hide": 1
},
{
"allow_on_submit": 1,
"default": ":Company",
"depends_on": "eval:!doc.is_fixed_asset",
"fieldname": "cost_center",
@@ -883,18 +877,12 @@
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Apply TDS"
},
{
"fieldname": "wip_composite_asset",
"fieldtype": "Link",
"label": "WIP Composite Asset",
"options": "Asset"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-11-30 16:26:05.629780",
"modified": "2023-07-04 17:22:21.501152",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
@@ -904,4 +892,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -86,7 +86,6 @@
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"columns": 2,
"fieldname": "account_head",
"fieldtype": "Link",
@@ -98,7 +97,6 @@
"reqd": 1
},
{
"allow_on_submit": 1,
"default": ":Company",
"fieldname": "cost_center",
"fieldtype": "Link",

View File

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

View File

@@ -55,7 +55,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-09-26 14:21:27.362567",
"modified": "2023-07-27 15:47:58.975034",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Accounting Ledger",
@@ -77,6 +77,5 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
"states": []
}

View File

@@ -10,7 +10,9 @@ from frappe.utils.data import comma_and
class RepostAccountingLedger(Document):
def __init__(self, *args, **kwargs):
super(RepostAccountingLedger, self).__init__(*args, **kwargs)
self._allowed_types = get_allowed_types_from_settings()
self._allowed_types = set(
["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"]
)
def validate(self):
self.validate_vouchers()
@@ -19,8 +21,29 @@ class RepostAccountingLedger(Document):
def validate_for_deferred_accounting(self):
sales_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Sales Invoice"]
docs_with_deferred_revenue = frappe.db.get_all(
"Sales Invoice Item",
filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True},
fields=["parent"],
as_list=1,
)
purchase_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Purchase Invoice"]
validate_docs_for_deferred_accounting(sales_docs, purchase_docs)
docs_with_deferred_expense = frappe.db.get_all(
"Purchase Invoice Item",
filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1},
fields=["parent"],
as_list=1,
)
if docs_with_deferred_revenue or docs_with_deferred_expense:
frappe.throw(
_("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format(
frappe.bold(
comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue])
)
)
)
def validate_for_closed_fiscal_year(self):
if self.vouchers:
@@ -51,7 +74,15 @@ class RepostAccountingLedger(Document):
def validate_vouchers(self):
if self.vouchers:
validate_docs_for_voucher_types([x.voucher_type for x in self.vouchers])
# Validate voucher types
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):
vouchers = [x.voucher_no for x in self.vouchers]
@@ -108,17 +139,14 @@ class RepostAccountingLedger(Document):
return rendered_page
def on_submit(self):
if len(self.vouchers) > 5:
job_name = "repost_accounting_ledger_" + self.name
frappe.enqueue(
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
account_repost_doc=self.name,
is_async=True,
job_name=job_name,
)
frappe.msgprint(_("Repost has started in the background"))
else:
start_repost(self.name)
job_name = "repost_accounting_ledger_" + self.name
frappe.enqueue(
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
account_repost_doc=self.name,
is_async=True,
job_name=job_name,
)
frappe.msgprint(_("Repost has started in the background"))
@frappe.whitelist()
@@ -147,73 +175,9 @@ def start_repost(account_repost_doc=str) -> None:
doc.docstatus = 1
doc.make_gl_entries()
elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
elif doc.doctype in ["Payment Entry", "Journal Entry"]:
if not repost_doc.delete_cancelled_entries:
doc.make_gl_entries(1)
doc.make_gl_entries()
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):
docs_with_deferred_revenue = frappe.db.get_all(
"Sales Invoice Item",
filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True},
fields=["parent"],
as_list=1,
)
docs_with_deferred_expense = frappe.db.get_all(
"Purchase Invoice Item",
filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1},
fields=["parent"],
as_list=1,
)
if docs_with_deferred_revenue or docs_with_deferred_expense:
frappe.throw(
_("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format(
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 []
frappe.db.commit()

View File

@@ -20,9 +20,8 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
self.create_company()
self.create_customer()
self.create_item()
update_repost_settings()
def tearDown(self):
def teadDown(self):
frappe.db.rollback()
def test_01_basic_functions(self):
@@ -83,6 +82,9 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
# Submit repost document
ral.save().submit()
# background jobs don't run on test cases. Manually triggering repost function.
start_repost(ral.name)
res = (
qb.from_(gl)
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
@@ -167,6 +169,26 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
pe = get_payment_entry(si.doctype, si.name)
pe.save().submit()
# without deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
ral.delete_cancelled_entries = False
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
ral.save()
# assert preview data is generated
preview = ral.generate_preview()
self.assertIsNotNone(preview)
ral.save().submit()
# background jobs don't run on test cases. Manually triggering repost function.
start_repost(ral.name)
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
# with deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
@@ -175,38 +197,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
ral.save().submit()
start_repost(ral.name)
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
def test_05_without_deletion_flag(self):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
)
pe = get_payment_entry(si.doctype, si.name)
pe.save().submit()
# without deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
ral.delete_cancelled_entries = False
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
ral.save().submit()
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
def update_repost_settings():
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()

View File

@@ -1,8 +0,0 @@
// 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

@@ -1,46 +0,0 @@
{
"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

@@ -1,9 +0,0 @@
# 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

@@ -1,9 +0,0 @@
# 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

@@ -1,45 +0,0 @@
{
"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

@@ -1,9 +0,0 @@
# 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

@@ -99,7 +99,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-09-26 14:21:35.719727",
"modified": "2022-11-08 07:38:40.079038",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Payment Ledger",
@@ -155,6 +155,5 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
"states": []
}

View File

@@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
super.onload();
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 Payment", "Unreconcile Payment Entries"];
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format
@@ -177,8 +177,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
}, __('Create'));
}
}
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
}
make_maintenance_schedule() {
@@ -556,6 +554,15 @@ 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
// -----------------------------
cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function(doc) {
@@ -650,16 +657,6 @@ 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 = {
'Delivery Note': 'Delivery',
'Sales Invoice': 'Return / Credit Note',

View File

@@ -26,7 +26,6 @@
"is_return",
"return_against",
"update_billed_amount_in_sales_order",
"update_billed_amount_in_delivery_note",
"is_debit_note",
"amended_from",
"accounting_dimensions_section",
@@ -1613,8 +1612,7 @@
"hide_seconds": 1,
"label": "Inter Company Invoice Reference",
"options": "Purchase Invoice",
"read_only": 1,
"search_index": 1
"read_only": 1
},
{
"fieldname": "customer_group",
@@ -2146,13 +2144,6 @@
"fieldname": "use_company_roundoff_cost_center",
"fieldtype": "Check",
"label": "Use Company default Cost Center for Round off"
},
{
"default": "1",
"depends_on": "eval: doc.is_return",
"fieldname": "update_billed_amount_in_delivery_note",
"fieldtype": "Check",
"label": "Update Billed Amount in Delivery Note"
}
],
"icon": "fa fa-file-text",
@@ -2165,7 +2156,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2023-11-23 16:56:29.679499",
"modified": "2023-06-19 16:02:05.309332",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -11,14 +11,13 @@ from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form
import erpnext
from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
get_loyalty_program_details_with_points,
validate_loyalty_points,
)
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
validate_docs_for_deferred_accounting,
validate_docs_for_voucher_types,
)
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
@@ -177,13 +176,6 @@ class SalesInvoice(SellingController):
self.validate_account_for_change_amount()
self.validate_income_account()
def validate_for_repost(self):
self.validate_write_off_account()
self.validate_account_for_change_amount()
self.validate_income_account()
validate_docs_for_voucher_types(["Sales Invoice"])
validate_docs_for_deferred_accounting([self.name], [])
def validate_fixed_asset(self):
for d in self.get("items"):
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
@@ -263,7 +255,6 @@ class SalesInvoice(SellingController):
self.update_status_updater_args()
self.update_prevdoc_status()
self.update_billing_status_in_dn()
self.clear_unallocated_mode_of_payments()
@@ -410,8 +401,6 @@ class SalesInvoice(SellingController):
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
"Unreconcile Payment",
"Unreconcile Payment Entries",
"Payment Ledger Entry",
)
@@ -538,22 +527,89 @@ class SalesInvoice(SellingController):
def on_update_after_submit(self):
if hasattr(self, "repost_required"):
fields_to_check = [
"additional_discount_account",
"cash_bank_account",
"account_for_change_amount",
"write_off_account",
"loyalty_redemption_account",
"unrealized_profit_loss_account",
]
child_tables = {
"items": ("income_account", "expense_account", "discount_account"),
"taxes": ("account_head",),
}
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
if self.needs_repost:
self.validate_for_repost()
self.db_set("repost_required", self.needs_repost)
needs_repost = 0
# Check if any field affecting accounting entry is altered
doc_before_update = self.get_doc_before_save()
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
# Check if opening entry check updated
if doc_before_update.get("is_opening") != self.is_opening:
needs_repost = 1
if not needs_repost:
# Parent Level Accounts excluding party account
for field in (
"additional_discount_account",
"cash_bank_account",
"account_for_change_amount",
"write_off_account",
"loyalty_redemption_account",
"unrealized_profit_loss_account",
):
if doc_before_update.get(field) != self.get(field):
needs_repost = 1
break
# Check for parent accounting dimensions
for dimension in accounting_dimensions:
if doc_before_update.get(dimension) != self.get(dimension):
needs_repost = 1
break
# Check for child tables
if self.check_if_child_table_updated(
"items",
doc_before_update,
("income_account", "expense_account", "discount_account"),
accounting_dimensions,
):
needs_repost = 1
if self.check_if_child_table_updated(
"taxes", doc_before_update, ("account_head",), accounting_dimensions
):
needs_repost = 1
self.validate_accounts()
# validate if deferred revenue is enabled for any item
# Don't allow to update the invoice if deferred revenue is enabled
for item in self.get("items"):
if item.enable_deferred_revenue:
frappe.throw(
_(
"Deferred Revenue is enabled for item {0}. You cannot update the invoice after submission."
).format(item.item_code)
)
self.db_set("repost_required", needs_repost)
def check_if_child_table_updated(
self, child_table, doc_before_update, fields_to_check, accounting_dimensions
):
# Check if any field affecting accounting entry is altered
for index, item in enumerate(self.get(child_table)):
for field in fields_to_check:
if doc_before_update.get(child_table)[index].get(field) != item.get(field):
return True
for dimension in accounting_dimensions:
if doc_before_update.get(child_table)[index].get(dimension) != item.get(dimension):
return True
return False
@frappe.whitelist()
def repost_accounting_entries(self):
if self.repost_required:
self.docstatus = 2
self.make_gl_entries_on_cancel()
self.docstatus = 1
self.make_gl_entries()
self.db_set("repost_required", 0)
else:
frappe.throw(_("No updates pending for reposting"))
def set_paid_amount(self):
paid_amount = 0.0
@@ -1039,7 +1095,7 @@ class SalesInvoice(SellingController):
def make_customer_gl_entry(self, gl_entries):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introduction of posting GLE based on rounded total
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = (
self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
)
@@ -1274,7 +1330,7 @@ class SalesInvoice(SellingController):
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
payment_mode.base_amount -= flt(self.change_amount)
if payment_mode.base_amount:
if payment_mode.amount:
# POS, make payment entries
gl_entries.append(
self.get_gl_dict(
@@ -1436,8 +1492,6 @@ class SalesInvoice(SellingController):
)
def update_billing_status_in_dn(self, update_modified=True):
if self.is_return and not self.update_billed_amount_in_delivery_note:
return
updated_delivery_notes = []
for d in self.get("items"):
if d.dn_detail:
@@ -1934,6 +1988,7 @@ def make_delivery_note(source_name, target_doc=None):
set_missing_values,
)
doclist.set_onload("ignore_price_list", True)
return doclist
@@ -2195,18 +2250,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
def get_received_items(reference_name, doctype, reference_fieldname):
reference_field = "inter_company_invoice_reference"
if doctype == "Purchase Order":
reference_field = "inter_company_order_reference"
filters = {
reference_field: reference_name,
"docstatus": 1,
}
target_doctypes = frappe.get_all(
doctype,
filters=filters,
filters={"inter_company_invoice_reference": reference_name, "docstatus": 1},
as_list=True,
)

View File

@@ -7,7 +7,7 @@ import unittest
import frappe
from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.model.naming import make_autoname
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.tests.utils import change_settings
from frappe.utils import add_days, flt, getdate, nowdate, today
import erpnext
@@ -38,17 +38,13 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
class TestSalesInvoice(FrappeTestCase):
class TestSalesInvoice(unittest.TestCase):
def setUp(self):
from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items
create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}])
create_internal_parties()
setup_accounts()
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
def tearDown(self):
frappe.db.rollback()
def make(self):
w = frappe.copy_doc(test_records[0])
@@ -176,7 +172,6 @@ class TestSalesInvoice(FrappeTestCase):
self.assertRaises(frappe.LinkExistsError, si.cancel)
unlink_payment_on_cancel_of_invoice()
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_payment_entry_unlink_against_standalone_credit_note(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
@@ -782,28 +777,6 @@ class TestSalesInvoice(FrappeTestCase):
w = self.make()
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):
w = self.make()
@@ -1320,7 +1293,6 @@ class TestSalesInvoice(FrappeTestCase):
dn.submit()
return dn
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_sales_invoice_with_advance(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
test_records as jv_test_records,
@@ -1829,10 +1801,6 @@ class TestSalesInvoice(FrappeTestCase):
)
def test_outstanding_amount_after_advance_payment_entry_cancellation(self):
"""Test impact of advance PE submission/cancellation on SI and SO."""
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
sales_order = make_sales_order(item_code="138-CMS Shoe", qty=1, price_list_rate=500)
pe = frappe.get_doc(
{
"doctype": "Payment Entry",
@@ -1852,25 +1820,10 @@ class TestSalesInvoice(FrappeTestCase):
"paid_to": "_Test Cash - _TC",
}
)
pe.append(
"references",
{
"reference_doctype": "Sales Order",
"reference_name": sales_order.name,
"total_amount": sales_order.grand_total,
"outstanding_amount": sales_order.grand_total,
"allocated_amount": 300,
},
)
pe.insert()
pe.submit()
sales_order.reload()
self.assertEqual(sales_order.advance_paid, 300)
si = frappe.copy_doc(test_records[0])
si.items[0].sales_order = sales_order.name
si.items[0].so_detail = sales_order.get("items")[0].name
si.is_pos = 0
si.append(
"advances",
@@ -1878,7 +1831,6 @@ class TestSalesInvoice(FrappeTestCase):
"doctype": "Sales Invoice Advance",
"reference_type": "Payment Entry",
"reference_name": pe.name,
"reference_row": pe.references[0].name,
"advance_amount": 300,
"allocated_amount": 300,
"remarks": pe.remarks,
@@ -1887,13 +1839,7 @@ class TestSalesInvoice(FrappeTestCase):
si.insert()
si.submit()
si.reload()
pe.reload()
sales_order.reload()
# Check if SO is unlinked/replaced by SI in PE & if SO advance paid is 0
self.assertEqual(pe.references[0].reference_name, si.name)
self.assertEqual(sales_order.advance_paid, 0.0)
si.load_from_db()
# check outstanding after advance allocation
self.assertEqual(
@@ -1901,9 +1847,11 @@ class TestSalesInvoice(FrappeTestCase):
flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")),
)
# added to avoid Document has been modified exception
pe = frappe.get_doc("Payment Entry", pe.name)
pe.cancel()
si.reload()
si.load_from_db()
# check outstanding after advance cancellation
self.assertEqual(
flt(si.outstanding_amount),
@@ -2519,6 +2467,12 @@ class TestSalesInvoice(FrappeTestCase):
"stock_received_but_not_billed",
"Stock Received But Not Billed - _TC1",
)
frappe.db.set_value(
"Company",
"_Test Company 1",
"expenses_included_in_valuation",
"Expenses Included In Valuation - _TC1",
)
# begin test
si = create_sales_invoice(
@@ -2556,7 +2510,7 @@ class TestSalesInvoice(FrappeTestCase):
# tear down
frappe.local.enable_perpetual_inventory["_Test Company 1"] = old_perpetual_inventory
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", old_negative_stock)
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", old_negative_stock)
def test_sle_for_target_warehouse(self):
se = make_stock_entry(
@@ -2568,7 +2522,6 @@ class TestSalesInvoice(FrappeTestCase):
)
si = frappe.copy_doc(test_records[0])
si.customer = "_Test Internal Customer 3"
si.update_stock = 1
si.set_warehouse = "Finished Goods - _TC"
si.set_target_warehouse = "Stores - _TC"
@@ -2791,25 +2744,12 @@ class TestSalesInvoice(FrappeTestCase):
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self):
from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import (
update_repost_settings,
)
update_repost_settings()
additional_discount_account = create_account(
account_name="Discount Account",
parent_account="Indirect Expenses - _TC",
company="_Test Company",
)
tds_payable_account = create_account(
account_name="TDS Payable",
account_type="Tax",
parent_account="Duties and Taxes - _TC",
company="_Test Company",
)
si = create_sales_invoice(parent_cost_center="Main - _TC", do_not_save=1)
si.apply_discount_on = "Grand Total"
si.additional_discount_account = additional_discount_account
@@ -3108,8 +3048,8 @@ class TestSalesInvoice(FrappeTestCase):
si.commission_rate = commission_rate
self.assertRaises(frappe.ValidationError, si.save)
@change_settings("Accounts Settings", {"acc_frozen_upto": add_days(getdate(), 1)})
def test_sales_invoice_submission_post_account_freezing_date(self):
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", add_days(getdate(), 1))
si = create_sales_invoice(do_not_save=True)
si.posting_date = add_days(getdate(), 1)
si.save()
@@ -3118,6 +3058,8 @@ class TestSalesInvoice(FrappeTestCase):
si.posting_date = getdate()
si.submit()
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None)
def test_over_billing_case_against_delivery_note(self):
"""
Test a case where duplicating the item with qty = 1 in the invoice
@@ -3146,13 +3088,6 @@ class TestSalesInvoice(FrappeTestCase):
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", over_billing_allowance)
@change_settings(
"Accounts Settings",
{
"book_deferred_entries_via_journal_entry": 1,
"submit_journal_entries": 1,
},
)
def test_multi_currency_deferred_revenue_via_journal_entry(self):
deferred_account = create_account(
account_name="Deferred Revenue",
@@ -3160,6 +3095,11 @@ class TestSalesInvoice(FrappeTestCase):
company="_Test Company",
)
acc_settings = frappe.get_single("Accounts Settings")
acc_settings.book_deferred_entries_via_journal_entry = 1
acc_settings.submit_journal_entries = 1
acc_settings.save()
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_expense = 1
item.item_defaults[0].deferred_revenue_account = deferred_account
@@ -3225,6 +3165,13 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(expected_gle[i][2], gle.debit)
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
acc_settings = frappe.get_single("Accounts Settings")
acc_settings.book_deferred_entries_via_journal_entry = 0
acc_settings.submit_journal_entries = 0
acc_settings.save()
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None)
def test_standalone_serial_no_return(self):
si = create_sales_invoice(
item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1
@@ -3602,20 +3549,6 @@ def create_internal_parties():
allowed_to_interact_with="_Test Company with perpetual inventory",
)
create_internal_customer(
customer_name="_Test Internal Customer 3",
represents_company="_Test Company",
allowed_to_interact_with="_Test Company",
)
account = create_account(
account_name="Unrealized Profit",
parent_account="Current Liabilities - _TC",
company="_Test Company",
)
frappe.db.set_value("Company", "_Test Company", "unrealized_profit_loss_account", account)
create_internal_supplier(
supplier_name="_Test Internal Supplier",
represents_company="Wind Power LLC",

View File

@@ -167,7 +167,6 @@
"print_hide": 1
},
{
"fetch_from": "item_code.image",
"fieldname": "image",
"fieldtype": "Attach",
"hidden": 1,
@@ -892,7 +891,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-11-14 18:34:10.479329",
"modified": "2023-07-25 11:58:10.723833",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
@@ -902,4 +901,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -18,14 +18,6 @@ frappe.ui.form.on('Subscription', {
}
};
});
frm.set_query('sales_tax_template', function () {
return {
filters: {
company: frm.doc.company
}
};
});
},
refresh: function(frm) {

View File

@@ -4,7 +4,6 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils.data import (
add_days,
add_months,
@@ -91,14 +90,10 @@ def create_parties():
customer.insert()
class TestSubscription(FrappeTestCase):
class TestSubscription(unittest.TestCase):
def setUp(self):
create_plan()
create_parties()
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
def tearDown(self):
frappe.db.rollback()
def test_create_subscription_with_trial_with_correct_period(self):
subscription = frappe.new_doc("Subscription")

View File

@@ -1,316 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import today
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.create_customer()
self.create_usd_receivable_account()
self.create_item()
self.clear_old_entries()
def tearDown(self):
frappe.db.rollback()
def create_sales_invoice(self, do_not_submit=False):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
price_list_rate=100,
do_not_submit=do_not_submit,
)
return si
def create_payment_entry(self):
pe = create_payment_entry(
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer,
paid_from=self.debit_to,
paid_to=self.cash,
paid_amount=200,
save=True,
)
return pe
def test_01_unreconcile_invoice(self):
si1 = self.create_sales_invoice()
si2 = self.create_sales_invoice()
pe = self.create_payment_entry()
pe.append(
"references",
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100},
)
pe.append(
"references",
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100},
)
# Allocation payment against both invoices
pe.save().submit()
# Assert outstanding
[doc.reload() for doc in [si1, si2, pe]]
self.assertEqual(si1.outstanding_amount, 0)
self.assertEqual(si2.outstanding_amount, 0)
self.assertEqual(pe.unallocated_amount, 0)
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payment",
"company": self.company,
"voucher_type": pe.doctype,
"voucher_no": pe.name,
}
)
unreconcile.add_references()
self.assertEqual(len(unreconcile.allocations), 2)
allocations = [x.reference_name for x in unreconcile.allocations]
self.assertEquals([si1.name, si2.name], allocations)
# unreconcile si1
for x in unreconcile.allocations:
if x.reference_name != si1.name:
unreconcile.remove(x)
unreconcile.save().submit()
# Assert outstanding
[doc.reload() for doc in [si1, si2, pe]]
self.assertEqual(si1.outstanding_amount, 100)
self.assertEqual(si2.outstanding_amount, 0)
self.assertEqual(len(pe.references), 1)
self.assertEqual(pe.unallocated_amount, 100)
def test_02_unreconcile_one_payment_from_multi_payments(self):
"""
Scenario: 2 payments, both split against 2 different invoices
Unreconcile only one payment from one invoice
"""
si1 = self.create_sales_invoice()
si2 = self.create_sales_invoice()
pe1 = self.create_payment_entry()
pe1.paid_amount = 100
# Allocate payment against both invoices
pe1.append(
"references",
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
)
pe1.append(
"references",
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
)
pe1.save().submit()
pe2 = self.create_payment_entry()
pe2.paid_amount = 100
# Allocate payment against both invoices
pe2.append(
"references",
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
)
pe2.append(
"references",
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
)
pe2.save().submit()
# Assert outstanding and unallocated
[doc.reload() for doc in [si1, si2, pe1, pe2]]
self.assertEqual(si1.outstanding_amount, 0.0)
self.assertEqual(si2.outstanding_amount, 0.0)
self.assertEqual(pe1.unallocated_amount, 0.0)
self.assertEqual(pe2.unallocated_amount, 0.0)
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payment",
"company": self.company,
"voucher_type": pe2.doctype,
"voucher_no": pe2.name,
}
)
unreconcile.add_references()
self.assertEqual(len(unreconcile.allocations), 2)
allocations = [x.reference_name for x in unreconcile.allocations]
self.assertEquals([si1.name, si2.name], allocations)
# unreconcile si1 from pe2
for x in unreconcile.allocations:
if x.reference_name != si1.name:
unreconcile.remove(x)
unreconcile.save().submit()
# Assert outstanding and unallocated
[doc.reload() for doc in [si1, si2, pe1, pe2]]
self.assertEqual(si1.outstanding_amount, 50)
self.assertEqual(si2.outstanding_amount, 0)
self.assertEqual(len(pe1.references), 2)
self.assertEqual(len(pe2.references), 1)
self.assertEqual(pe1.unallocated_amount, 0)
self.assertEqual(pe2.unallocated_amount, 50)
def test_03_unreconciliation_on_multi_currency_invoice(self):
self.create_customer("_Test MC Customer USD", "USD")
si1 = self.create_sales_invoice(do_not_submit=True)
si1.currency = "USD"
si1.debit_to = self.debtors_usd
si1.conversion_rate = 80
si1.save().submit()
si2 = self.create_sales_invoice(do_not_submit=True)
si2.currency = "USD"
si2.debit_to = self.debtors_usd
si2.conversion_rate = 80
si2.save().submit()
pe = self.create_payment_entry()
pe.paid_from = self.debtors_usd
pe.paid_from_account_currency = "USD"
pe.source_exchange_rate = 75
pe.received_amount = 75 * 200
pe.save()
# Allocate payment against both invoices
pe.append(
"references",
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100},
)
pe.append(
"references",
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100},
)
pe.save().submit()
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payment",
"company": self.company,
"voucher_type": pe.doctype,
"voucher_no": pe.name,
}
)
unreconcile.add_references()
self.assertEqual(len(unreconcile.allocations), 2)
allocations = [x.reference_name for x in unreconcile.allocations]
self.assertEquals([si1.name, si2.name], allocations)
# unreconcile si1 from pe
for x in unreconcile.allocations:
if x.reference_name != si1.name:
unreconcile.remove(x)
unreconcile.save().submit()
# Assert outstanding and unallocated
[doc.reload() for doc in [si1, si2, pe]]
self.assertEqual(si1.outstanding_amount, 100)
self.assertEqual(si2.outstanding_amount, 0)
self.assertEqual(len(pe.references), 1)
self.assertEqual(pe.unallocated_amount, 100)
# Exc gain/loss JE should've been cancelled as well
self.assertEqual(
frappe.db.count(
"Journal Entry Account",
filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1},
),
0,
)
def test_04_unreconciliation_on_multi_currency_invoice(self):
"""
2 payments split against 2 foreign currency invoices
"""
self.create_customer("_Test MC Customer USD", "USD")
si1 = self.create_sales_invoice(do_not_submit=True)
si1.currency = "USD"
si1.debit_to = self.debtors_usd
si1.conversion_rate = 80
si1.save().submit()
si2 = self.create_sales_invoice(do_not_submit=True)
si2.currency = "USD"
si2.debit_to = self.debtors_usd
si2.conversion_rate = 80
si2.save().submit()
pe1 = self.create_payment_entry()
pe1.paid_from = self.debtors_usd
pe1.paid_from_account_currency = "USD"
pe1.source_exchange_rate = 75
pe1.received_amount = 75 * 100
pe1.save()
# Allocate payment against both invoices
pe1.append(
"references",
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
)
pe1.append(
"references",
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
)
pe1.save().submit()
pe2 = self.create_payment_entry()
pe2.paid_from = self.debtors_usd
pe2.paid_from_account_currency = "USD"
pe2.source_exchange_rate = 75
pe2.received_amount = 75 * 100
pe2.save()
# Allocate payment against both invoices
pe2.append(
"references",
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
)
pe2.append(
"references",
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
)
pe2.save().submit()
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payment",
"company": self.company,
"voucher_type": pe2.doctype,
"voucher_no": pe2.name,
}
)
unreconcile.add_references()
self.assertEqual(len(unreconcile.allocations), 2)
allocations = [x.reference_name for x in unreconcile.allocations]
self.assertEquals([si1.name, si2.name], allocations)
# unreconcile si1 from pe2
for x in unreconcile.allocations:
if x.reference_name != si1.name:
unreconcile.remove(x)
unreconcile.save().submit()
# Assert outstanding and unallocated
[doc.reload() for doc in [si1, si2, pe1, pe2]]
self.assertEqual(si1.outstanding_amount, 50)
self.assertEqual(si2.outstanding_amount, 0)
self.assertEqual(len(pe1.references), 2)
self.assertEqual(len(pe2.references), 1)
self.assertEqual(pe1.unallocated_amount, 0)
self.assertEqual(pe2.unallocated_amount, 50)
# Exc gain/loss JE from PE1 should be available
self.assertEqual(
frappe.db.count(
"Journal Entry Account",
filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1},
),
1,
)

View File

@@ -1,41 +0,0 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Unreconcile Payment", {
refresh(frm) {
frm.set_query("voucher_type", function() {
return {
filters: {
name: ["in", ["Payment Entry", "Journal Entry"]]
}
}
});
frm.set_query("voucher_no", function(doc) {
return {
filters: {
company: doc.company,
docstatus: 1
}
}
});
},
get_allocations: function(frm) {
frm.clear_table("allocations");
frappe.call({
method: "get_allocations_from_payment",
doc: frm.doc,
callback: function(r) {
if (r.message) {
r.message.forEach(x => {
frm.add_child("allocations", x)
})
frm.refresh_fields();
}
}
})
}
});

View File

@@ -1,93 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:UNREC-{#####}",
"creation": "2023-08-22 10:26:34.421423",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"voucher_type",
"voucher_no",
"get_allocations",
"allocations",
"amended_from"
],
"fields": [
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Unreconcile Payment",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"label": "Voucher Type",
"options": "DocType"
},
{
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"label": "Voucher No",
"options": "voucher_type"
},
{
"fieldname": "get_allocations",
"fieldtype": "Button",
"label": "Get Allocations"
},
{
"fieldname": "allocations",
"fieldtype": "Table",
"label": "Allocations",
"options": "Unreconcile Payment Entries"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-08-28 17:42:50.261377",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Unreconcile Payment",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"read": 1,
"role": "Accounts Manager",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"read": 1,
"role": "Accounts User",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1,158 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _, qb
from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Abs, Sum
from frappe.utils.data import comma_and
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
unlink_ref_doc_from_payment_entries,
update_voucher_outstanding,
)
class UnreconcilePayment(Document):
def validate(self):
self.supported_types = ["Payment Entry", "Journal Entry"]
if not self.voucher_type in self.supported_types:
frappe.throw(_("Only {0} are supported").format(comma_and(self.supported_types)))
@frappe.whitelist()
def get_allocations_from_payment(self):
allocated_references = []
ple = qb.DocType("Payment Ledger Entry")
allocated_references = (
qb.from_(ple)
.select(
ple.account,
ple.party_type,
ple.party,
ple.against_voucher_type.as_("reference_doctype"),
ple.against_voucher_no.as_("reference_name"),
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
ple.account_currency,
)
.where(
(ple.docstatus == 1)
& (ple.voucher_type == self.voucher_type)
& (ple.voucher_no == self.voucher_no)
& (ple.voucher_no != ple.against_voucher_no)
)
.groupby(ple.against_voucher_type, ple.against_voucher_no)
.run(as_dict=True)
)
return allocated_references
def add_references(self):
allocations = self.get_allocations_from_payment()
for alloc in allocations:
self.append("allocations", alloc)
def on_submit(self):
# todo: more granular unreconciliation
for alloc in self.allocations:
doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name)
unlink_ref_doc_from_payment_entries(doc, self.voucher_no)
cancel_exchange_gain_loss_journal(doc, self.voucher_type, self.voucher_no)
update_voucher_outstanding(
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
)
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
@frappe.whitelist()
def doc_has_references(doctype: str = None, docname: str = None):
if doctype in ["Sales Invoice", "Purchase Invoice"]:
return frappe.db.count(
"Payment Ledger Entry",
filters={"delinked": 0, "against_voucher_no": docname, "amount": ["<", 0]},
)
else:
return frappe.db.count(
"Payment Ledger Entry",
filters={"delinked": 0, "voucher_no": docname, "against_voucher_no": ["!=", docname]},
)
@frappe.whitelist()
def get_linked_payments_for_doc(
company: str = None, doctype: str = None, docname: str = None
) -> list:
if company and doctype and docname:
_dt = doctype
_dn = docname
ple = qb.DocType("Payment Ledger Entry")
if _dt in ["Sales Invoice", "Purchase Invoice"]:
criteria = [
(ple.company == company),
(ple.delinked == 0),
(ple.against_voucher_no == _dn),
(ple.amount < 0),
]
res = (
qb.from_(ple)
.select(
ple.company,
ple.voucher_type,
ple.voucher_no,
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
ple.account_currency,
)
.where(Criterion.all(criteria))
.groupby(ple.voucher_no, ple.against_voucher_no)
.having(qb.Field("allocated_amount") > 0)
.run(as_dict=True)
)
return res
else:
criteria = [
(ple.company == company),
(ple.delinked == 0),
(ple.voucher_no == _dn),
(ple.against_voucher_no != _dn),
]
query = (
qb.from_(ple)
.select(
ple.company,
ple.against_voucher_type.as_("voucher_type"),
ple.against_voucher_no.as_("voucher_no"),
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
ple.account_currency,
)
.where(Criterion.all(criteria))
.groupby(ple.against_voucher_no)
)
res = query.run(as_dict=True)
return res
return []
@frappe.whitelist()
def create_unreconcile_doc_for_selection(selections=None):
if selections:
selections = frappe.json.loads(selections)
# assuming each row is a unique voucher
for row in selections:
unrecon = frappe.new_doc("Unreconcile Payment")
unrecon.company = row.get("company")
unrecon.voucher_type = row.get("voucher_type")
unrecon.voucher_no = row.get("voucher_no")
unrecon.add_references()
# remove unselected references
unrecon.allocations = [
x
for x in unrecon.allocations
if x.reference_doctype == row.get("against_voucher_type")
and x.reference_name == row.get("against_voucher_no")
]
unrecon.save().submit()

View File

@@ -1,83 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2023-08-22 10:28:10.196712",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"account",
"party_type",
"party",
"reference_doctype",
"reference_name",
"allocated_amount",
"account_currency",
"unlinked"
],
"fields": [
{
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Name",
"options": "reference_doctype"
},
{
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Allocated Amount",
"options": "account_currency"
},
{
"default": "0",
"fieldname": "unlinked",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Unlinked",
"read_only": 1
},
{
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Reference Type",
"options": "DocType"
},
{
"fieldname": "account",
"fieldtype": "Data",
"label": "Account"
},
{
"fieldname": "party_type",
"fieldtype": "Data",
"label": "Party Type"
},
{
"fieldname": "party",
"fieldtype": "Data",
"label": "Party"
},
{
"fieldname": "account_currency",
"fieldtype": "Link",
"label": "Account Currency",
"options": "Currency",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-09-05 09:33:28.620149",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Unreconcile Payment Entries",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -1,9 +0,0 @@
# 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 UnreconcilePaymentEntries(Document):
pass

View File

@@ -41,7 +41,7 @@ def make_gl_entries(
from_repost=from_repost,
)
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
# Post GL Map process there may no be any GL Entries
# Post GL Map proccess there may no be any GL Entries
elif gl_map:
frappe.throw(
_(
@@ -556,12 +556,7 @@ def get_round_off_account_and_cost_center(
def make_reverse_gl_entries(
gl_entries=None,
voucher_type=None,
voucher_no=None,
adv_adj=False,
update_outstanding="Yes",
partial_cancel=False,
gl_entries=None, voucher_type=None, voucher_no=None, adv_adj=False, update_outstanding="Yes"
):
"""
Get original gl entries of the voucher
@@ -581,19 +576,14 @@ def make_reverse_gl_entries(
if gl_entries:
create_payment_ledger_entry(
gl_entries,
cancel=1,
adv_adj=adv_adj,
update_outstanding=update_outstanding,
partial_cancel=partial_cancel,
gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding
)
validate_accounting_period(gl_entries)
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"])
if not partial_cancel:
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
for entry in gl_entries:
new_gle = copy.deepcopy(entry)

View File

@@ -5,8 +5,13 @@
from typing import Optional
import frappe
from frappe import _, msgprint, qb, scrub
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
from frappe import _, msgprint, scrub
from frappe.contacts.doctype.address.address import (
get_address_display,
get_company_address,
get_default_address,
)
from frappe.contacts.doctype.contact.contact import get_contact_details
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import Abs, Date, Sum
@@ -31,12 +36,7 @@ from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
from erpnext.utilities.regional import temporary_flag
PURCHASE_TRANSACTION_TYPES = {
"Supplier Quotation",
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice",
}
PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"}
SALES_TRANSACTION_TYPES = {
"Quotation",
"Sales Order",
@@ -133,7 +133,6 @@ def _get_party_details(
party_address,
company_address,
shipping_address,
ignore_permissions=ignore_permissions,
)
set_contact_details(party_details, party, party_type)
set_other_values(party_details, party, party_type)
@@ -194,8 +193,6 @@ def set_address_details(
party_address=None,
company_address=None,
shipping_address=None,
*,
ignore_permissions=False,
):
billing_address_field = (
"customer_address" if party_type == "Lead" else party_type.lower() + "_address"
@@ -208,17 +205,13 @@ def set_address_details(
get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])
)
# address display
party_details.address_display = render_address(
party_details[billing_address_field], check_permissions=not ignore_permissions
)
party_details.address_display = get_address_display(party_details[billing_address_field])
# shipping address
if party_type in ["Customer", "Lead"]:
party_details.shipping_address_name = shipping_address or get_party_shipping_address(
party_type, party.name
)
party_details.shipping_address = render_address(
party_details["shipping_address_name"], check_permissions=not ignore_permissions
)
party_details.shipping_address = get_address_display(party_details["shipping_address_name"])
if doctype:
party_details.update(
get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name)
@@ -236,10 +229,8 @@ def set_address_details(
if shipping_address:
party_details.update(
shipping_address=shipping_address,
shipping_address_display=render_address(
shipping_address, check_permissions=not ignore_permissions
),
**get_fetch_values(doctype, "shipping_address", shipping_address),
shipping_address_display=get_address_display(shipping_address),
**get_fetch_values(doctype, "shipping_address", shipping_address)
)
if party_details.company_address:
@@ -247,10 +238,9 @@ def set_address_details(
party_details.update(
billing_address=party_details.company_address,
billing_address_display=(
party_details.company_address_display
or render_address(party_details.company_address, check_permissions=False)
party_details.company_address_display or get_address_display(party_details.company_address)
),
**get_fetch_values(doctype, "billing_address", party_details.company_address),
**get_fetch_values(doctype, "billing_address", party_details.company_address)
)
# shipping address - if not already set
@@ -258,7 +248,7 @@ def set_address_details(
party_details.update(
shipping_address=party_details.billing_address,
shipping_address_display=party_details.billing_address_display,
**get_fetch_values(doctype, "shipping_address", party_details.billing_address),
**get_fetch_values(doctype, "shipping_address", party_details.billing_address)
)
party_address, shipping_address = (
@@ -300,34 +290,7 @@ def set_contact_details(party_details, party, party_type):
}
)
else:
fields = [
"name as contact_person",
"salutation",
"first_name",
"last_name",
"email_id as contact_email",
"mobile_no as contact_mobile",
"phone as contact_phone",
"designation as contact_designation",
"department as contact_department",
]
contact_details = frappe.db.get_value(
"Contact", party_details.contact_person, fields, as_dict=True
)
contact_details.contact_display = " ".join(
filter(
None,
[
contact_details.get("salutation"),
contact_details.get("first_name"),
contact_details.get("last_name"),
],
)
)
party_details.update(contact_details)
party_details.update(get_contact_details(party_details.contact_person))
def set_other_values(party_details, party, party_type):
@@ -466,19 +429,11 @@ def get_party_account_currency(party_type, party, company):
def get_party_gle_currency(party_type, party, company):
def generator():
gl = qb.DocType("GL Entry")
existing_gle_currency = (
qb.from_(gl)
.select(gl.account_currency)
.where(
(gl.docstatus == 1)
& (gl.company == company)
& (gl.party_type == party_type)
& (gl.party == party)
& (gl.is_cancelled == 0)
)
.limit(1)
.run()
existing_gle_currency = frappe.db.sql(
"""select account_currency from `tabGL Entry`
where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s
limit 1""",
{"company": company, "party_type": party_type, "party": party},
)
return existing_gle_currency[0][0] if existing_gle_currency else None
@@ -956,9 +911,6 @@ def get_partywise_advanced_payment_amount(
if party:
query = query.where(ple.party == party)
if invoice_doctypes := frappe.get_hooks("invoice_doctypes"):
query = query.where(ple.voucher_type.notin(invoice_doctypes))
data = query.run()
if data:
return frappe._dict(data)
@@ -1005,13 +957,3 @@ def add_party_account(party_type, party, company, account):
doc.append("accounts", accounts)
doc.save()
def render_address(address, check_permissions=True):
try:
from frappe.contacts.doctype.address.address import render_address as _render
except ImportError:
# Older frappe versions where this function is not available
from frappe.contacts.doctype.address.address import get_address_display as _render
return frappe.call(_render, address, check_permissions=check_permissions)

View File

@@ -95,27 +95,30 @@ frappe.query_reports["Accounts Payable"] = {
"options": "Payment Terms Template"
},
{
"fieldname":"party_type",
"fieldname": "party_type",
"label": __("Party Type"),
"fieldtype": "Autocomplete",
options: get_party_type_options(),
on_change: function() {
"fieldtype": "Link",
"options": "Party Type",
get_query: () => {
return {
filters: {
'account_type': 'Payable'
}
};
},
on_change: () => {
frappe.query_report.set_filter_value('party', "");
let party_type = frappe.query_report.get_filter_value('party_type');
frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier");
}
},
{
"fieldname":"party",
"label": __("Party"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
if (!frappe.query_report.filters) return;
let party_type = frappe.query_report.get_filter_value('party_type');
if (!party_type) return;
return frappe.db.get_link_options(party_type, txt);
},
"fieldtype": "Dynamic Link",
"options": "party_type",
},
{
"fieldname": "supplier_group",
@@ -143,18 +146,7 @@ frappe.query_reports["Accounts Payable"] = {
"fieldname": "show_future_payments",
"label": __("Show Future Payments"),
"fieldtype": "Check",
},
{
"fieldname": "for_revaluation_journals",
"label": __("Revaluation Journals"),
"fieldtype": "Check",
},
{
"fieldname": "ignore_accounts",
"label": __("Group by Voucher"),
"fieldtype": "Check",
}
],
"formatter": function(value, row, column, data, default_formatter) {
@@ -175,15 +167,3 @@ frappe.query_reports["Accounts Payable"] = {
}
erpnext.utils.add_dimensions('Accounts Payable', 9);
function get_party_type_options() {
let options = [];
frappe.db.get_list(
"Party Type", {filters:{"account_type": "Payable"}, fields:['name']}
).then((res) => {
res.forEach((party_type) => {
options.push(party_type.name);
});
});
return options;
}

View File

@@ -24,7 +24,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
def test_accounts_payable_for_foreign_currency_supplier(self):
def test_accounts_receivable_with_supplier(self):
pi = self.create_purchase_invoice(do_not_submit=True)
pi.currency = "USD"
pi.conversion_rate = 80
@@ -34,7 +34,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"party_type": "Supplier",
"party": [self.supplier],
"party": self.supplier,
"report_date": today(),
"range1": 30,
"range2": 60,

View File

@@ -72,27 +72,10 @@ frappe.query_reports["Accounts Payable Summary"] = {
}
},
{
"fieldname":"party_type",
"label": __("Party Type"),
"fieldtype": "Autocomplete",
options: get_party_type_options(),
on_change: function() {
frappe.query_report.set_filter_value('party', "");
frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier");
}
},
{
"fieldname":"party",
"label": __("Party"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
if (!frappe.query_report.filters) return;
let party_type = frappe.query_report.get_filter_value('party_type');
if (!party_type) return;
return frappe.db.get_link_options(party_type, txt);
},
"fieldname":"supplier",
"label": __("Supplier"),
"fieldtype": "Link",
"options": "Supplier"
},
{
"fieldname":"payment_terms_template",
@@ -110,11 +93,6 @@ frappe.query_reports["Accounts Payable Summary"] = {
"fieldname":"based_on_payment_terms",
"label": __("Based On Payment Terms"),
"fieldtype": "Check",
},
{
"fieldname": "for_revaluation_journals",
"label": __("Revaluation Journals"),
"fieldtype": "Check",
}
],
@@ -127,15 +105,3 @@ frappe.query_reports["Accounts Payable Summary"] = {
}
erpnext.utils.add_dimensions('Accounts Payable Summary', 9);
function get_party_type_options() {
let options = [];
frappe.db.get_list(
"Party Type", {filters:{"account_type": "Payable"}, fields:['name']}
).then((res) => {
res.forEach((party_type) => {
options.push(party_type.name);
});
});
return options;
}

View File

@@ -1,8 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.utils");
frappe.query_reports["Accounts Receivable"] = {
"filters": [
{
@@ -40,28 +38,32 @@ frappe.query_reports["Accounts Receivable"] = {
}
},
{
"fieldname":"party_type",
"label": __("Party Type"),
"fieldtype": "Autocomplete",
options: get_party_type_options(),
on_change: function() {
frappe.query_report.set_filter_value('party', "");
frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer");
"fieldname": "customer",
"label": __("Customer"),
"fieldtype": "Link",
"options": "Customer",
on_change: () => {
var customer = frappe.query_report.get_filter_value('customer');
var company = frappe.query_report.get_filter_value('company');
if (customer) {
frappe.db.get_value('Customer', customer, ["customer_name", "payment_terms"], function(value) {
frappe.query_report.set_filter_value('customer_name', value["customer_name"]);
frappe.query_report.set_filter_value('payment_terms', value["payment_terms"]);
});
frappe.db.get_value('Customer Credit Limit', {'parent': customer, 'company': company},
["credit_limit"], function(value) {
if (value) {
frappe.query_report.set_filter_value('credit_limit', value["credit_limit"]);
}
}, "Customer");
} else {
frappe.query_report.set_filter_value('customer_name', "");
frappe.query_report.set_filter_value('credit_limit', "");
frappe.query_report.set_filter_value('payment_terms', "");
}
}
},
{
"fieldname":"party",
"label": __("Party"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
if (!frappe.query_report.filters) return;
let party_type = frappe.query_report.get_filter_value('party_type');
if (!party_type) return;
return frappe.db.get_link_options(party_type, txt);
},
},
{
"fieldname": "party_account",
"label": __("Receivable Account"),
@@ -114,13 +116,10 @@ frappe.query_reports["Accounts Receivable"] = {
"reqd": 1
},
{
"fieldname":"customer_group",
"fieldname": "customer_group",
"label": __("Customer Group"),
"fieldtype": "MultiSelectList",
"options": "Customer Group",
get_data: function(txt) {
return frappe.db.get_link_options('Customer Group', txt);
}
"fieldtype": "Link",
"options": "Customer Group"
},
{
"fieldname": "payment_terms_template",
@@ -177,17 +176,23 @@ frappe.query_reports["Accounts Receivable"] = {
"fieldtype": "Check",
},
{
"fieldname": "for_revaluation_journals",
"label": __("Revaluation Journals"),
"fieldtype": "Check",
"fieldname": "customer_name",
"label": __("Customer Name"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname": "ignore_accounts",
"label": __("Group by Voucher"),
"fieldtype": "Check",
"fieldname": "payment_terms",
"label": __("Payment Tems"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname": "credit_limit",
"label": __("Credit Limit"),
"fieldtype": "Currency",
"hidden": 1
}
],
"formatter": function(value, row, column, data, default_formatter) {
@@ -208,16 +213,3 @@ frappe.query_reports["Accounts Receivable"] = {
}
erpnext.utils.add_dimensions('Accounts Receivable', 9);
function get_party_type_options() {
let options = [];
frappe.db.get_list(
"Party Type", {filters:{"account_type": "Receivable"}, fields:['name']}
).then((res) => {
res.forEach((party_type) => {
options.push(party_type.name);
});
});
return options;
}

View File

@@ -7,7 +7,7 @@ from collections import OrderedDict
import frappe
from frappe import _, qb, scrub
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Date, Substring, Sum
from frappe.query_builder.functions import Date, Sum
from frappe.utils import cint, cstr, flt, getdate, nowdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -116,12 +116,7 @@ class ReceivablePayableReport(object):
# build all keys, since we want to exclude vouchers beyond the report date
for ple in self.ple_entries:
# get the balance object for voucher_type
if self.filters.get("ignore_accounts"):
key = (ple.voucher_type, ple.voucher_no, ple.party)
else:
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
key = (ple.voucher_type, ple.voucher_no, ple.party)
if not key in self.voucher_balance:
self.voucher_balance[key] = frappe._dict(
voucher_type=ple.voucher_type,
@@ -188,10 +183,7 @@ class ReceivablePayableReport(object):
):
return
if self.filters.get("ignore_accounts"):
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
else:
key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
# If payment is made against credit note
# and credit note is made against a Sales Invoice
@@ -200,19 +192,13 @@ class ReceivablePayableReport(object):
if ple.against_voucher_no in self.return_entries:
return_against = self.return_entries.get(ple.against_voucher_no)
if return_against:
if self.filters.get("ignore_accounts"):
key = (ple.against_voucher_type, return_against, ple.party)
else:
key = (ple.account, ple.against_voucher_type, return_against, ple.party)
key = (ple.against_voucher_type, return_against, ple.party)
row = self.voucher_balance.get(key)
if not row:
# no invoice, this is an invoice / stand-alone payment / credit note
if self.filters.get("ignore_accounts"):
row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
else:
row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party))
row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
row.party_type = ple.party_type
return row
@@ -244,12 +230,8 @@ class ReceivablePayableReport(object):
row.invoiced_in_account_currency += amount_in_account_currency
else:
if self.is_invoice(ple):
if row.voucher_no == ple.voucher_no == ple.against_voucher_no:
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
else:
row.credit_note -= amount
row.credit_note_in_account_currency -= amount_in_account_currency
row.credit_note -= amount
row.credit_note_in_account_currency -= amount_in_account_currency
else:
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
@@ -285,20 +267,11 @@ class ReceivablePayableReport(object):
row.invoice_grand_total = row.invoiced
must_consider = False
if self.filters.get("for_revaluation_journals"):
if (abs(row.outstanding) > 0.0 / 10**self.currency_precision) or (
(abs(row.outstanding_in_account_currency) > 0.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 (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)
):
if must_consider:
# non-zero oustanding, we must consider this row
if self.is_invoice(row) and self.filters.based_on_payment_terms:
@@ -745,7 +718,6 @@ class ReceivablePayableReport(object):
query = (
qb.from_(ple)
.select(
ple.name,
ple.account,
ple.voucher_type,
ple.voucher_no,
@@ -759,20 +731,13 @@ class ReceivablePayableReport(object):
ple.account_currency,
ple.amount,
ple.amount_in_account_currency,
ple.remarks,
)
.where(ple.delinked == 0)
.where(Criterion.all(self.qb_selection_filter))
.where(Criterion.any(self.or_filters))
)
if self.filters.get("show_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"):
query = query.orderby(self.ple.party, self.ple.posting_date)
else:
@@ -804,12 +769,15 @@ class ReceivablePayableReport(object):
self.or_filters = []
for party_type in self.party_type:
self.add_common_filters()
party_type_field = scrub(party_type)
self.or_filters.append(self.ple.party_type == party_type)
if self.account_type == "Receivable":
self.add_common_filters(party_type_field=party_type_field)
if party_type_field == "customer":
self.add_customer_filters()
elif self.account_type == "Payable":
elif party_type_field == "supplier":
self.add_supplier_filters()
if self.filters.cost_center:
@@ -825,18 +793,21 @@ class ReceivablePayableReport(object):
]
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
def add_common_filters(self):
def add_common_filters(self, party_type_field):
if self.filters.company:
self.qb_selection_filter.append(self.ple.company == self.filters.company)
if self.filters.finance_book:
self.qb_selection_filter.append(self.ple.finance_book == self.filters.finance_book)
if self.filters.get(party_type_field):
self.qb_selection_filter.append(self.ple.party == self.filters.get(party_type_field))
if self.filters.get("party_type"):
self.qb_selection_filter.append(self.filters.party_type == self.ple.party_type)
if self.filters.get("party"):
self.qb_selection_filter.append(self.ple.party.isin(self.filters.party))
self.qb_selection_filter.append(self.filters.party == self.ple.party)
if self.filters.party_account:
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
@@ -858,13 +829,7 @@ class ReceivablePayableReport(object):
self.customer = qb.DocType("Customer")
if self.filters.get("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))
self.get_hierarchical_filters("Customer Group", "customer_group")
if self.filters.get("territory"):
self.get_hierarchical_filters("Territory", "territory")
@@ -1004,20 +969,6 @@ class ReceivablePayableReport(object):
fieldtype="Link",
options="Contact",
)
if self.filters.party_type == "Customer":
self.add_column(
_("Customer Name"),
fieldname="customer_name",
fieldtype="Link",
options="Customer",
)
elif self.filters.party_type == "Supplier":
self.add_column(
_("Supplier Name"),
fieldname="supplier_name",
fieldtype="Link",
options="Supplier",
)
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
@@ -1089,7 +1040,7 @@ class ReceivablePayableReport(object):
)
if self.filters.show_remarks:
self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200)
self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200),
def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120):
if not fieldname:
@@ -1156,19 +1107,3 @@ class ReceivablePayableReport(object):
.run()
)
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

@@ -1,7 +1,6 @@
import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, getdate, today
@@ -24,6 +23,29 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
def create_usd_account(self):
name = "Debtors USD"
exists = frappe.db.get_list(
"Account", filters={"company": "_Test Company 2", "account_name": "Debtors USD"}
)
if exists:
self.debtors_usd = exists[0].name
else:
debtors = frappe.get_doc(
"Account",
frappe.db.get_list(
"Account", filters={"company": "_Test Company 2", "account_name": "Debtors"}
)[0].name,
)
debtors_usd = frappe.new_doc("Account")
debtors_usd.company = debtors.company
debtors_usd.account_name = "Debtors USD"
debtors_usd.account_currency = "USD"
debtors_usd.parent_account = debtors.parent_account
debtors_usd.account_type = debtors.account_type
self.debtors_usd = debtors_usd.save().name
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")
si = create_sales_invoice(
@@ -76,41 +98,6 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
return credit_note
def test_pos_receivable(self):
filters = {
"company": self.company,
"party_type": "Customer",
"party": [self.customer],
"report_date": add_days(today(), 2),
"based_on_payment_terms": 0,
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"show_remarks": False,
}
pos_inv = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
pos_inv.posting_date = add_days(today(), 2)
pos_inv.is_pos = 1
pos_inv.append(
"payments",
frappe._dict(
mode_of_payment="Cash",
amount=flt(pos_inv.grand_total / 2),
),
)
pos_inv.disable_rounded_total = 1
pos_inv.save()
pos_inv.submit()
report = execute(filters)
expected_data = [[pos_inv.grand_total, pos_inv.paid_amount, 0]]
row = report[1][-1]
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
pos_inv.cancel()
def test_accounts_receivable(self):
filters = {
"company": self.company,
@@ -510,30 +497,6 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
report = execute(filters)[1]
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):
si1 = self.create_sales_invoice()
self.customer2 = (
@@ -605,169 +568,3 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
row.account_currency,
],
)
def test_usd_customer_filter(self):
filters = {
"company": self.company,
"party_type": "Customer",
"party": [self.customer],
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
}
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.currency = "USD"
si.conversion_rate = 80
si.debit_to = self.debtors_usd
si.save().submit()
name = si.name
# check invoice grand total and invoiced column's value for 3 payment terms
report = execute(filters)
expected = {
"voucher_type": si.doctype,
"voucher_no": si.name,
"party_account": self.debtors_usd,
"customer_name": self.customer,
"invoiced": 100.0,
"outstanding": 100.0,
"account_currency": "USD",
}
self.assertEqual(len(report[1]), 1)
report_output = report[1][0]
for field in expected:
with self.subTest(field=field):
self.assertEqual(report_output.get(field), expected.get(field))
def test_multi_select_party_filter(self):
self.customer1 = self.customer
self.create_customer("_Test Customer 2")
self.customer2 = self.customer
self.create_customer("_Test Customer 3")
self.customer3 = self.customer
filters = {
"company": self.company,
"party_type": "Customer",
"party": [self.customer1, self.customer3],
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
}
si1 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si1.customer = self.customer1
si1.save().submit()
si2 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si2.customer = self.customer2
si2.save().submit()
si3 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si3.customer = self.customer3
si3.save().submit()
# check invoice grand total and invoiced column's value for 3 payment terms
report = execute(filters)
expected_output = {self.customer1, self.customer3}
self.assertEqual(len(report[1]), 2)
output_for = set([x.party for x in report[1]])
self.assertEqual(output_for, expected_output)
def test_report_output_if_party_is_missing(self):
acc_name = "Additional Debtors"
if not frappe.db.get_value(
"Account", filters={"account_name": acc_name, "company": self.company}
):
additional_receivable_acc = frappe.get_doc(
{
"doctype": "Account",
"account_name": acc_name,
"parent_account": "Accounts Receivable - " + self.company_abbr,
"company": self.company,
"account_type": "Receivable",
}
).save()
self.debtors2 = additional_receivable_acc.name
je = frappe.new_doc("Journal Entry")
je.company = self.company
je.posting_date = today()
je.append(
"accounts",
{
"account": self.debit_to,
"party_type": "Customer",
"party": self.customer,
"debit_in_account_currency": 150,
"credit_in_account_currency": 0,
"cost_center": self.cost_center,
},
)
je.append(
"accounts",
{
"account": self.debtors2,
"party_type": "Customer",
"party": self.customer,
"debit_in_account_currency": 200,
"credit_in_account_currency": 0,
"cost_center": self.cost_center,
},
)
je.append(
"accounts",
{
"account": self.cash,
"debit_in_account_currency": 0,
"credit_in_account_currency": 350,
"cost_center": self.cost_center,
},
)
je.save().submit()
# manually remove party from Payment Ledger
ple = qb.DocType("Payment Ledger Entry")
qb.update(ple).set(ple.party, None).where(ple.voucher_no == je.name).run()
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
}
report_ouput = execute(filters)[1]
expected_data = [
[self.debtors2, je.doctype, je.name, "Customer", self.customer, 200.0, 0.0, 0.0, 200.0],
[self.debit_to, je.doctype, je.name, "Customer", self.customer, 150.0, 0.0, 0.0, 150.0],
]
self.assertEqual(len(report_ouput), 2)
# fetch only required fields
report_output = [
[
x.party_account,
x.voucher_type,
x.voucher_no,
"Customer",
self.customer,
x.invoiced,
x.paid,
x.credit_note,
x.outstanding,
]
for x in report_ouput
]
# use account name to sort
# post sorting output should be [[Additional Debtors, ...], [Debtors, ...]]
report_output = sorted(report_output, key=lambda x: x[0])
self.assertEqual(expected_data, report_output)

View File

@@ -72,27 +72,10 @@ frappe.query_reports["Accounts Receivable Summary"] = {
}
},
{
"fieldname":"party_type",
"label": __("Party Type"),
"fieldtype": "Autocomplete",
options: get_party_type_options(),
on_change: function() {
frappe.query_report.set_filter_value('party', "");
frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer");
}
},
{
"fieldname":"party",
"label": __("Party"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
if (!frappe.query_report.filters) return;
let party_type = frappe.query_report.get_filter_value('party_type');
if (!party_type) return;
return frappe.db.get_link_options(party_type, txt);
},
"fieldname":"customer",
"label": __("Customer"),
"fieldtype": "Link",
"options": "Customer"
},
{
"fieldname":"customer_group",
@@ -139,11 +122,6 @@ frappe.query_reports["Accounts Receivable Summary"] = {
"label": __("Show GL Balance"),
"fieldtype": "Check",
},
{
"fieldname": "for_revaluation_journals",
"label": __("Revaluation Journals"),
"fieldtype": "Check",
}
],
onload: function(report) {
@@ -155,15 +133,3 @@ frappe.query_reports["Accounts Receivable Summary"] = {
}
erpnext.utils.add_dimensions('Accounts Receivable Summary', 9);
function get_party_type_options() {
let options = [];
frappe.db.get_list(
"Party Type", {filters:{"account_type": "Receivable"}, fields:['name']}
).then((res) => {
res.forEach((party_type) => {
options.push(party_type.name);
});
});
return options;
}

View File

@@ -8,7 +8,6 @@ from frappe.utils import cint, flt
from erpnext.accounts.party import get_partywise_advanced_payment_amount
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
from erpnext.accounts.utils import get_currency_precision
def execute(filters=None):
@@ -36,7 +35,6 @@ class AccountsReceivableSummary(ReceivablePayableReport):
def get_data(self, args):
self.data = []
self.receivables = ReceivablePayableReport(self.filters).run(args)[1]
self.currency_precision = get_currency_precision() or 2
self.get_party_total(args)
@@ -60,7 +58,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company)
for party, party_dict in self.party_total.items():
if flt(party_dict.outstanding, self.currency_precision) == 0:
if party_dict.outstanding == 0:
continue
row = frappe._dict()
@@ -101,11 +99,13 @@ class AccountsReceivableSummary(ReceivablePayableReport):
# Add all amount columns
for k in list(self.party_total[d.party]):
if isinstance(self.party_total[d.party][k], float):
self.party_total[d.party][k] += d.get(k) or 0.0
if k not in ["currency", "sales_person"]:
self.party_total[d.party][k] += d.get(k, 0.0)
# set territory, customer_group, sales person etc
self.set_party_details(d)
self.party_total[d.party].update({"party_type": d.party_type})
def init_party_total(self, row):
self.party_total.setdefault(
@@ -124,7 +124,6 @@ class AccountsReceivableSummary(ReceivablePayableReport):
"total_due": 0.0,
"future_amount": 0.0,
"sales_person": [],
"party_type": row.party_type,
}
),
)
@@ -134,12 +133,13 @@ class AccountsReceivableSummary(ReceivablePayableReport):
for key in ("territory", "customer_group", "supplier_group"):
if row.get(key):
self.party_total[row.party][key] = row.get(key, "")
self.party_total[row.party][key] = row.get(key)
if row.sales_person:
self.party_total[row.party].sales_person.append(row.get("sales_person", ""))
self.party_total[row.party].sales_person.append(row.sales_person)
if self.filters.sales_partner:
self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner", "")
self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner")
def get_columns(self):
self.columns = []

View File

@@ -31,18 +31,6 @@ frappe.query_reports["Asset Depreciation Ledger"] = {
"fieldtype": "Link",
"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",
"label": __("Finance Book"),
@@ -50,10 +38,10 @@ frappe.query_reports["Asset Depreciation Ledger"] = {
"options": "Finance Book"
},
{
"fieldname": "include_default_book_assets",
"label": __("Include Default FB Assets"),
"fieldtype": "Check",
"default": 1
},
"fieldname":"asset_category",
"label": __("Asset Category"),
"fieldtype": "Link",
"options": "Asset Category"
}
]
}

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
frappe.query_reports["Balance Sheet"]["filters"].push({
"fieldname": "include_default_book_entries",
"label": __("Include Default FB Entries"),
"label": __("Include Default Book Entries"),
"fieldtype": "Check",
"default": 1
});

View File

@@ -23,7 +23,6 @@ class TestBankReconciliationStatement(FrappeTestCase):
"Payment Entry",
]:
frappe.db.delete(dt)
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
def test_loan_entries_in_bank_reco_statement(self):
create_loan_accounts()

View File

@@ -16,7 +16,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
frappe.query_reports["Cash Flow"]["filters"].push(
{
"fieldname": "include_default_book_entries",
"label": __("Include Default FB Entries"),
"label": __("Include Default Book Entries"),
"fieldtype": "Check",
"default": 1
}

View File

@@ -67,7 +67,7 @@ def setup_mappers(mappers):
mapping["finance_costs"] = []
mapping["finance_costs_adjustments"] = []
doc = frappe.get_doc("Cash Flow Mapper", mapping["name"])
mapping_names = [item.mapping for item in doc.accounts]
mapping_names = [item.name for item in doc.accounts]
if not mapping_names:
continue

View File

@@ -105,7 +105,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
},
{
"fieldname": "include_default_book_entries",
"label": __("Include Default FB Entries"),
"label": __("Include Default Book Entries"),
"fieldtype": "Check",
"default": 1
},

View File

@@ -8,17 +8,7 @@ import re
import frappe
from frappe import _
from frappe.utils import (
add_days,
add_months,
cint,
cstr,
flt,
formatdate,
get_first_day,
getdate,
today,
)
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
@@ -53,8 +43,6 @@ def get_period_list(
year_start_date = getdate(period_start_date)
year_end_date = getdate(period_end_date)
year_end_date = getdate(today()) if year_end_date > getdate(today()) else year_end_date
months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity]
period_list = []
@@ -573,7 +561,9 @@ def apply_additional_conditions(doctype, query, from_date, ignore_closing_entrie
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):
frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'"))
frappe.throw(
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
)
query = query.where(
(gl_entry.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))

View File

@@ -79,9 +79,7 @@ class General_Payment_Ledger_Comparison(object):
.select(
gle.company,
gle.account,
gle.voucher_type,
gle.voucher_no,
gle.party_type,
gle.party,
outstanding,
)
@@ -91,9 +89,7 @@ class General_Payment_Ledger_Comparison(object):
& (gle.account.isin(val.accounts))
)
.where(Criterion.all(filter_criterion))
.groupby(
gle.company, gle.account, gle.voucher_type, gle.voucher_no, gle.party_type, gle.party
)
.groupby(gle.company, gle.account, gle.voucher_no, gle.party)
.run()
)
@@ -116,13 +112,7 @@ class General_Payment_Ledger_Comparison(object):
self.account_types[acc_type].ple = (
qb.from_(ple)
.select(
ple.company,
ple.account,
ple.voucher_type,
ple.voucher_no,
ple.party_type,
ple.party,
Sum(ple.amount).as_("outstanding"),
ple.company, ple.account, ple.voucher_no, ple.party, Sum(ple.amount).as_("outstanding")
)
.where(
(ple.company == self.filters.company)
@@ -130,9 +120,7 @@ class General_Payment_Ledger_Comparison(object):
& (ple.account.isin(val.accounts))
)
.where(Criterion.all(filter_criterion))
.groupby(
ple.company, ple.account, ple.voucher_type, ple.voucher_no, ple.party_type, ple.party
)
.groupby(ple.company, ple.account, ple.voucher_no, ple.party)
.run()
)
@@ -145,17 +133,15 @@ class General_Payment_Ledger_Comparison(object):
self.gle_balances = set(val.gle) | self.gle_balances
self.ple_balances = set(val.ple) | self.ple_balances
self.variation_in_payment_ledger = self.gle_balances.difference(self.ple_balances)
self.variation_in_general_ledger = self.ple_balances.difference(self.gle_balances)
self.diff1 = self.gle_balances.difference(self.ple_balances)
self.diff2 = self.ple_balances.difference(self.gle_balances)
self.diff = frappe._dict({})
for x in self.variation_in_payment_ledger:
self.diff[(x[0], x[1], x[2], x[3], x[4], x[5])] = frappe._dict({"gl_balance": x[6]})
for x in self.diff1:
self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]})
for x in self.variation_in_general_ledger:
self.diff.setdefault(
(x[0], x[1], x[2], x[3], x[4], x[5]), frappe._dict({"gl_balance": 0.0})
).update(frappe._dict({"pl_balance": x[6]}))
for x in self.diff2:
self.diff[(x[0], x[1], x[2], x[3])].update(frappe._dict({"pl_balance": x[4]}))
def generate_data(self):
self.data = []
@@ -163,12 +149,8 @@ class General_Payment_Ledger_Comparison(object):
self.data.append(
frappe._dict(
{
"company": key[0],
"account": key[1],
"voucher_type": key[2],
"voucher_no": key[3],
"party_type": key[4],
"party": key[5],
"voucher_no": key[2],
"party": key[3],
"gl_balance": val.gl_balance,
"pl_balance": val.pl_balance,
}
@@ -178,52 +160,12 @@ class General_Payment_Ledger_Comparison(object):
def get_columns(self):
self.columns = []
options = None
self.columns.append(
dict(
label=_("Company"),
fieldname="company",
fieldtype="Link",
options="Company",
width="100",
)
)
self.columns.append(
dict(
label=_("Account"),
fieldname="account",
fieldtype="Link",
options="Account",
width="100",
)
)
self.columns.append(
dict(
label=_("Voucher Type"),
fieldname="voucher_type",
fieldtype="Link",
options="DocType",
width="100",
)
)
self.columns.append(
dict(
label=_("Voucher No"),
fieldname="voucher_no",
fieldtype="Dynamic Link",
options="voucher_type",
width="100",
)
)
self.columns.append(
dict(
label=_("Party Type"),
fieldname="party_type",
fieldtype="Link",
options="DocType",
fieldtype="Data",
options=options,
width="100",
)
)
@@ -232,8 +174,8 @@ class General_Payment_Ledger_Comparison(object):
dict(
label=_("Party"),
fieldname="party",
fieldtype="Dynamic Link",
options="party_type",
fieldtype="Data",
options=options,
width="100",
)
)

View File

@@ -50,11 +50,7 @@ class TestGeneralAndPaymentLedger(FrappeTestCase, AccountsTestMixin):
self.assertEqual(len(data), 1)
expected = {
"company": sinv.company,
"account": sinv.debit_to,
"voucher_type": sinv.doctype,
"voucher_no": sinv.name,
"party_type": "Customer",
"party": sinv.customer,
"gl_balance": sinv.grand_total,
"pl_balance": sinv.grand_total - 1,

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