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
126 changed files with 919 additions and 3777 deletions

View File

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

View File

@@ -352,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

@@ -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

@@ -50,18 +50,8 @@ frappe.ui.form.on("Journal Entry", {
frm.trigger("make_inter_company_journal_entry");
}, __('Make'));
}
},
erpnext.accounts.unreconcile_payments.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

@@ -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 Payments', '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,7 +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);
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
},
validate_company: (frm) => {

View File

@@ -107,8 +107,6 @@ class PaymentEntry(AccountsController):
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
"Unreconcile Payments",
"Unreconcile Payment Entries",
)
super(PaymentEntry, self).on_cancel()
self.make_gl_entries(cancel=1)
@@ -229,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."
@@ -1604,10 +1600,11 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company):
"voucher_type": d.voucher_type,
"posting_date": d.posting_date,
"invoice_amount": flt(d.invoice_amount),
"outstanding_amount": payment_term_outstanding
"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_term_outstanding": payment_term_outstanding,
"payment_amount": payment_term.payment_amount,
"payment_term": payment_term.payment_term,
}

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):
@@ -62,7 +62,7 @@ class PaymentReconciliation(Document):
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,
@@ -350,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"
@@ -651,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

@@ -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

@@ -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:

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

@@ -47,20 +47,6 @@ 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 = ""
@@ -78,7 +64,6 @@ def get_statement_dict(doc, get_statement_dict=False):
filters = get_common_filters(doc)
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("'", "")
@@ -91,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):
@@ -108,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)
@@ -152,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

@@ -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(
@@ -479,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

@@ -166,7 +166,6 @@
"against_expense_account",
"column_break_63",
"unrealized_profit_loss_account",
"repost_required",
"subscription_section",
"auto_repeat",
"update_auto_repeat_reference",
@@ -191,7 +190,8 @@
"inter_company_invoice_reference",
"is_old_subcontracting_flow",
"remarks",
"connections_tab"
"connections_tab",
"column_break_38"
],
"fields": [
{
@@ -987,7 +987,6 @@
"print_hide": 1
},
{
"allow_on_submit": 1,
"fieldname": "cash_bank_account",
"fieldtype": "Link",
"label": "Cash/Bank Account",
@@ -1051,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",
@@ -1215,7 +1213,6 @@
"read_only": 1
},
{
"allow_on_submit": 1,
"default": "No",
"fieldname": "is_opening",
"fieldtype": "Select",
@@ -1348,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",
@@ -1381,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",
@@ -1499,6 +1494,10 @@
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_38",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_50",
"fieldtype": "Column Break"
@@ -1569,22 +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
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2023-10-01 21:01:47.282533",
"modified": "2023-07-04 17:23:59.145031",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -11,9 +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,
)
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
check_if_return_invoice_linked_with_payment_entry,
get_total_in_party_account_currency,
@@ -490,11 +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_deferred_accounting([], [self.name])
def on_submit(self):
super(PurchaseInvoice, self).on_submit()
@@ -537,18 +529,6 @@ class PurchaseInvoice(BuyingController):
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)
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()

View File

@@ -1796,6 +1796,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
pi = make_purchase_invoice(
company="_Test Company",
customer="_Test Supplier",
do_not_save=True,
do_not_submit=True,
rate=1000,
@@ -1825,32 +1826,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
clear_dimension_defaults("Branch")
disable_dimension()
def test_repost_accounting_entries(self):
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 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",
@@ -468,7 +467,6 @@
"label": "Accounting"
},
{
"allow_on_submit": 1,
"fieldname": "expense_account",
"fieldtype": "Link",
"label": "Expense Head",
@@ -879,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-10-03 21:01:01.824892",
"modified": "2023-07-04 17:22:21.501152",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
@@ -900,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

@@ -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

@@ -21,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:
@@ -118,17 +139,14 @@ class RepostAccountingLedger(Document):
return rendered_page
def on_submit(self):
if len(self.vouchers) > 1:
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()
@@ -163,26 +181,3 @@ def start_repost(account_repost_doc=str) -> None:
doc.make_gl_entries()
frappe.db.commit()
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]))
)
)

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 Payments", "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,11 +177,8 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
}, __('Create'));
}
}
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
}
make_maintenance_schedule() {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",

View File

@@ -11,13 +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,
)
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
@@ -176,12 +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_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:
@@ -407,8 +401,6 @@ class SalesInvoice(SellingController):
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
"Unreconcile Payments",
"Unreconcile Payment Entries",
"Payment Ledger Entry",
)
@@ -535,21 +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)
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

View File

@@ -1801,10 +1801,6 @@ class TestSalesInvoice(unittest.TestCase):
)
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",
@@ -1824,25 +1820,10 @@ class TestSalesInvoice(unittest.TestCase):
"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",
@@ -1850,7 +1831,6 @@ class TestSalesInvoice(unittest.TestCase):
"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,
@@ -1859,13 +1839,7 @@ class TestSalesInvoice(unittest.TestCase):
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(
@@ -1873,9 +1847,11 @@ class TestSalesInvoice(unittest.TestCase):
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),

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

@@ -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 TestUnreconcilePayments(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 Payments",
"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 Payments",
"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 Payments",
"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 Payments",
"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 Payments", {
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 Payments",
"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 Payments",
"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 UnreconcilePayments(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 Payments")
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

@@ -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",
@@ -164,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",
@@ -122,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"),
@@ -172,6 +174,24 @@ frappe.query_reports["Accounts Receivable"] = {
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check",
},
{
"fieldname": "customer_name",
"label": __("Customer Name"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname": "payment_terms",
"label": __("Payment Tems"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname": "credit_limit",
"label": __("Credit Limit"),
"fieldtype": "Currency",
"hidden": 1
}
],
@@ -193,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

@@ -769,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:
@@ -790,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)
@@ -963,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")

View File

@@ -568,78 +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)

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",
@@ -150,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

@@ -99,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(
@@ -122,7 +124,6 @@ class AccountsReceivableSummary(ReceivablePayableReport):
"total_due": 0.0,
"future_amount": 0.0,
"sales_person": [],
"party_type": row.party_type,
}
),
)
@@ -132,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

@@ -99,12 +99,6 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"label": __("Include Default Book Entries"),
"fieldtype": "Check",
"default": 1
},
{
"fieldname": "show_net_values",
"label": __("Show net values in opening and closing columns"),
"fieldtype": "Check",
"default": 1
}
],
"formatter": erpnext.financial_statements.formatter,

View File

@@ -120,9 +120,7 @@ def get_data(filters):
ignore_opening_entries=True,
)
calculate_values(
accounts, gl_entries_by_account, opening_balances, filters.get("show_net_values")
)
calculate_values(accounts, gl_entries_by_account, opening_balances)
accumulate_values_into_parents(accounts, accounts_by_name)
data = prepare_data(accounts, filters, parent_children_map, company_currency)
@@ -312,7 +310,7 @@ def get_opening_balance(
return gle
def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values):
def calculate_values(accounts, gl_entries_by_account, opening_balances):
init = {
"opening_debit": 0.0,
"opening_credit": 0.0,
@@ -337,8 +335,7 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net
d["closing_debit"] = d["opening_debit"] + d["debit"]
d["closing_credit"] = d["opening_credit"] + d["credit"]
if show_net_values:
prepare_opening_closing(d)
prepare_opening_closing(d)
def calculate_total_row(accounts, company_currency):
@@ -378,7 +375,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
for d in accounts:
# Prepare opening closing for group account
if parent_children_map.get(d.account) and filters.get("show_net_values"):
if parent_children_map.get(d.account):
prepare_opening_closing(d)
has_value = False

View File

@@ -46,7 +46,6 @@ def get_data(filters):
.select(
gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit")
)
.where(gle.is_cancelled == 0)
.groupby(gle.voucher_no)
)
query = apply_filters(query, filters, gle)

View File

@@ -560,10 +560,6 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
"""
jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0]
# Update Advance Paid in SO/PO since they might be getting unlinked
if jv_detail.get("reference_type") in ("Sales Order", "Purchase Order"):
frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
# adjust the unreconciled balance
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
@@ -629,13 +625,6 @@ def update_reference_in_payment_entry(
if d.voucher_detail_no:
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
# Update Advance Paid in SO/PO since they are getting unlinked
if existing_row.get("reference_doctype") in ("Sales Order", "Purchase Order"):
frappe.get_doc(
existing_row.reference_doctype, existing_row.reference_name
).set_total_advance_paid()
original_row = existing_row.as_dict().copy()
existing_row.update(reference_details)
@@ -663,9 +652,7 @@ def update_reference_in_payment_entry(
payment_entry.save(ignore_permissions=True)
def cancel_exchange_gain_loss_journal(
parent_doc: dict | object, referenced_dt: str = None, referenced_dn: str = None
) -> None:
def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None:
"""
Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any.
"""
@@ -692,147 +679,76 @@ def cancel_exchange_gain_loss_journal(
as_list=1,
)
for doc in gain_loss_journals:
gain_loss_je = frappe.get_doc("Journal Entry", doc[0])
if referenced_dt and referenced_dn:
references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts]
if (
len(references) == 2
and (referenced_dt, referenced_dn) in references
and (parent_doc.doctype, parent_doc.name) in references
):
# only cancel JE generated against parent_doc and referenced_dn
gain_loss_je.cancel()
else:
gain_loss_je.cancel()
frappe.get_doc("Journal Entry", doc[0]).cancel()
def update_accounting_ledgers_after_reference_removal(
ref_type: str = None, ref_no: str = None, payment_name: str = None
):
# General Ledger
gle = qb.DocType("GL Entry")
gle_update_query = (
qb.update(gle)
.set(gle.against_voucher_type, None)
.set(gle.against_voucher, None)
.set(gle.modified, now())
.set(gle.modified_by, frappe.session.user)
.where((gle.against_voucher_type == ref_type) & (gle.against_voucher == ref_no))
def unlink_ref_doc_from_payment_entries(ref_doc):
remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name)
remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name)
frappe.db.sql(
"""update `tabGL Entry`
set against_voucher_type=null, against_voucher=null,
modified=%s, modified_by=%s
where against_voucher_type=%s and against_voucher=%s
and voucher_no != ifnull(against_voucher, '')""",
(now(), frappe.session.user, ref_doc.doctype, ref_doc.name),
)
if payment_name:
gle_update_query = gle_update_query.where(gle.voucher_no == payment_name)
gle_update_query.run()
# Payment Ledger
ple = qb.DocType("Payment Ledger Entry")
ple_update_query = (
qb.update(ple)
.set(ple.against_voucher_type, ple.voucher_type)
.set(ple.against_voucher_no, ple.voucher_no)
.set(ple.modified, now())
.set(ple.modified_by, frappe.session.user)
.where(
(ple.against_voucher_type == ref_type)
& (ple.against_voucher_no == ref_no)
& (ple.delinked == 0)
)
)
if payment_name:
ple_update_query = ple_update_query.where(ple.voucher_no == payment_name)
ple_update_query.run()
qb.update(ple).set(ple.against_voucher_type, ple.voucher_type).set(
ple.against_voucher_no, ple.voucher_no
).set(ple.modified, now()).set(ple.modified_by, frappe.session.user).where(
(ple.against_voucher_type == ref_doc.doctype)
& (ple.against_voucher_no == ref_doc.name)
& (ple.delinked == 0)
).run()
def remove_ref_from_advance_section(ref_doc: object = None):
# TODO: this might need some testing
if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"):
ref_doc.set("advances", [])
adv_type = qb.DocType(f"{ref_doc.doctype} Advance")
qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run()
def unlink_ref_doc_from_payment_entries(ref_doc: object = None, payment_name: str = None):
remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name, payment_name)
remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name, payment_name)
update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name, payment_name)
remove_ref_from_advance_section(ref_doc)
def remove_ref_doc_link_from_jv(
ref_type: str = None, ref_no: str = None, payment_name: str = None
):
jea = qb.DocType("Journal Entry Account")
linked_jv = (
qb.from_(jea)
.select(jea.parent)
.where((jea.reference_type == ref_type) & (jea.reference_name == ref_no) & (jea.docstatus.lt(2)))
.run(as_list=1)
)
linked_jv = convert_to_list(linked_jv)
# remove reference only from specified payment
linked_jv = [x for x in linked_jv if x == payment_name] if payment_name else linked_jv
if linked_jv:
update_query = (
qb.update(jea)
.set(jea.reference_type, None)
.set(jea.reference_name, None)
.set(jea.modified, now())
.set(jea.modified_by, frappe.session.user)
.where((jea.reference_type == ref_type) & (jea.reference_name == ref_no))
frappe.db.sql(
"""delete from `tab{0} Advance` where parent = %s""".format(ref_doc.doctype), ref_doc.name
)
if payment_name:
update_query = update_query.where(jea.parent == payment_name)
update_query.run()
def remove_ref_doc_link_from_jv(ref_type, ref_no):
linked_jv = frappe.db.sql_list(
"""select parent from `tabJournal Entry Account`
where reference_type=%s and reference_name=%s and docstatus < 2""",
(ref_type, ref_no),
)
if linked_jv:
frappe.db.sql(
"""update `tabJournal Entry Account`
set reference_type=null, reference_name = null,
modified=%s, modified_by=%s
where reference_type=%s and reference_name=%s
and docstatus < 2""",
(now(), frappe.session.user, ref_type, ref_no),
)
frappe.msgprint(_("Journal Entries {0} are un-linked").format("\n".join(linked_jv)))
def convert_to_list(result):
"""
Convert tuple to list
"""
return [x[0] for x in result]
def remove_ref_doc_link_from_pe(
ref_type: str = None, ref_no: str = None, payment_name: str = None
):
per = qb.DocType("Payment Entry Reference")
pay = qb.DocType("Payment Entry")
linked_pe = (
qb.from_(per)
.select(per.parent)
.where(
(per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2))
)
.run(as_list=1)
def remove_ref_doc_link_from_pe(ref_type, ref_no):
linked_pe = frappe.db.sql_list(
"""select parent from `tabPayment Entry Reference`
where reference_doctype=%s and reference_name=%s and docstatus < 2""",
(ref_type, ref_no),
)
linked_pe = convert_to_list(linked_pe)
# remove reference only from specified payment
linked_pe = [x for x in linked_pe if x == payment_name] if payment_name else linked_pe
if linked_pe:
update_query = (
qb.update(per)
.set(per.allocated_amount, 0)
.set(per.modified, now())
.set(per.modified_by, frappe.session.user)
.where(
(per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no))
)
frappe.db.sql(
"""update `tabPayment Entry Reference`
set allocated_amount=0, modified=%s, modified_by=%s
where reference_doctype=%s and reference_name=%s
and docstatus < 2""",
(now(), frappe.session.user, ref_type, ref_no),
)
if payment_name:
update_query = update_query.where(per.parent == payment_name)
update_query.run()
for pe in linked_pe:
try:
pe_doc = frappe.get_doc("Payment Entry", pe)
@@ -845,13 +761,19 @@ def remove_ref_doc_link_from_pe(
msg += _("Please cancel payment entry manually first")
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
qb.update(pay).set(pay.total_allocated_amount, pe_doc.total_allocated_amount).set(
pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount
).set(pay.unallocated_amount, pe_doc.unallocated_amount).set(pay.modified, now()).set(
pay.modified_by, frappe.session.user
).where(
pay.name == pe
).run()
frappe.db.sql(
"""update `tabPayment Entry` set total_allocated_amount=%s,
base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s
where name=%s""",
(
pe_doc.total_allocated_amount,
pe_doc.base_total_allocated_amount,
pe_doc.unallocated_amount,
now(),
frappe.session.user,
pe,
),
)
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))

View File

@@ -147,15 +147,6 @@ frappe.ui.form.on('Asset', {
if (frm.doc.docstatus == 0) {
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
if (frm.doc.is_composite_asset && !frm.doc.capitalized_in) {
$('.primary-action').prop('hidden', true);
$('.form-message').text('Capitalize this asset to confirm');
frm.add_custom_button(__("Capitalize Asset"), function() {
frm.trigger("create_asset_capitalization");
});
}
}
},
@@ -177,7 +168,7 @@ frappe.ui.form.on('Asset', {
frm.set_df_property('purchase_invoice', 'read_only', 1);
frm.set_df_property('purchase_receipt', 'read_only', 1);
}
else if (frm.doc.is_existing_asset || frm.doc.is_composite_asset) {
else if (frm.doc.is_existing_asset) {
frm.toggle_reqd('purchase_receipt', 0);
frm.toggle_reqd('purchase_invoice', 0);
}
@@ -296,8 +287,7 @@ frappe.ui.form.on('Asset', {
method: "erpnext.assets.doctype.asset.asset.get_item_details",
args: {
item_code: frm.doc.item_code,
asset_category: frm.doc.asset_category,
gross_purchase_amount: frm.doc.gross_purchase_amount
asset_category: frm.doc.asset_category
},
callback: function(r, rt) {
if(r.message) {
@@ -309,17 +299,7 @@ frappe.ui.form.on('Asset', {
is_existing_asset: function(frm) {
frm.trigger("toggle_reference_doc");
},
is_composite_asset: function(frm) {
if(frm.doc.is_composite_asset) {
frm.set_value('gross_purchase_amount', 0);
frm.set_df_property('gross_purchase_amount', 'read_only', 1);
} else {
frm.set_df_property('gross_purchase_amount', 'read_only', 0);
}
frm.trigger("toggle_reference_doc");
// frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation));
},
make_schedules_editable: function(frm) {
@@ -380,19 +360,6 @@ frappe.ui.form.on('Asset', {
});
},
create_asset_capitalization: function(frm) {
frappe.call({
args: {
"asset": frm.doc.name,
},
method: "erpnext.assets.doctype.asset.asset.create_asset_capitalization",
callback: function(r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
});
},
split_asset: function(frm) {
const title = __('Split Asset');
@@ -456,11 +423,9 @@ frappe.ui.form.on('Asset', {
},
gross_purchase_amount: function(frm) {
if (frm.doc.finance_books) {
frm.doc.finance_books.forEach(d => {
frm.events.set_depreciation_rate(frm, d);
})
}
frm.doc.finance_books.forEach(d => {
frm.events.set_depreciation_rate(frm, d);
})
},
purchase_receipt: (frm) => {
@@ -539,21 +504,7 @@ frappe.ui.form.on('Asset', {
}
});
}
},
set_salvage_value_percentage_or_expected_value_after_useful_life: function(frm, row, salvage_value_percentage_changed, expected_value_after_useful_life_changed) {
if (expected_value_after_useful_life_changed) {
frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = true;
const new_salvage_value_percentage = flt((row.expected_value_after_useful_life * 100) / frm.doc.gross_purchase_amount, precision("salvage_value_percentage", row));
frappe.model.set_value(row.doctype, row.name, "salvage_value_percentage", new_salvage_value_percentage);
frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = false;
} else if (salvage_value_percentage_changed) {
frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = true;
const new_expected_value_after_useful_life = flt(frm.doc.gross_purchase_amount * (row.salvage_value_percentage / 100), precision('gross_purchase_amount'));
frappe.model.set_value(row.doctype, row.name, "expected_value_after_useful_life", new_expected_value_after_useful_life);
frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = false;
}
},
}
});
frappe.ui.form.on('Asset Finance Book', {
@@ -565,19 +516,9 @@ frappe.ui.form.on('Asset Finance Book', {
expected_value_after_useful_life: function(frm, cdt, cdn) {
const row = locals[cdt][cdn];
if (!frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life) {
frm.events.set_salvage_value_percentage_or_expected_value_after_useful_life(frm, row, false, true);
}
frm.events.set_depreciation_rate(frm, row);
},
salvage_value_percentage: function(frm, cdt, cdn) {
const row = locals[cdt][cdn];
if (!frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life) {
frm.events.set_salvage_value_percentage_or_expected_value_after_useful_life(frm, row, true, false);
}
},
frequency_of_depreciation: function(frm, cdt, cdn) {
const row = locals[cdt][cdn];
frm.events.set_depreciation_rate(frm, row);

View File

@@ -14,7 +14,6 @@
"asset_owner",
"asset_owner_company",
"is_existing_asset",
"is_composite_asset",
"supplier",
"customer",
"image",
@@ -73,8 +72,7 @@
"purchase_receipt_amount",
"default_finance_book",
"depr_entry_posting_status",
"amended_from",
"capitalized_in"
"amended_from"
],
"fields": [
{
@@ -201,7 +199,7 @@
"fieldtype": "Date",
"label": "Purchase Date",
"read_only": 1,
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset",
"read_only_depends_on": "eval:!doc.is_existing_asset",
"reqd": 1
},
{
@@ -239,12 +237,10 @@
"default": "0",
"fieldname": "calculate_depreciation",
"fieldtype": "Check",
"label": "Calculate Depreciation",
"read_only_depends_on": "eval:doc.is_composite_asset && !doc.gross_purchase_amount"
"label": "Calculate Depreciation"
},
{
"default": "0",
"depends_on": "eval:!doc.is_composite_asset",
"fieldname": "is_existing_asset",
"fieldtype": "Check",
"label": "Is Existing Asset"
@@ -496,7 +492,7 @@
"fieldname": "asset_quantity",
"fieldtype": "Int",
"label": "Asset Quantity",
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
"read_only_depends_on": "eval:!doc.is_existing_asset"
},
{
"fieldname": "depr_entry_posting_status",
@@ -514,21 +510,6 @@
"fieldname": "is_fully_depreciated",
"fieldtype": "Check",
"label": "Is Fully Depreciated"
},
{
"default": "0",
"depends_on": "eval:!doc.is_existing_asset",
"fieldname": "is_composite_asset",
"fieldtype": "Check",
"label": "Is Composite Asset"
},
{
"fieldname": "capitalized_in",
"fieldtype": "Link",
"hidden": 1,
"label": "Capitalized In",
"options": "Asset Capitalization",
"read_only": 1
}
],
"idx": 72,
@@ -557,7 +538,7 @@
"table_fieldname": "accounts"
}
],
"modified": "2023-10-03 23:28:26.732269",
"modified": "2023-08-10 20:25:09.913073",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -204,9 +204,7 @@ class Asset(AccountsController):
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
if self.item_code and not self.get("finance_books"):
finance_books = get_item_details(
self.item_code, self.asset_category, self.gross_purchase_amount
)
finance_books = get_item_details(self.item_code, self.asset_category)
self.set("finance_books", finance_books)
def validate_finance_books(self):
@@ -234,7 +232,7 @@ class Asset(AccountsController):
if not self.asset_category:
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
if not flt(self.gross_purchase_amount) and not self.is_composite_asset:
if not flt(self.gross_purchase_amount):
frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError)
if is_cwip_accounting_enabled(self.asset_category):
@@ -1166,15 +1164,6 @@ def create_asset_repair(asset, asset_name):
return asset_repair
@frappe.whitelist()
def create_asset_capitalization(asset):
asset_capitalization = frappe.new_doc("Asset Capitalization")
asset_capitalization.update(
{"target_asset": asset, "capitalization_method": "Choose a WIP composite asset"}
)
return asset_capitalization
@frappe.whitelist()
def create_asset_value_adjustment(asset, asset_category, company):
asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
@@ -1206,7 +1195,7 @@ def transfer_asset(args):
@frappe.whitelist()
def get_item_details(item_code, asset_category, gross_purchase_amount):
def get_item_details(item_code, asset_category):
asset_category_doc = frappe.get_doc("Asset Category", asset_category)
books = []
for d in asset_category_doc.finance_books:
@@ -1216,11 +1205,7 @@ def get_item_details(item_code, asset_category, gross_purchase_amount):
"depreciation_method": d.depreciation_method,
"total_number_of_depreciations": d.total_number_of_depreciations,
"frequency_of_depreciation": d.frequency_of_depreciation,
"daily_depreciation": d.daily_depreciation,
"salvage_value_percentage": d.salvage_value_percentage,
"expected_value_after_useful_life": flt(gross_purchase_amount)
* flt(d.salvage_value_percentage / 100),
"depreciation_start_date": d.depreciation_start_date or nowdate(),
"start_date": nowdate(),
}
)

View File

@@ -1686,7 +1686,6 @@ def create_asset(**args):
"location": args.location or "Test Location",
"asset_owner": args.asset_owner or "Company",
"is_existing_asset": args.is_existing_asset or 1,
"is_composite_asset": args.is_composite_asset or 0,
"asset_quantity": args.get("asset_quantity") or 1,
"depr_entry_posting_status": args.depr_entry_posting_status or "",
}

View File

@@ -15,15 +15,9 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
refresh() {
this.show_general_ledger();
if ((this.frm.doc.stock_items && this.frm.doc.stock_items.length) || !this.frm.doc.target_is_fixed_asset) {
this.show_stock_ledger();
}
if (this.frm.doc.stock_items && !this.frm.doc.stock_items.length && this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") {
this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset);
this.get_target_asset_details();
}
}
setup_queries() {
@@ -40,9 +34,18 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
});
me.frm.set_query("target_asset", function() {
return {
filters: {'is_composite_asset': 1, 'docstatus': 0 }
var filters = {};
if (me.frm.doc.target_item_code) {
filters['item_code'] = me.frm.doc.target_item_code;
}
filters['status'] = ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]];
filters['docstatus'] = 1;
return {
filters: filters
};
});
me.frm.set_query("asset", "asset_items", function() {
@@ -101,39 +104,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
return this.get_target_item_details();
}
target_asset() {
if (this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") {
this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset);
this.get_target_asset_details();
}
}
set_consumed_stock_items_tagged_to_wip_composite_asset(asset) {
var me = this;
if (asset) {
return me.frm.call({
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_items_tagged_to_wip_composite_asset",
args: {
asset: asset,
},
callback: function (r) {
if (!r.exc && r.message) {
me.frm.clear_table("stock_items");
for (let item of r.message) {
me.frm.add_child("stock_items", item);
}
refresh_field("stock_items");
me.calculate_totals();
}
}
});
}
}
item_code(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
if (cdt === "Asset Capitalization Stock Item") {
@@ -248,26 +218,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
}
}
get_target_asset_details() {
var me = this;
if (me.frm.doc.target_asset) {
return me.frm.call({
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details",
child: me.frm.doc,
args: {
asset: me.frm.doc.target_asset,
company: me.frm.doc.company,
},
callback: function (r) {
if (!r.exc) {
me.frm.refresh_fields();
}
}
});
}
}
get_consumed_stock_item_details(row) {
var me = this;

View File

@@ -8,25 +8,24 @@
"engine": "InnoDB",
"field_order": [
"title",
"company",
"naming_series",
"entry_type",
"target_item_code",
"target_asset",
"target_item_name",
"target_is_fixed_asset",
"target_has_batch_no",
"target_has_serial_no",
"column_break_9",
"capitalization_method",
"target_item_code",
"target_asset_location",
"target_asset",
"target_asset_name",
"target_asset_location",
"target_warehouse",
"target_qty",
"target_stock_uom",
"target_batch_no",
"target_serial_no",
"column_break_5",
"company",
"finance_book",
"posting_date",
"posting_time",
@@ -58,13 +57,12 @@
"label": "Title"
},
{
"depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || ((doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization')",
"fieldname": "target_item_code",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Target Item Code",
"mandatory_depends_on": "eval:(doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization'",
"options": "Item"
"options": "Item",
"reqd": 1
},
{
"depends_on": "eval:doc.target_item_code && doc.target_item_name != doc.target_item_code",
@@ -88,18 +86,16 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:(doc.target_asset && !doc.__islocal) || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')",
"fieldname": "target_asset",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Target Asset",
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset'",
"no_copy": 1,
"options": "Asset",
"read_only_depends_on": "eval:(doc.entry_type=='Decapitalization') || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset')"
"read_only": 1
},
{
"depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')",
"depends_on": "eval:doc.entry_type=='Capitalization'",
"fetch_from": "target_asset.asset_name",
"fieldname": "target_asset_name",
"fieldtype": "Data",
@@ -190,14 +186,12 @@
},
{
"default": "1",
"depends_on": "eval:doc.entry_type=='Decapitalization'",
"fieldname": "target_qty",
"fieldtype": "Float",
"label": "Target Qty",
"read_only_depends_on": "eval:doc.entry_type=='Capitalization'"
},
{
"depends_on": "eval:doc.entry_type=='Decapitalization'",
"fetch_from": "target_item_code.stock_uom",
"fieldname": "target_stock_uom",
"fieldtype": "Link",
@@ -337,26 +331,18 @@
"read_only": 1
},
{
"depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'",
"depends_on": "eval:doc.entry_type=='Capitalization'",
"fieldname": "target_asset_location",
"fieldtype": "Link",
"label": "Target Asset Location",
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'",
"options": "Location"
},
{
"depends_on": "eval:doc.entry_type=='Capitalization'",
"fieldname": "capitalization_method",
"fieldtype": "Select",
"label": "Capitalization Method",
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization'",
"options": "\nCreate a new composite asset\nChoose a WIP composite asset"
"options": "Location"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-10-03 22:55:59.461456",
"modified": "2023-06-22 14:17:07.995120",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization",

View File

@@ -53,7 +53,6 @@ class AssetCapitalization(StockController):
self.validate_posting_time()
self.set_missing_values(for_validate=True)
self.validate_target_item()
self.validate_target_asset()
self.validate_consumed_stock_item()
self.validate_consumed_asset_item()
self.validate_service_item()
@@ -64,12 +63,12 @@ class AssetCapitalization(StockController):
def before_submit(self):
self.validate_source_mandatory()
self.create_target_asset()
if self.entry_type == "Capitalization":
self.create_target_asset()
def on_submit(self):
self.update_stock_ledger()
self.make_gl_entries()
self.update_target_asset()
def on_cancel(self):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
@@ -86,11 +85,6 @@ class AssetCapitalization(StockController):
if self.meta.has_field(k) and (not self.get(k) or k in force_fields):
self.set(k, v)
target_asset_details = get_target_asset_details(self.target_asset, self.company)
for k, v in target_asset_details.items():
if self.meta.has_field(k) and (not self.get(k) or k in force_fields):
self.set(k, v)
for d in self.stock_items:
args = self.as_dict()
args.update(d.as_dict())
@@ -152,33 +146,6 @@ class AssetCapitalization(StockController):
self.validate_item(target_item)
def validate_target_asset(self):
if self.target_asset:
target_asset = self.get_asset_for_validation(self.target_asset)
if not target_asset.is_composite_asset:
frappe.throw(_("Target Asset {0} needs to be composite asset").format(target_asset.name))
if target_asset.item_code != self.target_item_code:
frappe.throw(
_("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code)
)
if target_asset.status in ("Scrapped", "Sold", "Capitalized", "Decapitalized"):
frappe.throw(
_("Target Asset {0} cannot be {1}").format(target_asset.name, target_asset.status)
)
if target_asset.docstatus == 1:
frappe.throw(_("Target Asset {0} cannot be submitted").format(target_asset.name))
elif target_asset.docstatus == 2:
frappe.throw(_("Target Asset {0} cannot be cancelled").format(target_asset.name))
if target_asset.company != self.company:
frappe.throw(
_("Target Asset {0} does not belong to company {1}").format(target_asset.name, self.company)
)
def validate_consumed_stock_item(self):
for d in self.stock_items:
if d.item_code:
@@ -203,23 +170,7 @@ class AssetCapitalization(StockController):
)
asset = self.get_asset_for_validation(d.asset)
if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"):
frappe.throw(
_("Row #{0}: Consumed Asset {1} cannot be {2}").format(d.idx, asset.name, asset.status)
)
if asset.docstatus == 0:
frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be Draft").format(d.idx, asset.name))
elif asset.docstatus == 2:
frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be cancelled").format(d.idx, asset.name))
if asset.company != self.company:
frappe.throw(
_("Row #{0}: Consumed Asset {1} does not belong to company {2}").format(
d.idx, asset.name, self.company
)
)
self.validate_asset(asset)
def validate_service_item(self):
for d in self.service_items:
@@ -254,12 +205,21 @@ class AssetCapitalization(StockController):
def get_asset_for_validation(self, asset):
return frappe.db.get_value(
"Asset",
asset,
["name", "item_code", "company", "status", "docstatus", "is_composite_asset"],
as_dict=1,
"Asset", asset, ["name", "item_code", "company", "status", "docstatus"], as_dict=1
)
def validate_asset(self, asset):
if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"):
frappe.throw(_("Asset {0} is {1}").format(asset.name, asset.status))
if asset.docstatus == 0:
frappe.throw(_("Asset {0} is Draft").format(asset.name))
if asset.docstatus == 2:
frappe.throw(_("Asset {0} is cancelled").format(asset.name))
if asset.company != self.company:
frappe.throw(_("Asset {0} does not belong to company {1}").format(asset.name, self.company))
@frappe.whitelist()
def set_warehouse_details(self):
for d in self.get("stock_items"):
@@ -525,25 +485,16 @@ class AssetCapitalization(StockController):
)
def create_target_asset(self):
if (
self.entry_type != "Capitalization"
or self.capitalization_method != "Create a new composite asset"
):
return
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
asset_doc = frappe.new_doc("Asset")
asset_doc.company = self.company
asset_doc.item_code = self.target_item_code
asset_doc.is_composite_asset = 1
asset_doc.is_existing_asset = 1
asset_doc.location = self.target_asset_location
asset_doc.available_for_use_date = self.posting_date
asset_doc.purchase_date = self.posting_date
asset_doc.gross_purchase_amount = total_target_asset_value
asset_doc.purchase_receipt_amount = total_target_asset_value
asset_doc.purchase_receipt_amount = total_target_asset_value
asset_doc.capitalized_in = self.name
asset_doc.flags.ignore_validate = True
asset_doc.insert()
@@ -559,28 +510,6 @@ class AssetCapitalization(StockController):
).format(get_link_to_form("Asset", asset_doc.name))
)
def update_target_asset(self):
if (
self.entry_type != "Capitalization"
or self.capitalization_method != "Choose a WIP composite asset"
):
return
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
asset_doc = frappe.get_doc("Asset", self.target_asset)
asset_doc.gross_purchase_amount = total_target_asset_value
asset_doc.purchase_receipt_amount = total_target_asset_value
asset_doc.capitalized_in = self.name
asset_doc.flags.ignore_validate = True
asset_doc.save()
frappe.msgprint(
_(
"Asset {0} has been updated. Please set the depreciation details if any and submit it."
).format(get_link_to_form("Asset", asset_doc.name))
)
def restore_consumed_asset_items(self):
for item in self.asset_items:
asset = frappe.get_doc("Asset", item.asset)
@@ -639,33 +568,6 @@ def get_target_item_details(item_code=None, company=None):
return out
@frappe.whitelist()
def get_target_asset_details(asset=None, company=None):
out = frappe._dict()
# Get Asset Details
asset_details = frappe._dict()
if asset:
asset_details = frappe.db.get_value("Asset", asset, ["asset_name", "item_code"], as_dict=1)
if not asset_details:
frappe.throw(_("Asset {0} does not exist").format(asset))
# Re-set item code from Asset
out.target_item_code = asset_details.item_code
# Set Asset Details
out.asset_name = asset_details.asset_name
if asset_details.item_code:
out.target_fixed_asset_account = get_asset_category_account(
"fixed_asset_account", item=asset_details.item_code, company=company
)
else:
out.target_fixed_asset_account = None
return out
@frappe.whitelist()
def get_consumed_stock_item_details(args):
if isinstance(args, string_types):
@@ -814,30 +716,3 @@ def get_service_item_details(args):
)
return out
@frappe.whitelist()
def get_items_tagged_to_wip_composite_asset(asset):
fields = [
"item_code",
"item_name",
"batch_no",
"serial_no",
"stock_qty",
"stock_uom",
"warehouse",
"cost_center",
"qty",
"valuation_rate",
"amount",
]
pi_items = frappe.get_all(
"Purchase Invoice Item", filters={"wip_composite_asset": asset}, fields=fields
)
pr_items = frappe.get_all(
"Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields
)
return pi_items + pr_items

View File

@@ -50,7 +50,6 @@ class TestAssetCapitalization(unittest.TestCase):
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Capitalization",
capitalization_method="Create a new composite asset",
target_item_code="Macbook Pro",
target_asset_location="Test Location",
stock_qty=stock_qty,
@@ -140,7 +139,6 @@ class TestAssetCapitalization(unittest.TestCase):
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Capitalization",
capitalization_method="Create a new composite asset",
target_item_code="Macbook Pro",
target_asset_location="Test Location",
stock_qty=stock_qty,
@@ -205,77 +203,6 @@ class TestAssetCapitalization(unittest.TestCase):
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
def test_capitalization_with_wip_composite_asset(self):
company = "_Test Company with perpetual inventory"
set_depreciation_settings_in_company(company=company)
stock_rate = 1000
stock_qty = 2
stock_amount = 2000
total_amount = 2000
wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset",
is_composite_asset=1,
warehouse="Stores - TCP1",
company=company,
)
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Capitalization",
capitalization_method="Choose a WIP composite asset",
target_asset=wip_composite_asset.name,
target_asset_location="Test Location",
stock_qty=stock_qty,
stock_rate=stock_rate,
service_expense_account="Expenses Included In Asset Valuation - TCP1",
company=company,
submit=1,
)
# Test Asset Capitalization values
self.assertEqual(asset_capitalization.entry_type, "Capitalization")
self.assertEqual(asset_capitalization.capitalization_method, "Choose a WIP composite asset")
self.assertEqual(asset_capitalization.target_qty, 1)
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
self.assertEqual(asset_capitalization.stock_items_total, stock_amount)
self.assertEqual(asset_capitalization.total_value, total_amount)
self.assertEqual(asset_capitalization.target_incoming_rate, total_amount)
# Test Target Asset values
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
# Test General Ledger Entries
expected_gle = {
"_Test Fixed Asset - TCP1": 2000,
"_Test Warehouse - TCP1": -2000,
}
actual_gle = get_actual_gle_dict(asset_capitalization.name)
self.assertEqual(actual_gle, expected_gle)
# Test Stock Ledger Entries
expected_sle = {
("Capitalization Source Stock Item", "_Test Warehouse - TCP1"): {
"actual_qty": -stock_qty,
"stock_value_difference": -stock_amount,
}
}
actual_sle = get_actual_sle_dict(asset_capitalization.name)
self.assertEqual(actual_sle, expected_sle)
# Cancel Asset Capitalization and make test entries and status are reversed
asset_capitalization.cancel()
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
def test_decapitalization_with_depreciation(self):
# Variables
purchase_date = "2020-01-01"
@@ -399,7 +326,6 @@ def create_asset_capitalization(**args):
asset_capitalization.update(
{
"entry_type": args.entry_type or "Capitalization",
"capitalization_method": args.capitalization_method or None,
"company": company,
"posting_date": args.posting_date or now.strftime("%Y-%m-%d"),
"posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"),

View File

@@ -12,7 +12,6 @@
"column_break_5",
"frequency_of_depreciation",
"depreciation_start_date",
"salvage_value_percentage",
"expected_value_after_useful_life",
"value_after_depreciation",
"rate_of_depreciation"
@@ -88,17 +87,12 @@
"fieldname": "daily_depreciation",
"fieldtype": "Check",
"label": "Daily Depreciation"
},
{
"fieldname": "salvage_value_percentage",
"fieldtype": "Percent",
"label": "Salvage Value Percentage"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-09-29 15:39:52.740594",
"modified": "2023-08-10 18:56:09.022246",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",

View File

@@ -475,7 +475,6 @@
"depends_on": "eval:doc.is_subcontracted",
"fieldname": "supplier_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Supplier Warehouse",
"options": "Warehouse"
},
@@ -1273,7 +1272,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2023-10-01 20:58:07.851037",
"modified": "2023-09-13 16:21:07.361700",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@@ -68,7 +68,7 @@ frappe.ui.form.on("Supplier", {
}, __("View"));
frm.add_custom_button(__('Accounts Payable'), function () {
frappe.set_route('query-report', 'Accounts Payable', { party_type: "Supplier", party: frm.doc.name });
frappe.set_route('query-report', 'Accounts Payable', { supplier: frm.doc.name });
}, __("View"));
frm.add_custom_button(__('Bank Account'), function () {

View File

@@ -35,12 +35,8 @@ def get_data(filters):
sq_item.parent,
sq_item.item_code,
sq_item.qty,
sq.currency,
sq_item.stock_qty,
sq_item.amount,
sq_item.base_rate,
sq_item.base_amount,
sq.price_list_currency,
sq_item.uom,
sq_item.stock_uom,
sq_item.request_for_quotation,
@@ -109,11 +105,7 @@ def prepare_data(supplier_quotation_data, filters):
"qty": data.get("qty"),
"price": flt(data.get("amount") * exchange_rate, float_precision),
"uom": data.get("uom"),
"price_list_currency": data.get("price_list_currency"),
"currency": data.get("currency"),
"stock_uom": data.get("stock_uom"),
"base_amount": flt(data.get("base_amount"), float_precision),
"base_rate": flt(data.get("base_rate"), float_precision),
"request_for_quotation": data.get("request_for_quotation"),
"valid_till": data.get("valid_till"),
"lead_time_days": data.get("lead_time_days"),
@@ -191,8 +183,6 @@ def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map):
def get_columns(filters):
currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
group_by_columns = [
{
"fieldname": "supplier_name",
@@ -213,18 +203,11 @@ def get_columns(filters):
columns = [
{"fieldname": "uom", "label": _("UOM"), "fieldtype": "Link", "options": "UOM", "width": 90},
{"fieldname": "qty", "label": _("Quantity"), "fieldtype": "Float", "width": 80},
{
"fieldname": "currency",
"label": _("Currency"),
"fieldtype": "Link",
"options": "Currency",
"width": 110,
},
{
"fieldname": "price",
"label": _("Price"),
"fieldtype": "Currency",
"options": "currency",
"options": "Company:company:default_currency",
"width": 110,
},
{
@@ -238,23 +221,9 @@ def get_columns(filters):
"fieldname": "price_per_unit",
"label": _("Price per Unit (Stock UOM)"),
"fieldtype": "Currency",
"options": "currency",
"options": "Company:company:default_currency",
"width": 120,
},
{
"fieldname": "base_amount",
"label": _("Price ({0})").format(currency),
"fieldtype": "Currency",
"options": "price_list_currency",
"width": 180,
},
{
"fieldname": "base_rate",
"label": _("Price Per Unit ({0})").format(currency),
"fieldtype": "Currency",
"options": "price_list_currency",
"width": 180,
},
{
"fieldname": "quotation",
"label": _("Supplier Quotation"),

View File

@@ -168,6 +168,7 @@ class AccountsController(TransactionBase):
self.validate_value("base_grand_total", ">=", 0)
validate_return(self)
self.set_total_in_words()
self.validate_all_documents_schedule()
@@ -206,75 +207,15 @@ class AccountsController(TransactionBase):
if self.doctype != "Material Request" and not self.ignore_pricing_rule:
apply_pricing_rule_on_transaction(self)
self.set_total_in_words()
def before_cancel(self):
validate_einvoice_fields(self)
def _remove_references_in_unreconcile(self):
upe = frappe.qb.DocType("Unreconcile Payment Entries")
rows = (
frappe.qb.from_(upe)
.select(upe.name, upe.parent)
.where((upe.reference_doctype == self.doctype) & (upe.reference_name == self.name))
.run(as_dict=True)
)
if rows:
references_map = frappe._dict()
for x in rows:
references_map.setdefault(x.parent, []).append(x.name)
for doc, rows in references_map.items():
unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc)
for row in rows:
unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0])
unreconcile_doc.flags.ignore_validate_update_after_submit = True
unreconcile_doc.flags.ignore_links = True
unreconcile_doc.save(ignore_permissions=True)
# delete docs upon parent doc deletion
unreconcile_docs = frappe.db.get_all("Unreconcile Payments", filters={"voucher_no": self.name})
for x in unreconcile_docs:
_doc = frappe.get_doc("Unreconcile Payments", x.name)
if _doc.docstatus == 1:
_doc.cancel()
_doc.delete()
def _remove_references_in_repost_doctypes(self):
repost_doctypes = ["Repost Payment Ledger Items", "Repost Accounting Ledger Items"]
for _doctype in repost_doctypes:
dt = frappe.qb.DocType(_doctype)
rows = (
frappe.qb.from_(dt)
.select(dt.name, dt.parent, dt.parenttype)
.where((dt.voucher_type == self.doctype) & (dt.voucher_no == self.name))
.run(as_dict=True)
)
if rows:
references_map = frappe._dict()
for x in rows:
references_map.setdefault((x.parenttype, x.parent), []).append(x.name)
for doc, rows in references_map.items():
repost_doc = frappe.get_doc(doc[0], doc[1])
for row in rows:
if _doctype == "Repost Payment Ledger Items":
repost_doc.remove(repost_doc.get("repost_vouchers", {"name": row})[0])
else:
repost_doc.remove(repost_doc.get("vouchers", {"name": row})[0])
repost_doc.flags.ignore_validate_update_after_submit = True
repost_doc.flags.ignore_links = True
repost_doc.save(ignore_permissions=True)
def on_trash(self):
self._remove_references_in_repost_doctypes()
self._remove_references_in_unreconcile()
# delete references in 'Repost Payment Ledger'
rpi = frappe.qb.DocType("Repost Payment Ledger Items")
frappe.qb.from_(rpi).delete().where(
(rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name)
).run()
# delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
@@ -967,7 +908,7 @@ class AccountsController(TransactionBase):
party_type, party, party_account, amount_field, order_doctype, order_list, include_unallocated
)
payment_entries = get_advance_payment_entries_for_regional(
payment_entries = get_advance_payment_entries(
party_type, party, party_account, order_doctype, order_list, include_unallocated
)
@@ -1464,7 +1405,7 @@ class AccountsController(TransactionBase):
"account": self.additional_discount_account,
"against": supplier_or_customer,
dr_or_cr: self.base_discount_amount,
"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company),
"cost_center": self.cost_center,
},
item=self,
)
@@ -2184,45 +2125,6 @@ class AccountsController(TransactionBase):
_("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx)
)
def check_if_fields_updated(self, fields_to_check, child_tables):
# 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
needs_repost = doc_before_update.get("is_opening") != self.is_opening
if not needs_repost:
# Parent Level Accounts excluding party account
fields_to_check += accounting_dimensions
for field in fields_to_check:
if doc_before_update.get(field) != self.get(field):
needs_repost = 1
break
if not needs_repost:
# Check for child tables
for table in child_tables:
needs_repost = check_if_child_table_updated(
doc_before_update.get(table), self.get(table), child_tables[table]
)
if needs_repost:
break
return needs_repost
@frappe.whitelist()
def repost_accounting_entries(self):
if self.repost_required:
repost_ledger = frappe.new_doc("Repost Accounting Ledger")
repost_ledger.company = self.company
repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name})
repost_ledger.insert()
repost_ledger.submit()
self.db_set("repost_required", 0)
else:
frappe.throw(_("No updates pending for reposting"))
@frappe.whitelist()
def get_tax_rate(account_head):
@@ -2446,11 +2348,6 @@ def get_advance_journal_entries(
return list(journal_entries)
@erpnext.allow_regional
def get_advance_payment_entries_for_regional(*args, **kwargs):
return get_advance_payment_entries(*args, **kwargs)
def get_advance_payment_entries(
party_type,
party,
@@ -3108,23 +3005,6 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.set_status()
def check_if_child_table_updated(
child_table_before_update, child_table_after_update, fields_to_check
):
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
# Check if any field affecting accounting entry is altered
for index, item in enumerate(child_table_after_update):
for field in fields_to_check:
if child_table_before_update[index].get(field) != item.get(field):
return True
for dimension in accounting_dimensions:
if child_table_before_update[index].get(dimension) != item.get(dimension):
return True
return False
@erpnext.allow_regional
def validate_regional(doc):
pass

View File

@@ -14,8 +14,7 @@ from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
from erpnext.stock.utils import get_incoming_rate
class QtyMismatchError(ValidationError):
@@ -515,20 +514,9 @@ class BuyingController(SubcontractingController):
)
if self.is_return:
if get_valuation_method(d.item_code) == "Moving Average":
previous_sle = get_previous_sle(
{
"item_code": d.item_code,
"warehouse": d.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
}
)
outgoing_rate = flt(previous_sle.get("valuation_rate"))
else:
outgoing_rate = get_rate_for_return(
self.doctype, self.name, d.item_code, self.return_against, item_row=d
)
outgoing_rate = get_rate_for_return(
self.doctype, self.name, d.item_code, self.return_against, item_row=d
)
sle.update({"outgoing_rate": outgoing_rate, "recalculate_rate": 1})
if d.from_warehouse:

View File

@@ -282,9 +282,7 @@ class SellingController(StockController):
last_valuation_rate_in_sales_uom = last_valuation_rate * (item.conversion_factor or 1)
if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom):
throw_message(
item.idx, item.item_name, last_valuation_rate_in_sales_uom, "valuation rate (Moving Average)"
)
throw_message(item.idx, item.item_name, last_valuation_rate_in_sales_uom, "valuation rate")
def get_item_list(self):
il = []

View File

@@ -655,7 +655,7 @@ class SubcontractingController(StockController):
{
"item_code": item.rm_item_code,
"warehouse": self.supplier_warehouse,
"actual_qty": -1 * flt(item.consumed_qty, item.precision("consumed_qty")),
"actual_qty": -1 * flt(item.consumed_qty),
"dependant_sle_voucher_detail_no": item.reference_name,
},
)

View File

@@ -190,9 +190,7 @@ class calculate_taxes_and_totals(object):
item.net_rate = item.rate
if (
not item.qty and self.doc.get("is_return") and self.doc.get("doctype") != "Purchase Receipt"
):
if not item.qty and self.doc.get("is_return"):
item.amount = flt(-1 * item.rate, item.precision("amount"))
elif not item.qty and self.doc.get("is_debit_note"):
item.amount = flt(item.rate, item.precision("amount"))

View File

@@ -382,7 +382,7 @@ def get_lead_details(lead, posting_date=None, company=None):
}
)
set_address_details(out, lead, "Lead", company=company)
set_address_details(out, lead, "Lead")
taxes_and_charges = set_taxes(
None,

View File

@@ -4,7 +4,6 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Concat_ws, Date
def execute(filters=None):
@@ -70,41 +69,53 @@ def get_columns():
def get_data(filters):
lead = frappe.qb.DocType("Lead")
address = frappe.qb.DocType("Address")
dynamic_link = frappe.qb.DocType("Dynamic Link")
query = (
frappe.qb.from_(lead)
.left_join(dynamic_link)
.on((lead.name == dynamic_link.link_name) & (dynamic_link.parenttype == "Address"))
.left_join(address)
.on(address.name == dynamic_link.parent)
.select(
lead.name,
lead.lead_name,
lead.status,
lead.lead_owner,
lead.territory,
lead.source,
lead.email_id,
lead.mobile_no,
lead.phone,
lead.owner,
lead.company,
(Concat_ws(", ", address.address_line1, address.address_line2)).as_("address"),
address.state,
address.pincode,
address.country,
)
.where(lead.company == filters.company)
.where(Date(lead.creation).between(filters.from_date, filters.to_date))
return frappe.db.sql(
"""
SELECT
`tabLead`.name,
`tabLead`.lead_name,
`tabLead`.status,
`tabLead`.lead_owner,
`tabLead`.territory,
`tabLead`.source,
`tabLead`.email_id,
`tabLead`.mobile_no,
`tabLead`.phone,
`tabLead`.owner,
`tabLead`.company,
concat_ws(', ',
trim(',' from `tabAddress`.address_line1),
trim(',' from tabAddress.address_line2)
) AS address,
`tabAddress`.state,
`tabAddress`.pincode,
`tabAddress`.country
FROM
`tabLead` left join `tabDynamic Link` on (
`tabLead`.name = `tabDynamic Link`.link_name and
`tabDynamic Link`.parenttype = 'Address')
left join `tabAddress` on (
`tabAddress`.name=`tabDynamic Link`.parent)
WHERE
company = %(company)s
AND DATE(`tabLead`.creation) BETWEEN %(from_date)s AND %(to_date)s
{conditions}
ORDER BY
`tabLead`.creation asc """.format(
conditions=get_conditions(filters)
),
filters,
as_dict=1,
)
def get_conditions(filters):
conditions = []
if filters.get("territory"):
query = query.where(lead.territory == filters.get("territory"))
conditions.append(" and `tabLead`.territory=%(territory)s")
if filters.get("status"):
query = query.where(lead.status == filters.get("status"))
conditions.append(" and `tabLead`.status=%(status)s")
return query.run(as_dict=1)
return " ".join(conditions) if conditions else ""

View File

@@ -312,7 +312,7 @@ class TestWebsiteItem(unittest.TestCase):
# check if stock details are fetched and item not in stock with warehouse set
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
self.assertFalse(bool(data.product_info["in_stock"]))
self.assertEqual(data.product_info["stock_qty"], 0)
self.assertEqual(data.product_info["stock_qty"][0][0], 0)
# disable show stock availability
setup_e_commerce_settings({"show_stock_availability": 0})
@@ -355,7 +355,7 @@ class TestWebsiteItem(unittest.TestCase):
# check if stock details are fetched and item is in stock with warehouse set
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
self.assertTrue(bool(data.product_info["in_stock"]))
self.assertEqual(data.product_info["stock_qty"], 2)
self.assertEqual(data.product_info["stock_qty"][0][0], 2)
# unset warehouse
frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "")
@@ -364,7 +364,7 @@ class TestWebsiteItem(unittest.TestCase):
# (even though it has stock in some warehouse)
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
self.assertFalse(bool(data.product_info["in_stock"]))
self.assertFalse(data.product_info["stock_qty"])
self.assertFalse(bool(data.product_info["stock_qty"]))
# disable show stock availability
setup_e_commerce_settings({"show_stock_availability": 0})

View File

@@ -5,6 +5,12 @@ frappe.ui.form.on('Website Item', {
onload: (frm) => {
// should never check Private
frm.fields_dict["website_image"].df.is_private = 0;
frm.set_query("website_warehouse", () => {
return {
filters: {"is_group": 0}
};
});
},
refresh: (frm) => {

View File

@@ -135,7 +135,7 @@
"fieldtype": "Column Break"
},
{
"description": "Show Stock availability based on this warehouse. If the parent warehouse is selected, then the system will display the consolidated available quantity of all child warehouses.",
"description": "Show Stock availability based on this warehouse.",
"fieldname": "website_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
@@ -348,7 +348,7 @@
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
"modified": "2023-09-12 14:19:22.822689",
"modified": "2022-09-30 04:01:52.090732",
"modified_by": "Administrator",
"module": "E-commerce",
"name": "Website Item",

View File

@@ -259,10 +259,6 @@ class ProductQuery:
)
def get_stock_availability(self, item):
from erpnext.templates.pages.wishlist import (
get_stock_availability as get_stock_availability_from_template,
)
"""Modify item object and add stock details."""
item.in_stock = False
warehouse = item.get("website_warehouse")
@@ -278,7 +274,11 @@ class ProductQuery:
else:
item.in_stock = True
elif warehouse:
item.in_stock = get_stock_availability_from_template(item.item_code, warehouse)
# stock item and has warehouse
actual_qty = frappe.db.get_value(
"Bin", {"item_code": item.item_code, "warehouse": item.get("website_warehouse")}, "actual_qty"
)
item.in_stock = bool(flt(actual_qty))
def get_cart_items(self):
customer = get_customer(silent=True)

View File

@@ -111,8 +111,8 @@ def place_order():
item_stock = get_web_item_qty_in_stock(item.item_code, "website_warehouse")
if not cint(item_stock.in_stock):
throw(_("{0} Not in Stock").format(item.item_code))
if item.qty > item_stock.stock_qty:
throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty, item.item_code))
if item.qty > item_stock.stock_qty[0][0]:
throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code))
sales_order.flags.ignore_permissions = True
sales_order.insert()
@@ -150,10 +150,6 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
empty_card = True
else:
warehouse = frappe.get_cached_value(
"Website Item", {"item_code": item_code}, "website_warehouse"
)
quotation_items = quotation.get("items", {"item_code": item_code})
if not quotation_items:
quotation.append(
@@ -163,13 +159,11 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
"item_code": item_code,
"qty": qty,
"additional_notes": additional_notes,
"warehouse": warehouse,
},
)
else:
quotation_items[0].qty = qty
quotation_items[0].additional_notes = additional_notes
quotation_items[0].warehouse = warehouse
apply_cart_settings(quotation=quotation)
@@ -328,10 +322,6 @@ def decorate_quotation_doc(doc):
fields = fields[2:]
d.update(frappe.db.get_value("Website Item", {"item_code": item_code}, fields, as_dict=True))
website_warehouse = frappe.get_cached_value(
"Website Item", {"item_code": item_code}, "website_warehouse"
)
d.warehouse = website_warehouse
return doc

View File

@@ -17,6 +17,7 @@ from erpnext.e_commerce.shopping_cart.cart import (
request_for_quotation,
update_cart,
)
from erpnext.tests.utils import create_test_contact_and_address
class TestShoppingCart(unittest.TestCase):
@@ -27,6 +28,7 @@ class TestShoppingCart(unittest.TestCase):
def setUp(self):
frappe.set_user("Administrator")
create_test_contact_and_address()
self.enable_shopping_cart()
if not frappe.db.exists("Website Item", {"item_code": "_Test Item"}):
make_website_item(frappe.get_cached_doc("Item", "_Test Item"))
@@ -44,57 +46,48 @@ class TestShoppingCart(unittest.TestCase):
frappe.db.sql("delete from `tabTax Rule`")
def test_get_cart_new_user(self):
self.login_as_customer(
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
)
create_address_and_contact(
address_title="_Test Address for Customer 2",
first_name="_Test Contact for Customer 2",
email="test_contact_two_customer@example.com",
customer="_Test Customer 2",
)
self.login_as_new_user()
# test if lead is created and quotation with new lead is fetched
customer = frappe.get_doc("Customer", "_Test Customer 2")
quotation = _get_cart_quotation(party=customer)
quotation = _get_cart_quotation()
self.assertEqual(quotation.quotation_to, "Customer")
self.assertEqual(
quotation.contact_person,
frappe.db.get_value("Contact", dict(email_id="test_contact_two_customer@example.com")),
frappe.db.get_value("Contact", dict(email_id="test_cart_user@example.com")),
)
self.assertEqual(quotation.contact_email, frappe.session.user)
return quotation
def test_get_cart_customer(self, customer="_Test Customer 2"):
def validate_quotation(customer_name):
def test_get_cart_customer(self):
def validate_quotation():
# test if quotation with customer is fetched
party = frappe.get_doc("Customer", customer_name)
quotation = _get_cart_quotation(party=party)
quotation = _get_cart_quotation()
self.assertEqual(quotation.quotation_to, "Customer")
self.assertEqual(quotation.party_name, customer_name)
self.assertEqual(quotation.party_name, "_Test Customer")
self.assertEqual(quotation.contact_email, frappe.session.user)
return quotation
quotation = validate_quotation(customer)
return quotation
def test_add_to_cart(self):
self.login_as_customer(
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
)
create_address_and_contact(
address_title="_Test Address for Customer 2",
first_name="_Test Contact for Customer 2",
email="test_contact_two_customer@example.com",
customer="_Test Customer 2",
)
validate_quotation()
self.login_as_customer()
quotation = validate_quotation()
return quotation
def test_add_to_cart(self):
self.login_as_customer()
# clear existing quotations
self.clear_existing_quotations()
# add first item
update_cart("_Test Item", 1)
quotation = self.test_get_cart_customer("_Test Customer 2")
quotation = self.test_get_cart_customer()
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item")
self.assertEqual(quotation.get("items")[0].qty, 1)
@@ -102,7 +95,7 @@ class TestShoppingCart(unittest.TestCase):
# add second item
update_cart("_Test Item 2", 1)
quotation = self.test_get_cart_customer("_Test Customer 2")
quotation = self.test_get_cart_customer()
self.assertEqual(quotation.get("items")[1].item_code, "_Test Item 2")
self.assertEqual(quotation.get("items")[1].qty, 1)
self.assertEqual(quotation.get("items")[1].amount, 20)
@@ -115,7 +108,7 @@ class TestShoppingCart(unittest.TestCase):
# update first item
update_cart("_Test Item", 5)
quotation = self.test_get_cart_customer("_Test Customer 2")
quotation = self.test_get_cart_customer()
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item")
self.assertEqual(quotation.get("items")[0].qty, 5)
self.assertEqual(quotation.get("items")[0].amount, 50)
@@ -128,7 +121,7 @@ class TestShoppingCart(unittest.TestCase):
# remove first item
update_cart("_Test Item", 0)
quotation = self.test_get_cart_customer("_Test Customer 2")
quotation = self.test_get_cart_customer()
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item 2")
self.assertEqual(quotation.get("items")[0].qty, 1)
@@ -139,17 +132,7 @@ class TestShoppingCart(unittest.TestCase):
@unittest.skip("Flaky in CI")
def test_tax_rule(self):
self.create_tax_rule()
self.login_as_customer(
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
)
create_address_and_contact(
address_title="_Test Address for Customer 2",
first_name="_Test Contact for Customer 2",
email="test_contact_two_customer@example.com",
customer="_Test Customer 2",
)
self.login_as_customer()
quotation = self.create_quotation()
from erpnext.accounts.party import set_taxes
@@ -337,7 +320,7 @@ class TestShoppingCart(unittest.TestCase):
if frappe.db.exists("User", email):
return
user = frappe.get_doc(
frappe.get_doc(
{
"doctype": "User",
"user_type": "Website User",
@@ -347,40 +330,6 @@ class TestShoppingCart(unittest.TestCase):
}
).insert(ignore_permissions=True)
user.add_roles("Customer")
def create_address_and_contact(**kwargs):
if not frappe.db.get_value("Address", {"address_title": kwargs.get("address_title")}):
frappe.get_doc(
{
"doctype": "Address",
"address_title": kwargs.get("address_title"),
"address_type": kwargs.get("address_type") or "Office",
"address_line1": kwargs.get("address_line1") or "Station Road",
"city": kwargs.get("city") or "_Test City",
"state": kwargs.get("state") or "Test State",
"country": kwargs.get("country") or "India",
"links": [
{"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"}
],
}
).insert()
if not frappe.db.get_value("Contact", {"first_name": kwargs.get("first_name")}):
contact = frappe.get_doc(
{
"doctype": "Contact",
"first_name": kwargs.get("first_name"),
"links": [
{"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"}
],
}
)
contact.add_email(kwargs.get("email") or "test_contact_customer@example.com", is_primary=True)
contact.add_phone(kwargs.get("phone") or "+91 0000000000", is_primary_phone=True)
contact.insert()
test_dependencies = [
"Sales Taxes and Charges Template",

View File

@@ -104,8 +104,6 @@ def get_attributes_and_values(item_code):
@frappe.whitelist(allow_guest=True)
def get_next_attribute_and_values(item_code, selected_attributes):
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
"""Find the count of Items that match the selected attributes.
Also, find the attribute values that are not applicable for further searching.
If less than equal to 10 items are found, return item_codes of those items.
@@ -170,7 +168,7 @@ def get_next_attribute_and_values(item_code, selected_attributes):
product_info = None
product_id = ""
warehouse = ""
website_warehouse = ""
if exact_match or filtered_items:
if exact_match and len(exact_match) == 1:
product_id = exact_match[0]
@@ -178,19 +176,16 @@ def get_next_attribute_and_values(item_code, selected_attributes):
product_id = list(filtered_items)[0]
if product_id:
warehouse = frappe.get_cached_value(
website_warehouse = frappe.get_cached_value(
"Website Item", {"item_code": product_id}, "website_warehouse"
)
available_qty = 0.0
if warehouse and frappe.get_cached_value("Warehouse", warehouse, "is_group") == 1:
warehouses = get_child_warehouses(warehouse)
else:
warehouses = [warehouse] if warehouse else []
for warehouse in warehouses:
available_qty += flt(
frappe.db.get_value("Bin", {"item_code": product_id, "warehouse": warehouse}, "actual_qty")
if website_warehouse:
available_qty = flt(
frappe.db.get_value(
"Bin", {"item_code": product_id, "warehouse": website_warehouse}, "actual_qty"
)
)
return {

View File

@@ -6,14 +6,7 @@
frappe.ui.form.on('Loan Repayment', {
// refresh: function(frm) {
// },
setup: function(frm) {
if (frappe.meta.has_field("Loan Repayment", "repay_from_salary")) {
frm.add_fetch("against_loan", "repay_from_salary", "repay_from_salary");
}
},
// }
onload: function(frm) {
frm.set_query('against_loan', function() {
return {

View File

@@ -80,12 +80,6 @@ class LoanRepayment(AccountsController):
if amounts.get("due_date"):
self.due_date = amounts.get("due_date")
if hasattr(self, "repay_from_salary") and hasattr(self, "payroll_payable_account"):
if self.repay_from_salary and not self.payroll_payable_account:
frappe.throw(_("Please set Payroll Payable Account in Loan Repayment"))
elif not self.repay_from_salary and self.payroll_payable_account:
self.repay_from_salary = 1
def check_future_entries(self):
future_repayment_date = frappe.db.get_value(
"Loan Repayment",

View File

@@ -228,7 +228,7 @@
},
{
"default": "0",
"description": "If enabled, the system will create material requests even if the stock exists in the 'Raw Materials Warehouse'.",
"description": "If enabled, the system won't create material requests for the available items.",
"fieldname": "ignore_existing_ordered_qty",
"fieldtype": "Check",
"label": "Ignore Available Stock"
@@ -422,7 +422,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-09-29 11:41:03.246059",
"modified": "2023-07-28 13:37:43.926686",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",

View File

@@ -1508,10 +1508,6 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
from erpnext.stock.doctype.pick_list.pick_list import get_available_item_locations
stock_uom, purchase_uom = frappe.db.get_value(
"Item", item.get("item_code"), ["stock_uom", "purchase_uom"]
)
locations = get_available_item_locations(
item.get("item_code"), warehouses, item.get("quantity"), company, ignore_validation=True
)
@@ -1522,10 +1518,6 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
if required_qty <= 0:
return
conversion_factor = 1.0
if purchase_uom != stock_uom and purchase_uom == item["uom"]:
conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"])
new_dict = copy.deepcopy(item)
quantity = required_qty if d.get("qty") > required_qty else d.get("qty")
@@ -1538,14 +1530,25 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
}
)
required_qty -= quantity / conversion_factor
required_qty -= quantity
new_mr_items.append(new_dict)
# raise purchase request for remaining qty
if required_qty:
stock_uom, purchase_uom = frappe.db.get_value(
"Item", item["item_code"], ["stock_uom", "purchase_uom"]
)
precision = frappe.get_precision("Material Request Plan Item", "quantity")
if flt(required_qty, precision) > 0:
required_qty = required_qty
if purchase_uom != stock_uom and purchase_uom == item["uom"]:
conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"])
if not (conversion_factor or frappe.flags.show_qty_in_stock_uom):
frappe.throw(
_("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format(
purchase_uom, stock_uom, item["item_code"]
)
)
required_qty = required_qty / conversion_factor
if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"):
required_qty = ceil(required_qty)
@@ -1616,25 +1619,18 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
table = frappe.qb.DocType("Production Plan")
child = frappe.qb.DocType("Material Request Plan Item")
non_completed_production_plans = get_non_completed_production_plans()
query = (
frappe.qb.from_(table)
.inner_join(child)
.on(table.name == child.parent)
.select(Sum(child.required_bom_qty))
.select(Sum(child.quantity * IfNull(child.conversion_factor, 1.0)))
.where(
(table.docstatus == 1)
& (child.item_code == item_code)
& (child.warehouse == warehouse)
& (table.status.notin(["Completed", "Closed"]))
)
)
if non_completed_production_plans:
query = query.where(table.name.isin(non_completed_production_plans))
query = query.run()
).run()
if not query:
return 0.0
@@ -1642,9 +1638,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
reserved_qty_for_production_plan = flt(query[0][0])
reserved_qty_for_production = flt(
get_reserved_qty_for_production(
item_code, warehouse, non_completed_production_plans, check_production_plan=True
)
get_reserved_qty_for_production(item_code, warehouse, check_production_plan=True)
)
if reserved_qty_for_production > reserved_qty_for_production_plan:
@@ -1653,25 +1647,6 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
return reserved_qty_for_production_plan - reserved_qty_for_production
def get_non_completed_production_plans():
table = frappe.qb.DocType("Production Plan")
child = frappe.qb.DocType("Production Plan Item")
query = (
frappe.qb.from_(table)
.inner_join(child)
.on(table.name == child.parent)
.select(table.name)
.where(
(table.docstatus == 1)
& (table.status.notin(["Completed", "Closed"]))
& (child.planned_qty > child.ordered_qty)
)
).run(as_dict=True)
return list(set([d.name for d in query]))
def get_raw_materials_of_sub_assembly_items(
item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1
):

View File

@@ -7,7 +7,6 @@ from frappe.utils import add_to_date, flt, getdate, now_datetime, nowdate
from erpnext.controllers.item_variant import create_variant
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_items_for_material_requests,
get_non_completed_production_plans,
get_sales_orders,
get_warehouse_list,
)
@@ -1040,102 +1039,6 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(after_qty, before_qty)
def test_resered_qty_for_production_plan_for_work_order(self):
from erpnext.stock.utils import get_or_make_bin
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
pln = create_production_plan(item_code="Test Production Item 1")
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(after_qty - before_qty, 1)
pln.make_work_order()
work_orders = []
for row in frappe.get_all("Work Order", filters={"production_plan": pln.name}, fields=["name"]):
wo_doc = frappe.get_doc("Work Order", row.name)
wo_doc.source_warehouse = "_Test Warehouse - _TC"
wo_doc.wip_warehouse = "_Test Warehouse 1 - _TC"
wo_doc.fg_warehouse = "_Test Warehouse - _TC"
for d in wo_doc.required_items:
d.source_warehouse = "_Test Warehouse - _TC"
make_stock_entry(
item_code=d.item_code,
qty=d.required_qty,
rate=100,
target="_Test Warehouse - _TC",
)
wo_doc.submit()
work_orders.append(wo_doc)
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(after_qty, before_qty)
rm_work_order = None
for wo_doc in work_orders:
for d in wo_doc.required_items:
if d.item_code == "Raw Material Item 1":
rm_work_order = wo_doc
break
if rm_work_order:
s = frappe.get_doc(make_se_from_wo(rm_work_order.name, "Material Transfer for Manufacture", 1))
s.submit()
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(after_qty, before_qty)
def test_resered_qty_for_production_plan_for_less_rm_qty(self):
from erpnext.stock.utils import get_or_make_bin
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
pln = create_production_plan(item_code="Test Production Item 1", planned_qty=10)
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(after_qty - before_qty, 10)
pln.make_work_order()
plans = []
for row in frappe.get_all("Work Order", filters={"production_plan": pln.name}, fields=["name"]):
wo_doc = frappe.get_doc("Work Order", row.name)
wo_doc.source_warehouse = "_Test Warehouse - _TC"
wo_doc.wip_warehouse = "_Test Warehouse 1 - _TC"
wo_doc.fg_warehouse = "_Test Warehouse - _TC"
for d in wo_doc.required_items:
d.source_warehouse = "_Test Warehouse - _TC"
d.required_qty -= 5
make_stock_entry(
item_code=d.item_code,
qty=d.required_qty,
rate=100,
target="_Test Warehouse - _TC",
)
wo_doc.submit()
plans.append(pln.name)
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(after_qty, before_qty)
completed_plans = get_non_completed_production_plans()
for plan in plans:
self.assertFalse(plan in completed_plans)
def test_resered_qty_for_production_plan_for_material_requests_with_multi_UOM(self):
from erpnext.stock.utils import get_or_make_bin
@@ -1263,64 +1166,6 @@ class TestProductionPlan(FrappeTestCase):
if row.item_code == "SubAssembly2 For SUB Test":
self.assertEqual(row.quantity, 10)
def test_transfer_and_purchase_mrp_for_purchase_uom(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
bom_tree = {
"Test FG Item INK PEN": {
"Test RM Item INK": {},
}
}
parent_bom = create_nested_bom(bom_tree, prefix="")
if not frappe.db.exists("UOM Conversion Detail", {"parent": "Test RM Item INK", "uom": "Kg"}):
doc = frappe.get_doc("Item", "Test RM Item INK")
doc.purchase_uom = "Kg"
doc.append("uoms", {"uom": "Kg", "conversion_factor": 0.5})
doc.save()
wh1 = create_warehouse("PNE Warehouse", company="_Test Company")
wh2 = create_warehouse("MBE Warehouse", company="_Test Company")
mrp_warhouse = create_warehouse("MRPBE Warehouse", company="_Test Company")
make_stock_entry(
item_code="Test RM Item INK",
qty=2,
rate=100,
target=wh1,
)
make_stock_entry(
item_code="Test RM Item INK",
qty=2,
rate=100,
target=wh2,
)
plan = create_production_plan(
item_code=parent_bom.item,
planned_qty=10,
do_not_submit=1,
warehouse="_Test Warehouse - _TC",
)
plan.for_warehouse = mrp_warhouse
items = get_items_for_material_requests(
plan.as_dict(), warehouses=[{"warehouse": wh1}, {"warehouse": wh2}]
)
for row in items:
row = frappe._dict(row)
if row.material_request_type == "Material Transfer":
self.assertTrue(row.from_warehouse in [wh1, wh2])
self.assertEqual(row.quantity, 2)
if row.material_request_type == "Purchase":
self.assertTrue(row.warehouse == mrp_warhouse)
self.assertEqual(row.quantity, 12)
def create_production_plan(**args):
"""

View File

@@ -362,10 +362,10 @@ class WorkOrder(Document):
else:
self.update_work_order_qty_in_so()
self.update_ordered_qty()
self.update_reserved_qty_for_production()
self.update_completed_qty_in_material_request()
self.update_planned_qty()
self.update_ordered_qty()
self.create_job_card()
def on_cancel(self):
@@ -1491,47 +1491,37 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
def get_reserved_qty_for_production(
item_code: str,
warehouse: str,
non_completed_production_plans: list = None,
check_production_plan: bool = False,
item_code: str, warehouse: str, check_production_plan: bool = False
) -> float:
"""Get total reserved quantity for any item in specified warehouse"""
wo = frappe.qb.DocType("Work Order")
wo_item = frappe.qb.DocType("Work Order Item")
if check_production_plan:
qty_field = wo_item.required_qty
else:
qty_field = Case()
qty_field = qty_field.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
qty_field = qty_field.else_(wo_item.required_qty - wo_item.consumed_qty)
query = (
frappe.qb.from_(wo)
.from_(wo_item)
.select(Sum(qty_field))
.select(
Sum(
Case()
.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
.else_(wo_item.required_qty - wo_item.consumed_qty)
)
)
.where(
(wo_item.item_code == item_code)
& (wo_item.parent == wo.name)
& (wo.docstatus == 1)
& (wo_item.source_warehouse == warehouse)
)
)
if check_production_plan:
query = query.where(wo.production_plan.isnotnull())
else:
query = query.where(
(wo.status.notin(["Stopped", "Completed", "Closed"]))
& (wo.status.notin(["Stopped", "Completed", "Closed"]))
& (
(wo_item.required_qty > wo_item.transferred_qty)
| (wo_item.required_qty > wo_item.consumed_qty)
)
)
)
if non_completed_production_plans:
query = query.where(wo.production_plan.isin(non_completed_production_plans))
if check_production_plan:
query = query.where(wo.production_plan.isnotnull())
return query.run()[0][0] or 0.0

View File

@@ -269,7 +269,6 @@ erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
erpnext.patches.v13_0.reset_corrupt_defaults
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
erpnext.patches.v14_0.update_reference_due_date_in_journal_entry
erpnext.patches.v14_0.france_depreciation_warning
[post_model_sync]
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
@@ -334,7 +333,7 @@ erpnext.patches.v14_0.update_company_in_ldc
erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes
erpnext.patches.v14_0.cleanup_workspaces
erpnext.patches.v14_0.enable_allow_existing_serial_no
erpnext.patches.v14_0.set_report_in_process_SOA
erpnext.patches.v14_0.set_report_in_process_SOA
erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance
erpnext.patches.v14_0.update_closing_balances #15-07-2023
execute:frappe.defaults.clear_default("fiscal_year")

View File

@@ -1,12 +0,0 @@
import click
import frappe
def execute():
if "erpnext_france" in frappe.get_installed_apps():
return
click.secho(
"Feature for region France will be remove in version-15 and moved to a separate app\n"
"Please install the app to continue using the regionnal France features: https://github.com/scopen-coop/erpnext_france.git",
fg="yellow",
)

View File

@@ -67,7 +67,6 @@ class Project(Document):
tmp_task_details.append(template_task_details)
task = self.create_task_from_template(template_task_details)
project_tasks.append(task)
self.dependency_mapping(tmp_task_details, project_tasks)
def create_task_from_template(self, task_details):
@@ -106,28 +105,36 @@ class Project(Document):
def dependency_mapping(self, template_tasks, project_tasks):
for project_task in project_tasks:
template_task = frappe.get_doc("Task", project_task.template_task)
if project_task.get("template_task"):
template_task = frappe.get_doc("Task", project_task.template_task)
else:
template_task = list(filter(lambda x: x.subject == project_task.subject, template_tasks))[0]
template_task = frappe.get_doc("Task", template_task.name)
self.check_depends_on_value(template_task, project_task, project_tasks)
self.check_for_parent_tasks(template_task, project_task, project_tasks)
def check_depends_on_value(self, template_task, project_task, project_tasks):
if template_task.get("depends_on") and not project_task.get("depends_on"):
project_template_map = {pt.template_task: pt for pt in project_tasks}
for child_task in template_task.get("depends_on"):
if project_template_map and project_template_map.get(child_task.task):
child_task_subject = frappe.db.get_value("Task", child_task.task, "subject")
corresponding_project_task = list(
filter(lambda x: x.subject == child_task_subject, project_tasks)
)
if len(corresponding_project_task):
project_task.reload() # reload, as it might have been updated in the previous iteration
project_task.append("depends_on", {"task": project_template_map.get(child_task.task).name})
project_task.append("depends_on", {"task": corresponding_project_task[0].name})
project_task.save()
def check_for_parent_tasks(self, template_task, project_task, project_tasks):
if template_task.get("parent_task") and not project_task.get("parent_task"):
for pt in project_tasks:
if pt.template_task == template_task.parent_task:
project_task.parent_task = pt.name
project_task.save()
break
parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject")
corresponding_project_task = list(
filter(lambda x: x.subject == parent_task_subject, project_tasks)
)
if len(corresponding_project_task):
project_task.parent_task = corresponding_project_task[0].name
project_task.save()
def is_row_updated(self, row, existing_task_data, fields):
if self.get("__islocal") or not existing_task_data:

View File

@@ -60,6 +60,7 @@
"fieldname": "subject",
"fieldtype": "Data",
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Subject",
"reqd": 1,
@@ -139,6 +140,7 @@
"fieldname": "parent_task",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Parent Task",
"options": "Task",
"search_index": 1
@@ -396,7 +398,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 5,
"modified": "2023-09-28 13:52:05.861175",
"modified": "2023-09-06 13:52:05.861175",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task",

View File

@@ -1,52 +1,156 @@
{
"actions": [],
"creation": "2015-04-29 04:52:48.868079",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"task",
"column_break_2",
"subject",
"project"
],
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2015-04-29 04:52:48.868079",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"fields": [
{
"fieldname": "task",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Task",
"options": "Task"
},
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "task",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Task",
"length": 0,
"no_copy": 0,
"options": "Task",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"fetch_from": "task.subject",
"fieldname": "subject",
"fieldtype": "Text",
"in_list_view": 1,
"label": "Subject",
"read_only": 1
},
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "subject",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Subject",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"fetch_from": "task.project",
"fieldname": "project",
"fieldtype": "Text",
"label": "Project",
"read_only": 1
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "project",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Project",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"istable": 1,
"links": [],
"modified": "2023-10-09 11:34:14.335853",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task Depends On",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-02-24 04:56:04.862502",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task Depends On",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
}

View File

@@ -135,15 +135,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
else {
// allow for '0' qty on Credit/Debit notes
let qty = flt(item.qty);
if (!qty) {
qty = (me.frm.doc.is_debit_note ? 1 : -1);
if (me.frm.doc.doctype !== "Purchase Receipt" && me.frm.doc.is_return === 1) {
// In case of Purchase Receipt, qty can be 0 if all items are rejected
qty = flt(item.qty);
}
}
let qty = item.qty || (me.frm.doc.is_debit_note ? 1 : -1);
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
}

View File

@@ -18,7 +18,6 @@ import "./utils/customer_quick_entry";
import "./utils/supplier_quick_entry";
import "./call_popup/call_popup";
import "./utils/dimension_tree_filter";
import "./utils/unreconcile.js";
import "./utils/barcode_scanner";
import "./telephony";
import "./templates/call_link.html";

View File

@@ -6,10 +6,8 @@ erpnext.financial_statements = {
if (data && column.fieldname=="account") {
value = data.account_name || value;
if (data.account) {
column.link_onclick =
"erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")";
}
column.link_onclick =
"erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")";
column.is_tree = true;
}

View File

@@ -666,9 +666,6 @@ erpnext.utils.update_child_items = function(opts) {
}).show();
}
erpnext.utils.map_current_doc = function(opts) {
function _map() {
if($.isArray(cur_frm.doc.items) && cur_frm.doc.items.length > 0) {

View File

@@ -1,127 +0,0 @@
frappe.provide('erpnext.accounts');
erpnext.accounts.unreconcile_payments = {
add_unreconcile_btn(frm) {
if (frm.doc.docstatus == 1) {
if(((frm.doc.doctype == "Journal Entry") && (frm.doc.voucher_type != "Journal Entry"))
|| !["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"].includes(frm.doc.doctype)
) {
return;
}
frappe.call({
"method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_references",
"args": {
"doctype": frm.doc.doctype,
"docname": frm.doc.name
},
callback: function(r) {
if (r.message) {
frm.add_custom_button(__("Un-Reconcile"), function() {
erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm);
});
}
}
});
}
},
build_selection_map(frm, selections) {
// assuming each row is an individual voucher
// pass this to server side method that creates unreconcile doc for each row
let selection_map = [];
if (['Sales Invoice', 'Purchase Invoice'].includes(frm.doc.doctype)) {
selection_map = selections.map(function(elem) {
return {
company: elem.company,
voucher_type: elem.voucher_type,
voucher_no: elem.voucher_no,
against_voucher_type: frm.doc.doctype,
against_voucher_no: frm.doc.name
};
});
} else if (['Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) {
selection_map = selections.map(function(elem) {
return {
company: elem.company,
voucher_type: frm.doc.doctype,
voucher_no: frm.doc.name,
against_voucher_type: elem.voucher_type,
against_voucher_no: elem.voucher_no,
};
});
}
return selection_map;
},
build_unreconcile_dialog(frm) {
if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) {
let child_table_fields = [
{ label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1},
{ label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 },
{ label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Currency", in_list_view: 1, read_only: 1 , options: "account_currency"},
{ label: __("Currency"), fieldname: "account_currency", fieldtype: "Currency", read_only: 1},
]
let unreconcile_dialog_fields = [
{
label: __('Allocations'),
fieldname: 'allocations',
fieldtype: 'Table',
read_only: 1,
fields: child_table_fields,
},
];
// get linked payments
frappe.call({
"method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc",
"args": {
"company": frm.doc.company,
"doctype": frm.doc.doctype,
"docname": frm.doc.name
},
callback: function(r) {
if (r.message) {
// populate child table with allocations
unreconcile_dialog_fields[0].data = r.message;
unreconcile_dialog_fields[0].get_data = function(){ return r.message};
let d = new frappe.ui.Dialog({
title: 'Un-Reconcile Allocations',
fields: unreconcile_dialog_fields,
size: 'large',
cannot_add_rows: true,
primary_action_label: 'Un-Reconcile',
primary_action(values) {
let selected_allocations = values.allocations.filter(x=>x.__checked);
if (selected_allocations.length > 0) {
let selection_map = erpnext.accounts.unreconcile_payments.build_selection_map(frm, selected_allocations);
erpnext.accounts.unreconcile_payments.create_unreconcile_docs(selection_map);
d.hide();
} else {
frappe.msgprint("No Selection");
}
}
});
d.show();
}
}
});
}
},
create_unreconcile_docs(selection_map) {
frappe.call({
"method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.create_unreconcile_doc_for_selection",
"args": {
"selections": selection_map
},
});
}
}

View File

@@ -118,7 +118,7 @@ frappe.ui.form.on("Customer", {
// custom buttons
frm.add_custom_button(__('Accounts Receivable'), function () {
frappe.set_route('query-report', 'Accounts Receivable', { party_type: "Customer", party: frm.doc.name });
frappe.set_route('query-report', 'Accounts Receivable', {customer:frm.doc.name});
}, __('View'));
frm.add_custom_button(__('Accounting Ledger'), function () {

View File

@@ -236,7 +236,6 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount",
"fieldname": "discount_and_margin",
"fieldtype": "Section Break",
"label": "Discount and Margin"
@@ -668,7 +667,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-09-27 14:02:12.332407",
"modified": "2023-02-06 11:00:07.042364",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",

View File

@@ -121,7 +121,6 @@ class TestCurrencyExchange(unittest.TestCase):
# Update Currency Exchange Rate
settings = frappe.get_single("Currency Exchange Settings")
settings.service_provider = "exchangerate.host"
settings.access_key = "12345667890"
settings.save()
# Update exchange

View File

@@ -616,7 +616,6 @@
"fieldname": "relieving_date",
"fieldtype": "Date",
"label": "Relieving Date",
"no_copy": 1,
"mandatory_depends_on": "eval:doc.status == \"Left\"",
"oldfieldname": "relieving_date",
"oldfieldtype": "Date"
@@ -823,7 +822,7 @@
"idx": 24,
"image_field": "image",
"links": [],
"modified": "2023-10-04 10:57:05.174592",
"modified": "2023-03-30 15:57:05.174592",
"modified_by": "Administrator",
"module": "Setup",
"name": "Employee",
@@ -871,4 +870,4 @@
"sort_order": "DESC",
"states": [],
"title_field": "employee_name"
}
}

View File

@@ -81,11 +81,6 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
if entries:
return flt(entries[0].exchange_rate)
if frappe.get_cached_value(
"Currency Exchange Settings", "Currency Exchange Settings", "disabled"
):
return 0.00
try:
cache = frappe.cache()
key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency)

View File

@@ -1252,7 +1252,6 @@
"depends_on": "eval: doc.is_internal_customer",
"fieldname": "set_target_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_standard_filter": 1,
"label": "Set Target Warehouse",
"no_copy": 1,
@@ -1400,7 +1399,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2023-09-04 14:15:28.363184",
"modified": "2023-06-16 14:58:55.066602",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",

View File

@@ -138,9 +138,7 @@ class DeliveryNote(SellingController):
self.validate_uom_is_integer("stock_uom", "stock_qty")
self.validate_uom_is_integer("uom", "qty")
self.validate_with_previous_doc()
if self.get("_action") == "submit":
self.validate_duplicate_serial_nos()
self.validate_duplicate_serial_nos()
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list

View File

@@ -1234,10 +1234,14 @@ class TestDeliveryNote(FrappeTestCase):
)
dn.items[0].serial_no = "\n".join(serial_nos[:2])
dn.append("items", dn.items[0].as_dict())
dn.save()
# Test - 1: ValidationError should be raised
self.assertRaises(frappe.ValidationError, dn.submit)
self.assertRaises(frappe.ValidationError, dn.save)
# Step - 4: Submit Delivery Note with unique Serial Nos
dn.items[1].serial_no = "\n".join(serial_nos[2:])
dn.save()
dn.submit()
def tearDown(self):
frappe.db.rollback()

View File

@@ -37,7 +37,7 @@ frappe.ui.form.on('Inventory Dimension', {
if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger
&& frm.doc.__onload.has_stock_ledger.length) {
let allow_to_edit_fields = ['disabled', 'fetch_from_parent',
'type_of_transaction', 'condition', 'mandatory_depends_on', 'validate_negative_stock'];
'type_of_transaction', 'condition', 'mandatory_depends_on'];
frm.fields.forEach((field) => {
if (!in_list(allow_to_edit_fields, field.df.fieldname)) {

View File

@@ -17,8 +17,6 @@
"target_fieldname",
"applicable_for_documents_tab",
"apply_to_all_doctypes",
"column_break_niy2u",
"validate_negative_stock",
"column_break_13",
"document_type",
"type_of_transaction",
@@ -175,21 +173,11 @@
"fieldname": "reqd",
"fieldtype": "Check",
"label": "Mandatory"
},
{
"fieldname": "column_break_niy2u",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "validate_negative_stock",
"fieldtype": "Check",
"label": "Validate Negative Stock"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-10-05 12:52:18.705431",
"modified": "2023-01-31 13:44:38.507698",
"modified_by": "Administrator",
"module": "Stock",
"name": "Inventory Dimension",

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