Merge branch 'develop' into fix-translation-files

This commit is contained in:
Raffael Meyer 2023-05-04 00:26:01 +02:00 committed by GitHub
commit 9fd0091b60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
116 changed files with 2545 additions and 450 deletions

View File

@ -22,4 +22,4 @@ erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
erpnext/patches/ @deepeshgarg007
.github/ @deepeshgarg007
pyproject.toml @ankush
pyproject.toml @phot0n

View File

@ -1,3 +1,4 @@
import functools
import inspect
import frappe
@ -120,12 +121,14 @@ def get_region(company=None):
You can also set global company flag in `frappe.flags.company`
"""
if company or frappe.flags.company:
return frappe.get_cached_value("Company", company or frappe.flags.company, "country")
elif frappe.flags.country:
return frappe.flags.country
else:
return frappe.get_system_settings("country")
if not company:
company = frappe.local.flags.company
if company:
return frappe.get_cached_value("Company", company, "country")
return frappe.flags.country or frappe.get_system_settings("country")
def allow_regional(fn):
@ -136,6 +139,7 @@ def allow_regional(fn):
def myfunction():
pass"""
@functools.wraps(fn)
def caller(*args, **kwargs):
overrides = frappe.get_hooks("regional_overrides", {}).get(get_region())
function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}"

View File

@ -297,7 +297,7 @@ def _make_test_records(verbose=None):
# fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
["_Test Depreciations", "Expenses", 0, None, None],
["_Test Depreciations", "Expenses", 0, "Depreciation", None],
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
# Receivable / Payable Account
["_Test Receivable", "Current Assets", 0, "Receivable", None],

View File

@ -19,6 +19,8 @@
"column_break_17",
"enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account",
"journals_section",
"merge_similar_account_heads",
"report_setting_section",
"use_custom_cash_flow",
"deferred_accounting_settings_section",
@ -38,6 +40,8 @@
"show_payment_schedule_in_print",
"currency_exchange_section",
"allow_stale",
"section_break_jpd0",
"auto_reconcile_payments",
"stale_days",
"invoicing_settings_tab",
"accounts_transactions_settings_section",
@ -57,7 +61,6 @@
"acc_frozen_upto",
"column_break_25",
"frozen_accounts_modifier",
"report_settings_sb",
"tab_break_dpet",
"show_balance_in_coa"
],
@ -170,11 +173,6 @@
"fieldtype": "Int",
"label": "Stale Days"
},
{
"fieldname": "report_settings_sb",
"fieldtype": "Section Break",
"label": "Report Settings"
},
{
"default": "0",
"description": "Only select this if you have set up the Cash Flow Mapper documents",
@ -369,6 +367,29 @@
"fieldname": "book_tax_discount_loss",
"fieldtype": "Check",
"label": "Book Tax Loss on Early Payment Discount"
},
{
"fieldname": "journals_section",
"fieldtype": "Section Break",
"label": "Journals"
},
{
"default": "0",
"description": "Rows with Same Account heads will be merged on Ledger",
"fieldname": "merge_similar_account_heads",
"fieldtype": "Check",
"label": "Merge Similar Account Heads"
},
{
"fieldname": "section_break_jpd0",
"fieldtype": "Section Break",
"label": "Payment Reconciliations"
},
{
"default": "0",
"fieldname": "auto_reconcile_payments",
"fieldtype": "Check",
"label": "Auto Reconcile Payments"
}
],
"icon": "icon-cog",
@ -376,7 +397,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-04-14 17:22:03.680886",
"modified": "2023-04-21 13:11:37.130743",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@ -56,7 +56,7 @@ class BankClearance(Document):
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date,
if(paid_from=%(account)s, paid_amount, 0) as credit,
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
if(paid_from=%(account)s, 0, received_amount) as debit,
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency

View File

@ -281,10 +281,13 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
)
elif payment_entry.payment_document == "Journal Entry":
return frappe.db.get_value(
"Journal Entry Account",
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
"sum(credit_in_account_currency)",
return abs(
frappe.db.get_value(
"Journal Entry Account",
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
"sum(debit_in_account_currency-credit_in_account_currency)",
)
or 0
)
elif payment_entry.payment_document == "Expense Claim":

View File

@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", {
setup: function(frm) {
frm.add_fetch("bank_account", "account", "account");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger"];
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset Depreciation Schedule'];
},
refresh: function(frm) {

View File

@ -69,6 +69,7 @@ class JournalEntry(AccountsController):
self.validate_empty_accounts_table()
self.set_account_and_party_balance()
self.validate_inter_company_accounts()
self.validate_depr_entry_voucher_type()
if self.docstatus == 0:
self.apply_tax_withholding()
@ -130,6 +131,13 @@ class JournalEntry(AccountsController):
if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit:
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
def validate_depr_entry_voucher_type(self):
if (
any(d.account_type == "Depreciation" for d in self.get("accounts"))
and self.voucher_type != "Depreciation Entry"
):
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
def validate_stock_accounts(self):
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
for account in stock_accounts:
@ -233,25 +241,30 @@ class JournalEntry(AccountsController):
self.remove(d)
def update_asset_value(self):
if self.voucher_type != "Depreciation Entry":
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
return
processed_assets = []
for d in self.get("accounts"):
if (
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
d.reference_type == "Asset"
and d.reference_name
and d.account_type == "Depreciation"
and d.debit
):
processed_assets.append(d.reference_name)
asset = frappe.get_doc("Asset", d.reference_name)
if asset.calculate_depreciation:
continue
depr_value = d.debit or d.credit
asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value)
fb_idx = 1
if self.finance_book:
for fb_row in asset.get("finance_books"):
if fb_row.finance_book == self.finance_book:
fb_idx = fb_row.idx
break
fb_row = asset.get("finance_books")[fb_idx - 1]
fb_row.value_after_depreciation -= d.debit
fb_row.db_update()
else:
asset.db_set("value_after_depreciation", asset.value_after_depreciation - d.debit)
asset.set_status()
@ -316,42 +329,47 @@ class JournalEntry(AccountsController):
if self.voucher_type != "Depreciation Entry":
return
processed_assets = []
for d in self.get("accounts"):
if (
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
d.reference_type == "Asset"
and d.reference_name
and d.account_type == "Depreciation"
and d.debit
):
processed_assets.append(d.reference_name)
asset = frappe.get_doc("Asset", d.reference_name)
if asset.calculate_depreciation:
je_found = False
for row in asset.get("finance_books"):
for fb_row in asset.get("finance_books"):
if je_found:
break
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
depr_schedule = get_depr_schedule(asset.name, "Active", fb_row.finance_book)
for s in depr_schedule or []:
if s.journal_entry == self.name:
s.db_set("journal_entry", None)
row.value_after_depreciation += s.depreciation_amount
row.db_update()
asset.set_status()
fb_row.value_after_depreciation += d.debit
fb_row.db_update()
je_found = True
break
if not je_found:
fb_idx = 1
if self.finance_book:
for fb_row in asset.get("finance_books"):
if fb_row.finance_book == self.finance_book:
fb_idx = fb_row.idx
break
fb_row = asset.get("finance_books")[fb_idx - 1]
fb_row.value_after_depreciation += d.debit
fb_row.db_update()
else:
depr_value = d.debit or d.credit
asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value)
asset.set_status()
asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit)
asset.set_status()
def unlink_inter_company_jv(self):
if (
@ -885,6 +903,8 @@ class JournalEntry(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0):
from erpnext.accounts.general_ledger import make_gl_entries
merge_entries = frappe.db.get_single_value("Accounts Settings", "merge_similar_account_heads")
gl_map = self.build_gl_map()
if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
update_outstanding = "No"
@ -892,7 +912,13 @@ class JournalEntry(AccountsController):
update_outstanding = "Yes"
if gl_map:
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
make_gl_entries(
gl_map,
cancel=cancel,
adv_adj=adv_adj,
merge_entries=merge_entries,
update_outstanding=update_outstanding,
)
@frappe.whitelist()
def get_balance(self, difference_account=None):

View File

@ -2,6 +2,21 @@
// For license information, please see license.txt
frappe.ui.form.on("Journal Entry Template", {
onload: function(frm) {
if(frm.is_new()) {
frappe.call({
type: "GET",
method: "erpnext.accounts.doctype.journal_entry_template.journal_entry_template.get_naming_series",
callback: function(r){
if(r.message) {
frm.set_df_property("naming_series", "options", r.message.split("\n"));
frm.set_value("naming_series", r.message.split("\n")[0]);
frm.refresh_field("naming_series");
}
}
});
}
},
refresh: function(frm) {
frappe.model.set_default_values(frm.doc);
@ -19,18 +34,6 @@ frappe.ui.form.on("Journal Entry Template", {
return { filters: filters };
});
frappe.call({
type: "GET",
method: "erpnext.accounts.doctype.journal_entry_template.journal_entry_template.get_naming_series",
callback: function(r){
if(r.message){
frm.set_df_property("naming_series", "options", r.message.split("\n"));
frm.set_value("naming_series", r.message.split("\n")[0]);
frm.refresh_field("naming_series");
}
}
});
},
voucher_type: function(frm) {
var add_accounts = function(doc, r) {

View File

@ -977,6 +977,7 @@ frappe.ui.form.on('Payment Entry', {
precision("difference_amount"));
const add_deductions = (details) => {
let row = null;
if (!write_off_row.length && difference_amount) {
row = frm.add_child("deductions");
row.account = details[account];

View File

@ -60,6 +60,7 @@ class PaymentEntry(AccountsController):
def validate(self):
self.setup_party_account_field()
self.set_missing_values()
self.set_missing_ref_details()
self.validate_payment_type()
self.validate_party_details()
self.set_exchange_rate()
@ -219,11 +220,16 @@ class PaymentEntry(AccountsController):
else self.paid_to_account_currency
)
self.set_missing_ref_details()
def set_missing_ref_details(self, force=False):
def set_missing_ref_details(
self, force: bool = False, update_ref_details_only_for: list | None = None
) -> None:
for d in self.get("references"):
if d.allocated_amount:
if update_ref_details_only_for and (
not (d.reference_doctype, d.reference_name) in update_ref_details_only_for
):
continue
ref_details = get_reference_details(
d.reference_doctype, d.reference_name, self.party_account_currency
)
@ -648,6 +654,28 @@ class PaymentEntry(AccountsController):
self.precision("base_received_amount"),
)
def calculate_base_allocated_amount_for_reference(self, d) -> float:
base_allocated_amount = 0
if d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
# This is so there are no Exchange Gain/Loss generated for such doctypes
exchange_rate = 1
if self.payment_type == "Receive":
exchange_rate = self.source_exchange_rate
elif self.payment_type == "Pay":
exchange_rate = self.target_exchange_rate
base_allocated_amount += flt(
flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
)
else:
base_allocated_amount += flt(
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
)
return base_allocated_amount
def set_total_allocated_amount(self):
if self.payment_type == "Internal Transfer":
return
@ -656,9 +684,7 @@ class PaymentEntry(AccountsController):
for d in self.get("references"):
if d.allocated_amount:
total_allocated_amount += flt(d.allocated_amount)
base_total_allocated_amount += flt(
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
)
base_total_allocated_amount += self.calculate_base_allocated_amount_for_reference(d)
self.total_allocated_amount = abs(total_allocated_amount)
self.base_total_allocated_amount = abs(base_total_allocated_amount)
@ -875,9 +901,7 @@ class PaymentEntry(AccountsController):
}
)
allocated_amount_in_company_currency = flt(
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("paid_amount")
)
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
gle.update(
{
@ -1687,7 +1711,10 @@ def get_payment_entry(
):
reference_doc = None
doc = frappe.get_doc(dt, dn)
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= 99.99:
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (
100.0 + over_billing_allowance
):
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
if not party_type:
@ -1706,6 +1733,13 @@ def get_payment_entry(
# bank or cash
bank = get_bank_cash_account(doc, bank_account)
# if default bank or cash account is not set in company master and party has default company bank account, fetch it
if party_type in ["Customer", "Supplier"] and not bank:
party_bank_account = get_party_bank_account(party_type, doc.get(scrub(party_type)))
if party_bank_account:
account = frappe.db.get_value("Bank Account", party_bank_account, "account")
bank = get_bank_cash_account(doc, account)
paid_amount, received_amount = set_paid_amount_and_received_amount(
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
)
@ -1811,6 +1845,7 @@ def get_payment_entry(
pe.setup_party_account_field()
pe.set_missing_values()
pe.set_missing_ref_details()
update_accounting_dimensions(pe, doc)
@ -1921,19 +1956,27 @@ def set_paid_amount_and_received_amount(
paid_amount = received_amount = 0
if party_account_currency == bank.account_currency:
paid_amount = received_amount = abs(outstanding_amount)
elif payment_type == "Receive":
paid_amount = abs(outstanding_amount)
if bank_amount:
received_amount = bank_amount
else:
received_amount = paid_amount * doc.get("conversion_rate", 1)
else:
received_amount = abs(outstanding_amount)
if bank_amount:
paid_amount = bank_amount
company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency")
if payment_type == "Receive":
paid_amount = abs(outstanding_amount)
if bank_amount:
received_amount = bank_amount
else:
if company_currency != bank.account_currency:
received_amount = paid_amount / doc.get("conversion_rate", 1)
else:
received_amount = paid_amount * doc.get("conversion_rate", 1)
else:
# if party account currency and bank currency is different then populate paid amount as well
paid_amount = received_amount * doc.get("conversion_rate", 1)
received_amount = abs(outstanding_amount)
if bank_amount:
paid_amount = bank_amount
else:
if company_currency != bank.account_currency:
paid_amount = received_amount / doc.get("conversion_rate", 1)
else:
# if party account currency and bank currency is different then populate paid amount as well
paid_amount = received_amount * doc.get("conversion_rate", 1)
return paid_amount, received_amount

View File

@ -51,6 +51,38 @@ class TestPaymentEntry(FrappeTestCase):
so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
self.assertEqual(so_advance_paid, 0)
def test_payment_against_sales_order_usd_to_inr(self):
so = make_sales_order(
customer="_Test Customer USD", currency="USD", qty=1, rate=100, do_not_submit=True
)
so.conversion_rate = 50
so.submit()
pe = get_payment_entry("Sales Order", so.name)
pe.source_exchange_rate = 55
pe.received_amount = 5500
pe.insert()
pe.submit()
# there should be no difference amount
pe.reload()
self.assertEqual(pe.difference_amount, 0)
self.assertEqual(pe.deductions, [])
expected_gle = dict(
(d[0], d)
for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], ["Cash - _TC", 5500.0, 0, None]]
)
self.validate_gl_entries(pe.name, expected_gle)
so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
self.assertEqual(so_advance_paid, 100)
pe.cancel()
so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
self.assertEqual(so_advance_paid, 0)
def test_payment_entry_for_blocked_supplier_invoice(self):
supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1

View File

@ -82,6 +82,32 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
this.frm.change_custom_button_type('Allocate', null, 'default');
}
// check for any running reconciliation jobs
if (this.frm.doc.receivable_payable_account) {
frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments").then((enabled) => {
if(enabled) {
this.frm.call({
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running",
"args": {
for_filter: {
company: this.frm.doc.company,
party_type: this.frm.doc.party_type,
party: this.frm.doc.party,
receivable_payable_account: this.frm.doc.receivable_payable_account
}
}
}).then(r => {
if (r.message) {
let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true);
let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]);
this.frm.dashboard.add_comment(msg, "yellow");
}
});
}
});
}
}
company() {

View File

@ -7,9 +7,12 @@ from frappe import _, msgprint, qb
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import IfNull
from frappe.utils import flt, getdate, nowdate, today
from frappe.utils import flt, get_link_to_form, getdate, nowdate, today
import erpnext
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
is_any_doc_running,
)
from erpnext.accounts.utils import (
QueryPaymentLedger,
get_outstanding_invoices,
@ -304,9 +307,7 @@ class PaymentReconciliation(Document):
}
)
@frappe.whitelist()
def reconcile(self):
self.validate_allocation()
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
@ -330,12 +331,35 @@ class PaymentReconciliation(Document):
self.make_difference_entry(payment_details)
if entry_list:
reconcile_against_document(entry_list)
reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
if dr_or_cr_notes:
reconcile_dr_cr_note(dr_or_cr_notes, self.company)
@frappe.whitelist()
def reconcile(self):
if frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
running_doc = is_any_doc_running(
dict(
company=self.company,
party_type=self.party_type,
party=self.party,
receivable_payable_account=self.receivable_payable_account,
)
)
if running_doc:
frappe.throw(
_("A Reconciliation Job {0} is running for the same filters. Cannot reconcile now").format(
get_link_to_form("Auto Reconcile", running_doc)
)
)
return
self.validate_allocation()
self.reconcile_allocations()
msgprint(_("Successfully Reconciled"))
self.get_unreconciled_entries()
def make_difference_entry(self, row):

View File

@ -205,7 +205,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
self.assertRaises(frappe.ValidationError, jv1.submit)
def test_closing_balance_with_dimensions(self):
def test_closing_balance_with_dimensions_and_test_reposting_entry(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
frappe.db.sql("delete from `tabAccount Closing Balance` where company='Test PCV Company'")
@ -299,6 +299,24 @@ class TestPeriodClosingVoucher(unittest.TestCase):
self.assertEqual(cc2_closing_balance.credit, 500)
self.assertEqual(cc2_closing_balance.credit_in_account_currency, 500)
warehouse = frappe.db.get_value("Warehouse", {"company": company}, "name")
repost_doc = frappe.get_doc(
{
"doctype": "Repost Item Valuation",
"company": company,
"posting_date": "2020-03-15",
"based_on": "Item and Warehouse",
"item_code": "Test Item 1",
"warehouse": warehouse,
}
)
self.assertRaises(frappe.ValidationError, repost_doc.save)
repost_doc.posting_date = today()
repost_doc.save()
def make_period_closing_voucher(self, posting_date=None, submit=True):
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")

View File

@ -0,0 +1,130 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Process Payment Reconciliation", {
onload: function(frm) {
// set queries
frm.set_query("party_type", function() {
return {
"filters": {
"name": ["in", Object.keys(frappe.boot.party_account_types)],
}
}
});
frm.set_query('receivable_payable_account', function(doc) {
return {
filters: {
"company": doc.company,
"is_group": 0,
"account_type": frappe.boot.party_account_types[doc.party_type]
}
};
});
frm.set_query('cost_center', function(doc) {
return {
filters: {
"company": doc.company,
"is_group": 0,
}
};
});
frm.set_query('bank_cash_account', function(doc) {
return {
filters:[
['Account', 'company', '=', doc.company],
['Account', 'is_group', '=', 0],
['Account', 'account_type', 'in', ['Bank', 'Cash']]
]
};
});
},
refresh: function(frm) {
if (frm.doc.docstatus==1 && ['Queued', 'Paused'].find(x => x == frm.doc.status)) {
let execute_btn = __("Start / Resume")
frm.add_custom_button(execute_btn, () => {
frm.call({
method: 'erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_job_for_doc',
args: {
docname: frm.doc.name
}
}).then(r => {
if(!r.exc) {
frappe.show_alert(__("Job Started"));
frm.reload_doc();
}
});
});
}
if (frm.doc.docstatus==1 && ['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) {
frm.call({
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.get_reconciled_count",
args: {
"docname": frm.docname,
}
}).then(r => {
if (r.message) {
let progress = 0;
let description = "";
if (r.message.processed) {
progress = (r.message.processed/r.message.total) * 100;
description = r.message.processed + "/" + r.message.total + " processed";
} else if (r.message.total == 0 && frm.doc.status == "Completed") {
progress = 100;
}
frm.dashboard.add_progress('Reconciliation Progress', progress, description);
}
})
}
if (frm.doc.docstatus==1 && frm.doc.status == 'Running') {
let execute_btn = __("Pause")
frm.add_custom_button(execute_btn, () => {
frm.call({
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.pause_job_for_doc",
args: {
"docname": frm.docname,
}
}).then(r => {
if (!r.exc) {
frappe.show_alert(__("Job Paused"));
frm.reload_doc()
}
});
});
}
},
company(frm) {
frm.set_value('party', '');
frm.set_value('receivable_payable_account', '');
},
party_type(frm) {
frm.set_value('party', '');
},
party(frm) {
frm.set_value('receivable_payable_account', '');
if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) {
return frappe.call({
method: "erpnext.accounts.party.get_party_account",
args: {
company: frm.doc.company,
party_type: frm.doc.party_type,
party: frm.doc.party
},
callback: (r) => {
if (!r.exc && r.message) {
frm.set_value("receivable_payable_account", r.message);
}
frm.refresh();
}
});
}
}
});

View File

@ -0,0 +1,173 @@
{
"actions": [],
"autoname": "format:ACC-PPR-{#####}",
"beta": 1,
"creation": "2023-03-30 21:28:39.793927",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"party_type",
"column_break_io6c",
"party",
"receivable_payable_account",
"filter_section",
"from_invoice_date",
"to_invoice_date",
"column_break_kegk",
"from_payment_date",
"to_payment_date",
"column_break_uj04",
"cost_center",
"bank_cash_account",
"section_break_2n02",
"status",
"error_log",
"section_break_a8yx",
"amended_from"
],
"fields": [
{
"allow_on_submit": 1,
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "\nQueued\nRunning\nPaused\nCompleted\nPartially Reconciled\nFailed\nCancelled",
"read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "party_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Party Type",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "column_break_io6c",
"fieldtype": "Column Break"
},
{
"fieldname": "party",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Party",
"options": "party_type",
"reqd": 1
},
{
"fieldname": "receivable_payable_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Receivable/Payable Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "filter_section",
"fieldtype": "Section Break",
"label": "Filters"
},
{
"fieldname": "from_invoice_date",
"fieldtype": "Date",
"label": "From Invoice Date"
},
{
"fieldname": "to_invoice_date",
"fieldtype": "Date",
"label": "To Invoice Date"
},
{
"fieldname": "column_break_kegk",
"fieldtype": "Column Break"
},
{
"fieldname": "from_payment_date",
"fieldtype": "Date",
"label": "From Payment Date"
},
{
"fieldname": "to_payment_date",
"fieldtype": "Date",
"label": "To Payment Date"
},
{
"fieldname": "column_break_uj04",
"fieldtype": "Column Break"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "bank_cash_account",
"fieldtype": "Link",
"label": "Bank/Cash Account",
"options": "Account"
},
{
"fieldname": "section_break_2n02",
"fieldtype": "Section Break",
"label": "Status"
},
{
"depends_on": "eval:doc.error_log",
"fieldname": "error_log",
"fieldtype": "Long Text",
"label": "Error Log"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Process Payment Reconciliation",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "section_break_a8yx",
"fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-04-21 17:19:30.912953",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "company"
}

View File

@ -0,0 +1,503 @@
# 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.utils import get_link_to_form
from frappe.utils.scheduler import is_scheduler_inactive
class ProcessPaymentReconciliation(Document):
def validate(self):
self.validate_receivable_payable_account()
self.validate_bank_cash_account()
def validate_receivable_payable_account(self):
if self.receivable_payable_account:
if self.company != frappe.db.get_value("Account", self.receivable_payable_account, "company"):
frappe.throw(
_("Receivable/Payable Account: {0} doesn't belong to company {1}").format(
frappe.bold(self.receivable_payable_account), frappe.bold(self.company)
)
)
def validate_bank_cash_account(self):
if self.bank_cash_account:
if self.company != frappe.db.get_value("Account", self.bank_cash_account, "company"):
frappe.throw(
_("Bank/Cash Account {0} doesn't belong to company {1}").format(
frappe.bold(self.bank_cash_account), frappe.bold(self.company)
)
)
def before_save(self):
self.status = ""
self.error_log = ""
def on_submit(self):
self.db_set("status", "Queued")
self.db_set("error_log", None)
def on_cancel(self):
self.db_set("status", "Cancelled")
log = frappe.db.get_value(
"Process Payment Reconciliation Log", filters={"process_pr": self.name}
)
if log:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Cancelled")
@frappe.whitelist()
def get_reconciled_count(docname: str | None = None) -> float:
current_status = {}
if docname:
reconcile_log = frappe.db.get_value(
"Process Payment Reconciliation Log", filters={"process_pr": docname}, fieldname="name"
)
if reconcile_log:
res = frappe.get_all(
"Process Payment Reconciliation Log",
filters={"name": reconcile_log},
fields=["reconciled_entries", "total_allocations"],
as_list=1,
)
current_status["processed"], current_status["total"] = res[0]
return current_status
def get_pr_instance(doc: str):
process_payment_reconciliation = frappe.get_doc("Process Payment Reconciliation", doc)
pr = frappe.get_doc("Payment Reconciliation")
fields = [
"company",
"party_type",
"party",
"receivable_payable_account",
"from_invoice_date",
"to_invoice_date",
"from_payment_date",
"to_payment_date",
]
d = {}
for field in fields:
d[field] = process_payment_reconciliation.get(field)
pr.update(d)
pr.invoice_limit = 1000
pr.payment_limit = 1000
return pr
def is_job_running(job_name: str) -> bool:
jobs = frappe.db.get_all("RQ Job", filters={"status": ["in", ["started", "queued"]]})
for x in jobs:
if x.job_name == job_name:
return True
return False
@frappe.whitelist()
def pause_job_for_doc(docname: str | None = None):
if docname:
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Paused")
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
if log:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Paused")
@frappe.whitelist()
def trigger_job_for_doc(docname: str | None = None):
"""
Trigger background job
"""
if not docname:
return
if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
frappe.throw(
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
get_link_to_form("Accounts Settings", "Accounts Settings")
)
)
return
if not is_scheduler_inactive():
if frappe.db.get_value("Process Payment Reconciliation", docname, "status") == "Queued":
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running")
job_name = f"start_processing_{docname}"
if not is_job_running(job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
queue="long",
is_async=True,
job_name=job_name,
enqueue_after_commit=True,
doc=docname,
)
elif frappe.db.get_value("Process Payment Reconciliation", docname, "status") == "Paused":
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running")
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
if log:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Running")
# Resume tasks for running doc
job_name = f"start_processing_{docname}"
if not is_job_running(job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
queue="long",
is_async=True,
job_name=job_name,
doc=docname,
)
else:
frappe.msgprint(_("Scheduler is Inactive. Can't trigger job now."))
def trigger_reconciliation_for_queued_docs():
"""
Will be called from Cron Job
Fetch queued docs and start reconciliation process for each one
"""
if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
frappe.throw(
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
get_link_to_form("Accounts Settings", "Accounts Settings")
)
)
return
if not is_scheduler_inactive():
# Get all queued documents
all_queued = frappe.db.get_all(
"Process Payment Reconciliation",
filters={"docstatus": 1, "status": "Queued"},
order_by="creation desc",
as_list=1,
)
docs_to_trigger = []
unique_filters = set()
queue_size = 5
fields = ["company", "party_type", "party", "receivable_payable_account"]
def get_filters_as_tuple(fields, doc):
filters = ()
for x in fields:
filters += tuple(doc.get(x))
return filters
for x in all_queued:
doc = frappe.get_doc("Process Payment Reconciliation", x)
filters = get_filters_as_tuple(fields, doc)
if filters not in unique_filters:
unique_filters.add(filters)
docs_to_trigger.append(doc.name)
if len(docs_to_trigger) == queue_size:
break
# trigger reconcilation process for queue_size unique filters
for doc in docs_to_trigger:
trigger_job_for_doc(doc)
else:
frappe.msgprint(_("Scheduler is Inactive. Can't trigger jobs now."))
def reconcile_based_on_filters(doc: None | str = None) -> None:
"""
Identify current state of document and execute next tasks in background
"""
if doc:
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
if not log:
log = frappe.new_doc("Process Payment Reconciliation Log")
log.process_pr = doc
log.status = "Running"
log = log.save()
job_name = f"process_{doc}_fetch_and_allocate"
if not is_job_running(job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
queue="long",
timeout="3600",
is_async=True,
job_name=job_name,
enqueue_after_commit=True,
doc=doc,
)
else:
res = frappe.get_all(
"Process Payment Reconciliation Log",
filters={"name": log},
fields=["allocated", "reconciled"],
as_list=1,
)
allocated, reconciled = res[0]
if not allocated:
job_name = f"process__{doc}_fetch_and_allocate"
if not is_job_running(job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
queue="long",
timeout="3600",
is_async=True,
job_name=job_name,
enqueue_after_commit=True,
doc=doc,
)
elif not reconciled:
allocation = get_next_allocation(log)
if allocation:
reconcile_job_name = (
f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
)
else:
reconcile_job_name = f"process_{doc}_reconcile"
if not is_job_running(reconcile_job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
queue="long",
timeout="3600",
is_async=True,
job_name=reconcile_job_name,
enqueue_after_commit=True,
doc=doc,
)
elif reconciled:
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
def get_next_allocation(log: str) -> list:
if log:
allocations = []
next = frappe.db.get_all(
"Process Payment Reconciliation Log Allocations",
filters={"parent": log, "reconciled": 0},
fields=["reference_type", "reference_name"],
order_by="idx",
limit=1,
)
if next:
allocations = frappe.db.get_all(
"Process Payment Reconciliation Log Allocations",
filters={
"parent": log,
"reconciled": 0,
"reference_type": next[0].reference_type,
"reference_name": next[0].reference_name,
},
fields=["*"],
order_by="idx",
)
return allocations
return []
def fetch_and_allocate(doc: str) -> None:
"""
Fetch Invoices and Payments based on filters applied. FIFO ordering is used for allocation.
"""
if doc:
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
if log:
if not frappe.db.get_value("Process Payment Reconciliation Log", log, "allocated"):
reconcile_log = frappe.get_doc("Process Payment Reconciliation Log", log)
pr = get_pr_instance(doc)
pr.get_unreconciled_entries()
if len(pr.invoices) > 0 and len(pr.payments) > 0:
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
for x in pr.get("allocation"):
reconcile_log.append(
"allocations",
x.as_dict().update(
{
"parenttype": "Process Payment Reconciliation Log",
"parent": reconcile_log.name,
"name": None,
"reconciled": False,
}
),
)
reconcile_log.allocated = True
reconcile_log.total_allocations = len(reconcile_log.get("allocations"))
reconcile_log.reconciled_entries = 0
reconcile_log.save()
# generate reconcile job name
allocation = get_next_allocation(log)
if allocation:
reconcile_job_name = (
f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
)
else:
reconcile_job_name = f"process_{doc}_reconcile"
if not is_job_running(reconcile_job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
queue="long",
timeout="3600",
is_async=True,
job_name=reconcile_job_name,
enqueue_after_commit=True,
doc=doc,
)
def reconcile(doc: None | str = None) -> None:
if doc:
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
if log:
res = frappe.get_all(
"Process Payment Reconciliation Log",
filters={"name": log},
fields=["reconciled_entries", "total_allocations"],
as_list=1,
limit=1,
)
reconciled_entries, total_allocations = res[0]
if reconciled_entries != total_allocations:
try:
# Fetch next allocation
allocations = get_next_allocation(log)
pr = get_pr_instance(doc)
# pass allocation to PR instance
for x in allocations:
pr.append("allocation", x)
# reconcile
pr.reconcile_allocations(skip_ref_details_update_for_pe=True)
# If Payment Entry, update details only for newly linked references
# This is for performance
if allocations[0].reference_type == "Payment Entry":
references = [(x.invoice_type, x.invoice_number) for x in allocations]
pe = frappe.get_doc(allocations[0].reference_type, allocations[0].reference_name)
pe.flags.ignore_validate_update_after_submit = True
pe.set_missing_ref_details(update_ref_details_only_for=references)
pe.save()
# Update reconciled flag
allocation_names = [x.name for x in allocations]
ppa = qb.DocType("Process Payment Reconciliation Log Allocations")
qb.update(ppa).set(ppa.reconciled, True).where(ppa.name.isin(allocation_names)).run()
# Update reconciled count
reconciled_count = frappe.db.count(
"Process Payment Reconciliation Log Allocations", filters={"parent": log, "reconciled": True}
)
frappe.db.set_value(
"Process Payment Reconciliation Log", log, "reconciled_entries", reconciled_count
)
except Exception as err:
# Update the parent doc about the exception
frappe.db.rollback()
traceback = frappe.get_traceback()
if traceback:
message = "Traceback: <br>" + traceback
frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message)
frappe.db.set_value(
"Process Payment Reconciliation",
doc,
"error_log",
message,
)
if reconciled_entries and total_allocations and reconciled_entries < total_allocations:
frappe.db.set_value(
"Process Payment Reconciliation Log", log, "status", "Partially Reconciled"
)
frappe.db.set_value(
"Process Payment Reconciliation",
doc,
"status",
"Partially Reconciled",
)
else:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Failed")
frappe.db.set_value(
"Process Payment Reconciliation",
doc,
"status",
"Failed",
)
finally:
if reconciled_entries == total_allocations:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Reconciled")
frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True)
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
else:
if not (frappe.db.get_value("Process Payment Reconciliation", doc, "status") == "Paused"):
# trigger next batch in job
# generate reconcile job name
allocation = get_next_allocation(log)
if allocation:
reconcile_job_name = (
f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
)
else:
reconcile_job_name = f"process_{doc}_reconcile"
if not is_job_running(reconcile_job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
queue="long",
timeout="3600",
is_async=True,
job_name=reconcile_job_name,
enqueue_after_commit=True,
doc=doc,
)
else:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Reconciled")
frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True)
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
@frappe.whitelist()
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
running_doc = None
if for_filter:
if type(for_filter) == str:
for_filter = frappe.json.loads(for_filter)
running_doc = frappe.db.get_value(
"Process Payment Reconciliation",
filters={
"docstatus": 1,
"status": ["in", ["Running", "Paused"]],
"company": for_filter.get("company"),
"party_type": for_filter.get("party_type"),
"party": for_filter.get("party"),
"receivable_payable_account": for_filter.get("receivable_payable_account"),
},
fieldname="name",
)
else:
running_doc = frappe.db.get_value(
"Process Payment Reconciliation", filters={"docstatus": 1, "status": "Running"}
)
return running_doc

View File

@ -0,0 +1,15 @@
from frappe import _
def get_data():
return {
"fieldname": "process_pr",
"transactions": [
{
"label": _("Reconciliation Logs"),
"items": [
"Process Payment Reconciliation Log",
],
},
],
}

View File

@ -0,0 +1,15 @@
frappe.listview_settings['Process Payment Reconciliation'] = {
add_fields: ["status"],
get_indicator: function(doc) {
let colors = {
'Queued': 'orange',
'Paused': 'orange',
'Completed': 'green',
'Partially Reconciled': 'orange',
'Running': 'blue',
'Failed': 'red',
};
let status = doc.status;
return [__(status), colors[status], 'status,=,'+status];
},
};

View File

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

View File

@ -0,0 +1,17 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Process Payment Reconciliation Log", {
refresh(frm) {
if (['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) {
let progress = 0;
if (frm.doc.reconciled_entries != 0) {
progress = frm.doc.reconciled_entries / frm.doc.total_allocations * 100;
} else if(frm.doc.total_allocations == 0 && frm.doc.status == "Completed"){
progress = 100;
}
frm.dashboard.add_progress(__('Reconciliation Progress'), progress);
}
},
});

View File

@ -0,0 +1,137 @@
{
"actions": [],
"autoname": "format:PPR-LOG-{##}",
"beta": 1,
"creation": "2023-03-13 15:00:09.149681",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"process_pr",
"section_break_fvdw",
"status",
"tasks_section",
"allocated",
"reconciled",
"column_break_yhin",
"total_allocations",
"reconciled_entries",
"section_break_4ywv",
"error_log",
"allocations_section",
"allocations"
],
"fields": [
{
"fieldname": "allocations",
"fieldtype": "Table",
"label": "Allocations",
"options": "Process Payment Reconciliation Log Allocations",
"read_only": 1
},
{
"default": "0",
"description": "All allocations have been successfully reconciled",
"fieldname": "reconciled",
"fieldtype": "Check",
"label": "Reconciled",
"read_only": 1
},
{
"fieldname": "total_allocations",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Total Allocations",
"read_only": 1
},
{
"default": "0",
"description": "Invoices and Payments have been Fetched and Allocated",
"fieldname": "allocated",
"fieldtype": "Check",
"label": "Allocated",
"read_only": 1
},
{
"fieldname": "reconciled_entries",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Reconciled Entries",
"read_only": 1
},
{
"fieldname": "tasks_section",
"fieldtype": "Section Break",
"label": "Tasks"
},
{
"fieldname": "allocations_section",
"fieldtype": "Section Break",
"label": "Allocations"
},
{
"fieldname": "column_break_yhin",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_4ywv",
"fieldtype": "Section Break"
},
{
"depends_on": "eval:doc.error_log",
"fieldname": "error_log",
"fieldtype": "Long Text",
"label": "Reconciliation Error Log",
"read_only": 1
},
{
"fieldname": "process_pr",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Parent Document",
"options": "Process Payment Reconciliation",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "section_break_fvdw",
"fieldtype": "Section Break",
"label": "Status"
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Running\nPaused\nReconciled\nPartially Reconciled\nFailed\nCancelled",
"read_only": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-04-21 17:36:26.642617",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation Log",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "allocated, reconciled, total_allocations, reconciled_entries",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

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

View File

@ -0,0 +1,15 @@
frappe.listview_settings['Process Payment Reconciliation Log'] = {
add_fields: ["status"],
get_indicator: function(doc) {
var colors = {
'Partially Reconciled': 'orange',
'Paused': 'orange',
'Reconciled': 'green',
'Failed': 'red',
'Cancelled': 'red',
'Running': 'blue',
};
let status = doc.status;
return [__(status), colors[status], "status,=,"+status];
},
};

View File

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

View File

@ -0,0 +1,170 @@
{
"actions": [],
"creation": "2023-03-13 13:51:27.351463",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"reference_type",
"reference_name",
"reference_row",
"column_break_3",
"invoice_type",
"invoice_number",
"section_break_6",
"allocated_amount",
"unreconciled_amount",
"column_break_8",
"amount",
"is_advance",
"section_break_5",
"difference_amount",
"column_break_7",
"difference_account",
"exchange_rate",
"currency",
"reconciled"
],
"fields": [
{
"fieldname": "reference_type",
"fieldtype": "Link",
"label": "Reference Type",
"options": "DocType",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Name",
"options": "reference_type",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "reference_row",
"fieldtype": "Data",
"hidden": 1,
"label": "Reference Row",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "invoice_type",
"fieldtype": "Link",
"label": "Invoice Type",
"options": "DocType",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "invoice_number",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Invoice Number",
"options": "invoice_type",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Allocated Amount",
"options": "currency",
"reqd": 1
},
{
"fieldname": "unreconciled_amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Unreconciled Amount",
"options": "currency",
"read_only": 1
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Amount",
"options": "currency",
"read_only": 1
},
{
"fieldname": "is_advance",
"fieldtype": "Data",
"hidden": 1,
"label": "Is Advance",
"read_only": 1
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"fieldname": "difference_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Difference Amount",
"options": "Currency",
"read_only": 1
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"fieldname": "difference_account",
"fieldtype": "Link",
"label": "Difference Account",
"options": "Account",
"read_only": 1
},
{
"fieldname": "exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"read_only": 1
},
{
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Currency",
"options": "Currency"
},
{
"default": "0",
"fieldname": "reconciled",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Reconciled"
}
],
"istable": 1,
"links": [],
"modified": "2023-03-20 21:05:43.121945",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation Log Allocations",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

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

View File

@ -15,7 +15,12 @@
</div>
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
<div>
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party_name[0] }}</b></h5>
{% if filters.party[0] == filters.party_name[0] %}
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party_name[0] }}</b></h5>
{% else %}
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party[0] }}</b></h5>
<h5 style="float: left; margin-left:15px">{{ _("Customer Name: ") }} <b>{{filters.party_name[0] }}</b></h5>
{% endif %}
<h5 style="float: right;">
{{ _("Date: ") }}
<b>{{ frappe.format(filters.from_date, 'Date')}}

View File

@ -36,6 +36,8 @@
"terms_and_conditions",
"section_break_1",
"enable_auto_email",
"column_break_ocfq",
"sender",
"section_break_18",
"frequency",
"filter_duration",
@ -298,10 +300,20 @@
"fieldname": "show_net_values_in_party_account",
"fieldtype": "Check",
"label": "Show Net Values in Party Account"
},
{
"fieldname": "sender",
"fieldtype": "Link",
"label": "Sender",
"options": "Email Account"
},
{
"fieldname": "column_break_ocfq",
"fieldtype": "Column Break"
}
],
"links": [],
"modified": "2022-11-10 17:44:17.165991",
"modified": "2023-04-26 12:46:43.645455",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@ -334,7 +334,7 @@ def send_emails(document_name, from_scheduler=False):
queue="short",
method=frappe.sendmail,
recipients=recipients,
sender=frappe.session.user,
sender=doc.sender or frappe.session.user,
cc=cc,
subject=subject,
message=message,

View File

@ -27,7 +27,7 @@
},
{
"fieldname": "billing_email",
"fieldtype": "Read Only",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Billing Email"
},
@ -41,7 +41,7 @@
],
"istable": 1,
"links": [],
"modified": "2023-03-13 00:12:34.508086",
"modified": "2023-04-26 13:02:41.964499",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts Customer",

View File

@ -89,6 +89,7 @@
"column_break8",
"grand_total",
"rounding_adjustment",
"use_company_roundoff_cost_center",
"rounded_total",
"in_words",
"total_advance",
@ -1559,13 +1560,19 @@
"fieldname": "only_include_allocated_payments",
"fieldtype": "Check",
"label": "Only Include Allocated Payments"
},
{
"default": "0",
"fieldname": "use_company_roundoff_cost_center",
"fieldtype": "Check",
"label": "Use Company Default Round Off Cost Center"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2023-04-03 22:57:14.074982",
"modified": "2023-04-28 12:57:50.832598",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@ -978,7 +978,7 @@ class PurchaseInvoice(BuyingController):
def make_precision_loss_gl_entry(self, gl_entries):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
)
precision_loss = self.get("base_net_total") - flt(
@ -992,7 +992,9 @@ class PurchaseInvoice(BuyingController):
"account": round_off_account,
"against": self.supplier,
"credit": precision_loss,
"cost_center": self.cost_center or round_off_cost_center,
"cost_center": round_off_cost_center
if self.use_company_roundoff_cost_center
else self.cost_center or round_off_cost_center,
"remarks": _("Net total calculation precision loss"),
}
)
@ -1386,7 +1388,7 @@ class PurchaseInvoice(BuyingController):
not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment
):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
)
gl_entries.append(
@ -1396,7 +1398,9 @@ class PurchaseInvoice(BuyingController):
"against": self.supplier,
"debit_in_account_currency": self.rounding_adjustment,
"debit": self.base_rounding_adjustment,
"cost_center": self.cost_center or round_off_cost_center,
"cost_center": round_off_cost_center
if self.use_company_roundoff_cost_center
else (self.cost_center or round_off_cost_center),
},
item=self,
)

View File

@ -334,6 +334,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
}
make_inter_company_invoice() {
let me = this;
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_inter_company_purchase_invoice",
frm: me.frm

View File

@ -79,6 +79,7 @@
"column_break5",
"grand_total",
"rounding_adjustment",
"use_company_roundoff_cost_center",
"rounded_total",
"in_words",
"total_advance",
@ -2135,6 +2136,12 @@
"fieldname": "only_include_allocated_payments",
"fieldtype": "Check",
"label": "Only Include Allocated Payments"
},
{
"default": "0",
"fieldname": "use_company_roundoff_cost_center",
"fieldtype": "Check",
"label": "Use Company default Cost Center for Round off"
}
],
"icon": "fa fa-file-text",
@ -2147,7 +2154,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2023-04-03 22:55:14.206473",
"modified": "2023-04-28 14:15:59.901154",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@ -1464,7 +1464,7 @@ class SalesInvoice(SellingController):
and not self.is_internal_transfer()
):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Sales Invoice", self.name
self.company, "Sales Invoice", self.name, self.use_company_roundoff_cost_center
)
gl_entries.append(
@ -1476,7 +1476,9 @@ class SalesInvoice(SellingController):
self.rounding_adjustment, self.precision("rounding_adjustment")
),
"credit": flt(self.base_rounding_adjustment, self.precision("base_rounding_adjustment")),
"cost_center": self.cost_center or round_off_cost_center,
"cost_center": round_off_cost_center
if self.use_company_roundoff_cost_center
else (self.cost_center or round_off_cost_center),
},
item=self,
)

View File

@ -215,7 +215,7 @@ def get_tax_row_for_tds(tax_details, tax_amount):
}
def get_lower_deduction_certificate(tax_details, pan_no):
def get_lower_deduction_certificate(company, tax_details, pan_no):
ldc_name = frappe.db.get_value(
"Lower Deduction Certificate",
{
@ -223,6 +223,7 @@ def get_lower_deduction_certificate(tax_details, pan_no):
"tax_withholding_category": tax_details.tax_withholding_category,
"valid_from": (">=", tax_details.from_date),
"valid_upto": ("<=", tax_details.to_date),
"company": company,
},
"name",
)
@ -255,7 +256,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_amount = 0
if party_type == "Supplier":
ldc = get_lower_deduction_certificate(tax_details, pan_no)
ldc = get_lower_deduction_certificate(inv.company, tax_details, pan_no)
if tax_deducted:
net_total = inv.tax_withholding_net_total
if ldc:

View File

@ -475,7 +475,9 @@ def update_accounting_dimensions(round_off_gle):
round_off_gle[dimension] = dimension_values.get(dimension)
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no):
def get_round_off_account_and_cost_center(
company, voucher_type, voucher_no, use_company_default=False
):
round_off_account, round_off_cost_center = frappe.get_cached_value(
"Company", company, ["round_off_account", "round_off_cost_center"]
) or [None, None]
@ -483,7 +485,7 @@ def get_round_off_account_and_cost_center(company, voucher_type, voucher_no):
meta = frappe.get_meta(voucher_type)
# Give first preference to parent cost center for round off GLE
if meta.has_field("cost_center"):
if not use_company_default and meta.has_field("cost_center"):
parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center")
if parent_cost_center:
round_off_cost_center = parent_cost_center

View File

@ -259,6 +259,8 @@ def set_address_details(
)
if doctype in TRANSACTION_TYPES:
# required to set correct region
frappe.flags.company = company
get_regional_address_details(party_details, doctype, company)
return party_address, shipping_address

View File

@ -114,28 +114,6 @@ def get_assets(filters):
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.asset_category,
ifnull(sum(case when ds.schedule_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
ds.depreciation_amount
else
0
end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s and ds.schedule_date <= a.disposal_date then
ds.depreciation_amount
else
0
end), 0) as depreciation_eliminated_during_the_period,
ifnull(sum(case when ds.schedule_date >= %(from_date)s and ds.schedule_date <= %(to_date)s
and (ifnull(a.disposal_date, 0) = 0 or ds.schedule_date <= a.disposal_date) then
ds.depreciation_amount
else
0
end), 0) as depreciation_amount_during_the_period
from `tabAsset` a, `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and ads.asset = a.name and ads.docstatus=1 and ads.name = ds.parent and ifnull(ds.journal_entry, '') != ''
group by a.asset_category
union
SELECT a.asset_category,
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
gle.debit
else
@ -160,7 +138,7 @@ def get_assets(filters):
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where a.docstatus=1 and a.company=%(company)s and a.calculate_depreciation=0 and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
group by a.asset_category
union
SELECT a.asset_category,

View File

@ -80,7 +80,7 @@ def get_entries(filters):
payment_entries = frappe.db.sql(
"""SELECT
"Payment Entry", name, posting_date, reference_no, clearance_date, party,
if(paid_from=%(account)s, paid_amount * -1, received_amount)
if(paid_from=%(account)s, ((paid_amount * -1) - total_taxes_and_charges) , received_amount)
FROM
`tabPayment Entry`
WHERE

View File

@ -538,13 +538,20 @@ def apply_additional_conditions(doctype, query, from_date, ignore_closing_entrie
query = query.where(gl_entry.cost_center.isin(filters.cost_center))
if filters.get("include_default_book_entries"):
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
frappe.throw(
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
)
query = query.where(
(gl_entry.finance_book.isin([cstr(filters.finance_book), cstr(filters.company_fb), ""]))
(gl_entry.finance_book.isin([cstr(filters.finance_book), cstr(company_fb)]))
| (gl_entry.finance_book.isnull())
)
else:
query = query.where(
(gl_entry.finance_book.isin([cstr(filters.company_fb), ""])) | (gl_entry.finance_book.isnull())
(gl_entry.finance_book.isin([cstr(filters.finance_book)])) | (gl_entry.finance_book.isnull())
)
if accounting_dimensions:

View File

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

View File

@ -244,13 +244,23 @@ def get_conditions(filters):
if filters.get("project"):
conditions.append("project in %(project)s")
if filters.get("finance_book"):
if filters.get("include_default_book_entries"):
conditions.append(
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
)
if filters.get("include_default_book_entries"):
if filters.get("finance_book"):
if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr(
filters.get("company_fb")
):
frappe.throw(
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
)
else:
conditions.append("(finance_book in (%(finance_book)s) OR finance_book IS NULL)")
else:
conditions.append("finance_book in (%(finance_book)s)")
conditions.append("(finance_book in (%(company_fb)s) OR finance_book IS NULL)")
else:
if filters.get("finance_book"):
conditions.append("(finance_book in (%(finance_book)s) OR finance_book IS NULL)")
else:
conditions.append("(finance_book IS NULL)")
if not filters.get("show_cancelled_entries"):
conditions.append("is_cancelled = 0")

View File

@ -73,7 +73,7 @@ frappe.query_reports["Gross Profit"] = {
if (column.fieldname == "sales_invoice" && column.options == "Item" && data && data.indent == 0) {
column._options = "Sales Invoice";
} else {
column._options = "Item";
column._options = "";
}
value = default_formatter(value, row, column, data);

View File

@ -250,7 +250,7 @@ def get_columns(group_wise_columns, filters):
"label": _("Warehouse"),
"fieldname": "warehouse",
"fieldtype": "Link",
"options": "warehouse",
"options": "Warehouse",
"width": 100,
},
"qty": {"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 80},
@ -305,7 +305,8 @@ def get_columns(group_wise_columns, filters):
"sales_person": {
"label": _("Sales Person"),
"fieldname": "sales_person",
"fieldtype": "Data",
"fieldtype": "Link",
"options": "Sales Person",
"width": 100,
},
"allocated_amount": {
@ -326,14 +327,14 @@ def get_columns(group_wise_columns, filters):
"label": _("Customer Group"),
"fieldname": "customer_group",
"fieldtype": "Link",
"options": "customer",
"options": "Customer Group",
"width": 100,
},
"territory": {
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "territory",
"options": "Territory",
"width": 100,
},
"monthly": {

View File

@ -4,7 +4,6 @@
import frappe
from frappe import _
from frappe.utils import flt
def execute(filters=None):
@ -66,12 +65,6 @@ def get_result(
else:
total_amount_credited += entry.credit
## Check if ldc is applied and show rate as per ldc
actual_rate = (tds_deducted / total_amount_credited) * 100
if flt(actual_rate) < flt(rate):
rate = actual_rate
if tds_deducted:
row = {
"pan"

View File

@ -248,13 +248,20 @@ def get_opening_balance(
opening_balance = opening_balance.where(closing_balance.project == filters.project)
if filters.get("include_default_book_entries"):
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
frappe.throw(
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
)
opening_balance = opening_balance.where(
(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(filters.company_fb), ""]))
(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb)]))
| (closing_balance.finance_book.isnull())
)
else:
opening_balance = opening_balance.where(
(closing_balance.finance_book.isin([cstr(filters.finance_book), ""]))
(closing_balance.finance_book.isin([cstr(filters.finance_book)]))
| (closing_balance.finance_book.isnull())
)

View File

@ -436,7 +436,7 @@ def add_cc(args=None):
return cc.name
def reconcile_against_document(args): # nosemgrep
def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # nosemgrep
"""
Cancel PE or JV, Update against document, split if required and resubmit
"""
@ -465,7 +465,9 @@ def reconcile_against_document(args): # nosemgrep
if voucher_type == "Journal Entry":
update_reference_in_journal_entry(entry, doc, do_not_save=True)
else:
update_reference_in_payment_entry(entry, doc, do_not_save=True)
update_reference_in_payment_entry(
entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe
)
doc.save(ignore_permissions=True)
# re-submit advance entry
@ -602,7 +604,9 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
journal_entry.save(ignore_permissions=True)
def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
def update_reference_in_payment_entry(
d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False
):
reference_details = {
"reference_doctype": d.against_voucher_type,
"reference_name": d.against_voucher,
@ -646,6 +650,8 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
payment_entry.flags.ignore_validate_update_after_submit = True
payment_entry.setup_party_account_field()
payment_entry.set_missing_values()
if not skip_ref_details_update_for_pe:
payment_entry.set_missing_ref_details()
payment_entry.set_amounts()
if not do_not_save:
@ -1368,10 +1374,7 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None)
if wh_details.account == account and not wh_details.is_group
]
total_stock_value = 0.0
for warehouse in related_warehouses:
value = get_stock_value_on(warehouse, posting_date)
total_stock_value += value
total_stock_value = get_stock_value_on(related_warehouses, posting_date)
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses

View File

@ -783,7 +783,7 @@ def make_journal_entry(asset_name):
je.voucher_type = "Depreciation Entry"
je.naming_series = depreciation_series
je.company = asset.company
je.remark = "Depreciation Entry against asset {0}".format(asset_name)
je.remark = _("Depreciation Entry against asset {0}").format(asset_name)
je.append(
"accounts",

View File

@ -157,6 +157,7 @@ def make_depreciation_entry(asset_depr_schedule_name, date=None):
je.append("accounts", debit_entry)
je.flags.ignore_permissions = True
je.flags.planned_depr_entry = True
je.save()
if not je.meta.get_workflow():
je.submit()

View File

@ -1511,7 +1511,7 @@ class TestDepreciationBasics(AssetSetup):
)
self.assertEqual(asset.status, "Submitted")
self.assertEqual(asset.get("value_after_depreciation"), 100000)
self.assertEqual(asset.get_value_after_depreciation(), 100000)
jv = make_journal_entry(
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
@ -1524,12 +1524,68 @@ class TestDepreciationBasics(AssetSetup):
jv.submit()
asset.reload()
self.assertEqual(asset.get("value_after_depreciation"), 99900)
self.assertEqual(asset.get_value_after_depreciation(), 99900)
jv.cancel()
asset.reload()
self.assertEqual(asset.get("value_after_depreciation"), 100000)
self.assertEqual(asset.get_value_after_depreciation(), 100000)
def test_manual_depreciation_for_depreciable_asset(self):
asset = create_asset(
item_code="Macbook Pro",
calculate_depreciation=1,
purchase_date="2020-01-30",
available_for_use_date="2020-01-30",
expected_value_after_useful_life=10000,
total_number_of_depreciations=10,
frequency_of_depreciation=1,
submit=1,
)
self.assertEqual(asset.status, "Submitted")
self.assertEqual(asset.get_value_after_depreciation(), 100000)
jv = make_journal_entry(
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
)
for d in jv.accounts:
d.reference_type = "Asset"
d.reference_name = asset.name
jv.voucher_type = "Depreciation Entry"
jv.insert()
jv.submit()
asset.reload()
self.assertEqual(asset.get_value_after_depreciation(), 99900)
jv.cancel()
asset.reload()
self.assertEqual(asset.get_value_after_depreciation(), 100000)
def test_manual_depreciation_with_incorrect_jv_voucher_type(self):
asset = create_asset(
item_code="Macbook Pro",
calculate_depreciation=1,
purchase_date="2020-01-30",
available_for_use_date="2020-01-30",
expected_value_after_useful_life=10000,
total_number_of_depreciations=10,
frequency_of_depreciation=1,
submit=1,
)
jv = make_journal_entry(
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
)
for d in jv.accounts:
d.reference_type = "Asset"
d.reference_name = asset.name
d.account_type = "Depreciation"
jv.voucher_type = "Journal Entry"
self.assertRaises(frappe.ValidationError, jv.insert)
def create_asset_data():

View File

@ -150,7 +150,9 @@ class AssetValueAdjustment(Document):
if d.depreciation_method in ("Straight Line", "Manual"):
end_date = max(s.schedule_date for s in depr_schedule)
total_days = date_diff(end_date, self.date)
rate_per_day = flt(d.value_after_depreciation) / flt(total_days)
rate_per_day = flt(d.value_after_depreciation - d.expected_value_after_useful_life) / flt(
total_days
)
from_date = self.date
else:
no_of_depreciations = len([s.name for s in depr_schedule if not s.journal_entry])

View File

@ -94,11 +94,11 @@ frappe.query_reports["Fixed Asset Register"] = {
label: __("Finance Book"),
fieldtype: "Link",
options: "Finance Book",
depends_on: "eval: doc.only_depreciable_assets == 1",
depends_on: "eval: doc.filter_by_finance_book == 1",
},
{
fieldname:"only_depreciable_assets",
label: __("Only depreciable assets"),
fieldname:"filter_by_finance_book",
label: __("Filter by Finance Book"),
fieldtype: "Check"
},
{

View File

@ -45,8 +45,6 @@ def get_conditions(filters):
filters.year_end_date = getdate(fiscal_year.year_end_date)
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
if filters.get("only_depreciable_assets"):
conditions["calculate_depreciation"] = filters.get("only_depreciable_assets")
if filters.get("only_existing_assets"):
conditions["is_existing_asset"] = filters.get("only_existing_assets")
if filters.get("asset_category"):
@ -106,7 +104,7 @@ def get_data(filters):
assets_linked_to_fb = None
if filters.only_depreciable_assets:
if filters.filter_by_finance_book:
assets_linked_to_fb = frappe.db.get_all(
doctype="Asset Finance Book",
filters={"finance_book": filters.finance_book or ("is", "not set")},

View File

@ -495,6 +495,7 @@
"allow_bulk_edit": 1,
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"oldfieldname": "po_details",
"oldfieldtype": "Table",
"options": "Purchase Order Item",
@ -1100,8 +1101,7 @@
{
"fieldname": "before_items_section",
"fieldtype": "Section Break",
"hide_border": 1,
"label": "Items"
"hide_border": 1
},
{
"fieldname": "items_col_break",
@ -1271,7 +1271,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2023-01-28 18:59:16.322824",
"modified": "2023-04-14 16:42:29.448464",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@ -43,7 +43,7 @@ frappe.listview_settings['Purchase Order'] = {
});
listview.page.add_action_item(__("Advance Payment"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Advance Payment");
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Payment Entry");
});
}

View File

@ -310,7 +310,6 @@
"fieldname": "items_section",
"fieldtype": "Section Break",
"hide_border": 1,
"label": "Items",
"oldfieldtype": "Section Break",
"options": "fa fa-shopping-cart"
},
@ -318,6 +317,7 @@
"allow_bulk_edit": 1,
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"oldfieldname": "po_details",
"oldfieldtype": "Table",
"options": "Supplier Quotation Item",
@ -844,7 +844,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-12-12 18:35:39.740974",
"modified": "2023-04-14 16:43:41.714832",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",

View File

@ -5,7 +5,7 @@
import json
import frappe
from frappe import _, throw
from frappe import _, bold, throw
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
from frappe.query_builder.functions import Abs, Sum
from frappe.utils import (
@ -405,6 +405,15 @@ class AccountsController(TransactionBase):
msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
label = "Delivery Note Item" if self.doctype == "Purchase Receipt" else "Sales Invoice Item"
field = frappe.scrub(label)
for row in self.get("items"):
if not row.get(field):
msg = f"At Row {row.idx}: The field {bold(label)} is mandatory for internal transfer"
frappe.throw(_(msg), title=_("Internal Transfer Reference Missing"))
def disable_pricing_rule_on_internal_transfer(self):
if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
self.ignore_pricing_rule = 1
@ -1653,7 +1662,10 @@ class AccountsController(TransactionBase):
)
self.append("payment_schedule", data)
if not automatically_fetch_payment_terms:
if not (
automatically_fetch_payment_terms
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
):
for d in self.get("payment_schedule"):
if d.invoice_portion:
d.payment_amount = flt(
@ -1892,12 +1904,14 @@ class AccountsController(TransactionBase):
reconcilation_entry.party = secondary_party
reconcilation_entry.reference_type = self.doctype
reconcilation_entry.reference_name = self.name
reconcilation_entry.cost_center = self.cost_center
reconcilation_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(
self.company
)
advance_entry.account = primary_account
advance_entry.party_type = primary_party_type
advance_entry.party = primary_party
advance_entry.cost_center = self.cost_center
advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
advance_entry.is_advance = "Yes"
if self.doctype == "Sales Invoice":

View File

@ -576,7 +576,9 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters):
def get_filtered_dimensions(
doctype, txt, searchfield, start, page_len, filters, reference_doctype=None
):
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
get_dimension_filter_map,
)
@ -617,7 +619,12 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters)
query_filters.append(["name", query_selector, dimensions])
output = frappe.get_list(
doctype, fields=fields, filters=query_filters, or_filters=or_filters, as_list=1
doctype,
fields=fields,
filters=query_filters,
or_filters=or_filters,
as_list=1,
reference_doctype=reference_doctype,
)
return [tuple(d) for d in set(output)]

View File

@ -329,9 +329,10 @@ class StockController(AccountsController):
"""Create batches if required. Called before submit"""
for d in self.items:
if d.get(warehouse_field) and not d.batch_no:
has_batch_no, create_new_batch = frappe.db.get_value(
has_batch_no, create_new_batch = frappe.get_cached_value(
"Item", d.item_code, ["has_batch_no", "create_new_batch"]
)
if has_batch_no and create_new_batch:
d.batch_no = (
frappe.get_doc(
@ -414,7 +415,7 @@ class StockController(AccountsController):
"voucher_no": self.name,
"voucher_detail_no": d.name,
"actual_qty": (self.docstatus == 1 and 1 or -1) * flt(d.get("stock_qty")),
"stock_uom": frappe.db.get_value(
"stock_uom": frappe.get_cached_value(
"Item", args.get("item_code") or d.get("item_code"), "stock_uom"
),
"incoming_rate": 0,
@ -609,7 +610,7 @@ class StockController(AccountsController):
def validate_customer_provided_item(self):
for d in self.get("items"):
# Customer Provided parts will have zero valuation rate
if frappe.db.get_value("Item", d.item_code, "is_customer_provided_item"):
if frappe.get_cached_value("Item", d.item_code, "is_customer_provided_item"):
d.allow_zero_valuation_rate = 1
def set_rate_of_stock_uom(self):
@ -722,7 +723,7 @@ class StockController(AccountsController):
message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link)
return message
def repost_future_sle_and_gle(self):
def repost_future_sle_and_gle(self, force=False):
args = frappe._dict(
{
"posting_date": self.posting_date,
@ -733,7 +734,7 @@ class StockController(AccountsController):
}
)
if future_sle_exists(args) or repost_required_for_queue(self):
if force or future_sle_exists(args) or repost_required_for_queue(self):
item_based_reposting = cint(
frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")
)
@ -894,9 +895,6 @@ def future_sle_exists(args, sl_entries=None):
)
for d in data:
if key not in frappe.local.future_sle:
frappe.local.future_sle[key] = frappe._dict({})
frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row
return len(data)
@ -919,7 +917,7 @@ def validate_future_sle_not_exists(args, key, sl_entries=None):
def get_cached_data(args, key):
if key not in frappe.local.future_sle:
return False
frappe.local.future_sle[key] = frappe._dict({})
if args.get("item_code"):
item_key = (args.get("item_code"), args.get("warehouse"))

View File

@ -976,6 +976,8 @@ def get_itemised_tax_breakup_html(doc):
@frappe.whitelist()
def get_round_off_applicable_accounts(company, account_list):
# required to set correct region
frappe.flags.company = company
account_list = get_regional_round_off_accounts(company, account_list)
return account_list

View File

@ -59,7 +59,7 @@ class Opportunity(TransactionBase, CRMNote):
if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field):
try:
value = frappe.db.get_value(self.opportunity_from, self.party_name, field)
self.db_set(field, value)
self.set(field, value)
except Exception:
continue

View File

@ -315,6 +315,7 @@ class WebsiteItem(WebsiteGenerator):
self.item_code, skip_quotation_creation=True
)
@frappe.whitelist()
def copy_specification_from_item_group(self):
self.set("website_specifications", [])
if self.item_group:

View File

@ -362,6 +362,7 @@ scheduler_events = {
"cron": {
"0/15 * * * *": [
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
"erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs",
],
"0/30 * * * *": [
"erpnext.utilities.doctype.video.video.update_youtube_data",

View File

@ -411,7 +411,6 @@ frappe.ui.form.on("BOM", {
}
frm.set_value("process_loss_qty", qty);
frm.set_value("add_process_loss_cost_in_fg", qty ? 1: 0);
}
});
@ -653,7 +652,7 @@ frappe.ui.form.on("BOM Operation", "operation", function(frm, cdt, cdn) {
frappe.ui.form.on("BOM Operation", "workstation", function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
if(!d.workstation) return;
frappe.call({
"method": "frappe.client.get",
args: {

View File

@ -1317,7 +1317,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if not field in searchfields
]
query_filters = {"disabled": 0, "end_of_life": (">", today())}
query_filters = {"disabled": 0, "ifnull(end_of_life, '3099-12-31')": (">", today())}
or_cond_filters = {}
if txt:

View File

@ -74,6 +74,52 @@ class JobCard(Document):
self.update_sub_operation_status()
self.validate_work_order()
def on_update(self):
self.validate_job_card_qty()
def validate_job_card_qty(self):
if not (self.operation_id and self.work_order):
return
wo_qty = flt(frappe.get_cached_value("Work Order", self.work_order, "qty"))
completed_qty = flt(
frappe.db.get_value("Work Order Operation", self.operation_id, "completed_qty")
)
over_production_percentage = flt(
frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order")
)
wo_qty = wo_qty + (wo_qty * over_production_percentage / 100)
job_card_qty = frappe.get_all(
"Job Card",
fields=["sum(for_quantity)"],
filters={
"work_order": self.work_order,
"operation_id": self.operation_id,
"docstatus": ["!=", 2],
},
as_list=1,
)
job_card_qty = flt(job_card_qty[0][0]) if job_card_qty else 0
if job_card_qty and ((job_card_qty - completed_qty) > wo_qty):
form_link = get_link_to_form("Manufacturing Settings", "Manufacturing Settings")
msg = f"""
Qty To Manufacture in the job card
cannot be greater than Qty To Manufacture in the
work order for the operation {bold(self.operation)}.
<br><br><b>Solution: </b> Either you can reduce the
Qty To Manufacture in the job card or set the
'Overproduction Percentage For Work Order'
in the {form_link}."""
frappe.throw(_(msg), title=_("Extra Job Card Quantity"))
def set_sub_operations(self):
if not self.sub_operations and self.operation:
self.sub_operations = []

View File

@ -16,6 +16,7 @@
"column_break_4",
"quantity",
"uom",
"conversion_factor",
"projected_qty",
"reserved_qty_for_production",
"safety_stock",
@ -169,11 +170,17 @@
"label": "Qty As Per BOM",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "conversion_factor",
"fieldtype": "Float",
"label": "Conversion Factor",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2022-11-26 14:59:25.879631",
"modified": "2023-05-03 12:43:29.895754",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Material Request Plan Item",

View File

@ -336,10 +336,6 @@ frappe.ui.form.on('Production Plan', {
},
get_items_for_material_requests(frm, warehouses) {
let set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom', 'from_warehouse',
'min_order_qty', 'required_bom_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'ordered_qty',
'reserved_qty_for_production', 'material_request_type'];
frappe.call({
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests",
freeze: true,
@ -352,11 +348,11 @@ frappe.ui.form.on('Production Plan', {
frm.set_value('mr_items', []);
r.message.forEach(row => {
let d = frm.add_child('mr_items');
set_fields.forEach(field => {
if (row[field]) {
for (let field in row) {
if (field !== 'name') {
d[field] = row[field];
}
});
}
});
}
refresh_field('mr_items');

View File

@ -28,6 +28,7 @@ from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.utils import get_or_make_bin
from erpnext.utilities.transaction_base import validate_uom_is_integer
@ -398,9 +399,20 @@ class ProductionPlan(Document):
self.set_status()
self.db_set("status", self.status)
def on_submit(self):
self.update_bin_qty()
def on_cancel(self):
self.db_set("status", "Cancelled")
self.delete_draft_work_order()
self.update_bin_qty()
def update_bin_qty(self):
for d in self.mr_items:
if d.warehouse:
bin_name = get_or_make_bin(d.item_code, d.warehouse)
bin = frappe.get_doc("Bin", bin_name, for_update=True)
bin.update_reserved_qty_for_production_plan()
def delete_draft_work_order(self):
for d in frappe.get_all(
@ -1068,6 +1080,7 @@ def get_material_request_items(
"item_code": row.item_code,
"item_name": row.item_name,
"quantity": required_qty / conversion_factor,
"conversion_factor": conversion_factor,
"required_bom_qty": total_qty,
"stock_uom": row.get("stock_uom"),
"warehouse": warehouse
@ -1474,3 +1487,34 @@ def set_default_warehouses(row, default_warehouses):
for field in ["wip_warehouse", "fg_warehouse"]:
if not row.get(field):
row[field] = default_warehouses.get(field)
def get_reserved_qty_for_production_plan(item_code, warehouse):
from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production
table = frappe.qb.DocType("Production Plan")
child = frappe.qb.DocType("Material Request Plan Item")
query = (
frappe.qb.from_(table)
.inner_join(child)
.on(table.name == child.parent)
.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"]))
)
).run()
if not query:
return 0.0
reserved_qty_for_production_plan = flt(query[0][0])
reserved_qty_for_production = flt(
get_reserved_qty_for_production(item_code, warehouse, check_production_plan=True)
)
return reserved_qty_for_production_plan - reserved_qty_for_production

View File

@ -868,6 +868,27 @@ class TestProductionPlan(FrappeTestCase):
for item_code in mr_items:
self.assertTrue(item_code in validate_mr_items)
def test_resered_qty_for_production_plan_for_material_requests(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 = frappe.get_doc("Production Plan", pln.name)
pln.cancel()
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 create_production_plan(**args):
"""

View File

@ -50,7 +50,7 @@ frappe.ui.form.on('BOM Operation', {
workstation: function(frm, cdt, cdn) {
const d = locals[cdt][cdn];
if(!d.workstation) return;
frappe.call({
"method": "frappe.client.get",
args: {

View File

@ -1598,6 +1598,65 @@ class TestWorkOrder(FrappeTestCase):
self.assertEqual(row.to_time, add_to_date(planned_start_date, minutes=30))
self.assertEqual(row.workstation, workstations_to_check[index])
def test_job_card_extra_qty(self):
items = [
"Test FG Item for Scrap Item Test 1",
"Test RM Item 1 for Scrap Item Test 1",
"Test RM Item 2 for Scrap Item Test 1",
]
company = "_Test Company with perpetual inventory"
for item_code in items:
create_item(
item_code=item_code,
is_stock_item=1,
is_purchase_item=1,
opening_stock=100,
valuation_rate=10,
company=company,
warehouse="Stores - TCP1",
)
item = "Test FG Item for Scrap Item Test 1"
raw_materials = ["Test RM Item 1 for Scrap Item Test 1", "Test RM Item 2 for Scrap Item Test 1"]
if not frappe.db.get_value("BOM", {"item": item}):
bom = make_bom(
item=item, source_warehouse="Stores - TCP1", raw_materials=raw_materials, do_not_save=True
)
bom.with_operations = 1
bom.append(
"operations",
{
"operation": "_Test Operation 1",
"workstation": "_Test Workstation 1",
"hour_rate": 20,
"time_in_mins": 60,
},
)
bom.submit()
wo_order = make_wo_order_test_record(
item=item,
company=company,
planned_start_date=now(),
qty=20,
)
job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name}, "name")
job_card_doc = frappe.get_doc("Job Card", job_card)
# Make another Job Card for the same Work Order
job_card2 = frappe.copy_doc(job_card_doc)
self.assertRaises(frappe.ValidationError, job_card2.save)
frappe.db.set_single_value(
"Manufacturing Settings", "overproduction_percentage_for_work_order", 100
)
job_card2 = frappe.copy_doc(job_card_doc)
job_card2.time_logs = []
job_card2.save()
def prepare_data_for_workstation_type_check():
from erpnext.manufacturing.doctype.operation.test_operation import make_operation

View File

@ -625,20 +625,18 @@ erpnext.work_order = {
// all materials transferred for manufacturing, make this primary
finish_btn.addClass('btn-primary');
}
} else {
frappe.db.get_doc("Manufacturing Settings").then((doc) => {
let allowance_percentage = doc.overproduction_percentage_for_work_order;
} else if (frm.doc.__onload && frm.doc.__onload.overproduction_percentage) {
let allowance_percentage = frm.doc.__onload.overproduction_percentage;
if (allowance_percentage > 0) {
let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
if (allowance_percentage > 0) {
let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
if ((flt(doc.produced_qty) < allowed_qty)) {
frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
}
if ((flt(doc.produced_qty) < allowed_qty)) {
frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
}
});
}
}
}
} else {

View File

@ -558,12 +558,19 @@ class WorkOrder(Document):
and self.production_plan_item
and not self.production_plan_sub_assembly_item
):
qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0
table = frappe.qb.DocType("Work Order")
if self.docstatus == 1:
qty += self.qty
elif self.docstatus == 2:
qty -= self.qty
query = (
frappe.qb.from_(table)
.select(Sum(table.qty))
.where(
(table.production_plan == self.production_plan)
& (table.production_plan_item == self.production_plan_item)
& (table.docstatus == 1)
)
).run()
qty = flt(query[0][0]) if query else 0
frappe.db.set_value("Production Plan Item", self.production_plan_item, "ordered_qty", qty)
@ -1476,12 +1483,14 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
return doc
def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float:
def get_reserved_qty_for_production(
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")
return (
query = (
frappe.qb.from_(wo)
.from_(wo_item)
.select(
@ -1502,7 +1511,12 @@ def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float:
| (wo_item.required_qty > wo_item.consumed_qty)
)
)
).run()[0][0] or 0.0
)
if check_production_plan:
query = query.where(wo.production_plan.isnotnull())
return query.run()[0][0] or 0.0
@frappe.whitelist()

View File

@ -69,7 +69,7 @@ def get_columns(filters):
"label": _("Id"),
"fieldname": "name",
"fieldtype": "Link",
"options": "Work Order",
"options": "Quality Inspection",
"width": 100,
},
{"label": _("Report Date"), "fieldname": "report_date", "fieldtype": "Date", "width": 150},

View File

@ -327,7 +327,9 @@ erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries
erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch
erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance
erpnext.patches.v14_0.update_closing_balances
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
# below migration patches should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
execute:frappe.delete_doc_if_exists("Report", "Tax Detail")
erpnext.patches.v15_0.enable_all_leads
erpnext.patches.v14_0.update_company_in_ldc

View File

@ -26,7 +26,15 @@ def execute():
pcv_doc.year_start_date = get_fiscal_year(
pcv.posting_date, pcv.fiscal_year, company=pcv.company
)[1]
gl_entries = pcv_doc.get_gl_entries()
gl_entries = frappe.db.get_all(
"GL Entry", filters={"voucher_no": pcv.name, "is_cancelled": 0}, fields=["*"]
)
for entry in gl_entries:
entry["is_period_closing_voucher_entry"] = 1
entry["closing_date"] = pcv_doc.posting_date
entry["period_closing_voucher"] = pcv_doc.name
closing_entries = pcv_doc.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
make_closing_entries(gl_entries + closing_entries, voucher_name=pcv.name)
company_wise_order[pcv.company].append(pcv.posting_date)

View File

@ -0,0 +1,14 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
from erpnext import get_default_company
def execute():
company = get_default_company()
if company:
for d in frappe.get_all("Lower Deduction Certificate", pluck="name"):
frappe.db.set_value("Lower Deduction Certificate", d, "company", company, update_modified=False)

View File

@ -96,7 +96,6 @@
"read_only": 1
},
{
"depends_on": "eval:!doc.work_order || doc.docstatus == 1",
"fieldname": "employee_detail",
"fieldtype": "Section Break",
"label": "Employee Detail"
@ -311,7 +310,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-02-14 04:55:41.735991",
"modified": "2023-04-20 15:59:11.107831",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet",

View File

@ -68,7 +68,7 @@ erpnext.timesheet.control_timer = function(frm, dialog, row, timestamp=0) {
// New activity if no activities found
var args = dialog.get_values();
if(!args) return;
if (frm.doc.time_logs.length <= 1 && !frm.doc.time_logs[0].activity_type && !frm.doc.time_logs[0].from_time) {
if (frm.doc.time_logs.length == 1 && !frm.doc.time_logs[0].activity_type && !frm.doc.time_logs[0].from_time) {
frm.doc.time_logs = [];
}
row = frappe.model.add_child(frm.doc, "Timesheet Detail", "time_logs");

View File

@ -10,6 +10,7 @@
"tax_withholding_category",
"fiscal_year",
"column_break_3",
"company",
"certificate_no",
"section_break_3",
"supplier",
@ -123,11 +124,18 @@
"label": "Tax Withholding Category",
"options": "Tax Withholding Category",
"reqd": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-10-23 18:33:38.962622",
"modified": "2023-04-18 08:25:35.302081",
"modified_by": "Administrator",
"module": "Regional",
"name": "Lower Deduction Certificate",
@ -136,5 +144,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -1,31 +1,135 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import re
import frappe
from frappe import _
from frappe.utils import format_datetime
COLUMNS = [
{
"label": "JournalCode",
"fieldname": "JournalCode",
"fieldtype": "Data",
"width": 90,
},
{
"label": "JournalLib",
"fieldname": "JournalLib",
"fieldtype": "Data",
"width": 90,
},
{
"label": "EcritureNum",
"fieldname": "EcritureNum",
"fieldtype": "Data",
"width": 90,
},
{
"label": "EcritureDate",
"fieldname": "EcritureDate",
"fieldtype": "Data",
"width": 90,
},
{
"label": "CompteNum",
"fieldname": "CompteNum",
"fieldtype": "Link",
"options": "Account",
"width": 100,
},
{
"label": "CompteLib",
"fieldname": "CompteLib",
"fieldtype": "Link",
"options": "Account",
"width": 200,
},
{
"label": "CompAuxNum",
"fieldname": "CompAuxNum",
"fieldtype": "Data",
"width": 90,
},
{
"label": "CompAuxLib",
"fieldname": "CompAuxLib",
"fieldtype": "Data",
"width": 90,
},
{
"label": "PieceRef",
"fieldname": "PieceRef",
"fieldtype": "Data",
"width": 90,
},
{
"label": "PieceDate",
"fieldname": "PieceDate",
"fieldtype": "Data",
"width": 90,
},
{
"label": "EcritureLib",
"fieldname": "EcritureLib",
"fieldtype": "Data",
"width": 90,
},
{
"label": "Debit",
"fieldname": "Debit",
"fieldtype": "Data",
"width": 90,
},
{
"label": "Credit",
"fieldname": "Credit",
"fieldtype": "Data",
"width": 90,
},
{
"label": "EcritureLet",
"fieldname": "EcritureLet",
"fieldtype": "Data",
"width": 90,
},
{
"label": "DateLet",
"fieldname": "DateLet",
"fieldtype": "Data",
"width": 90,
},
{
"label": "ValidDate",
"fieldname": "ValidDate",
"fieldtype": "Data",
"width": 90,
},
{
"label": "Montantdevise",
"fieldname": "Montantdevise",
"fieldtype": "Data",
"width": 90,
},
{
"label": "Idevise",
"fieldname": "Idevise",
"fieldtype": "Data",
"width": 90,
},
]
def execute(filters=None):
account_details = {}
for acc in frappe.db.sql("""select name, is_group from tabAccount""", as_dict=1):
account_details.setdefault(acc.name, acc)
validate_filters(filters, account_details)
filters = set_account_currency(filters)
columns = get_columns(filters)
res = get_result(filters)
return columns, res
validate_filters(filters)
return COLUMNS, get_result(
company=filters["company"],
fiscal_year=filters["fiscal_year"],
)
def validate_filters(filters, account_details):
def validate_filters(filters):
if not filters.get("company"):
frappe.throw(_("{0} is mandatory").format(_("Company")))
@ -33,107 +137,96 @@ def validate_filters(filters, account_details):
frappe.throw(_("{0} is mandatory").format(_("Fiscal Year")))
def set_account_currency(filters):
def get_gl_entries(company, fiscal_year):
gle = frappe.qb.DocType("GL Entry")
sales_invoice = frappe.qb.DocType("Sales Invoice")
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
journal_entry = frappe.qb.DocType("Journal Entry")
payment_entry = frappe.qb.DocType("Payment Entry")
customer = frappe.qb.DocType("Customer")
supplier = frappe.qb.DocType("Supplier")
employee = frappe.qb.DocType("Employee")
filters["company_currency"] = frappe.get_cached_value(
"Company", filters.company, "default_currency"
debit = frappe.query_builder.functions.Sum(gle.debit).as_("debit")
credit = frappe.query_builder.functions.Sum(gle.credit).as_("credit")
debit_currency = frappe.query_builder.functions.Sum(gle.debit_in_account_currency).as_(
"debitCurr"
)
credit_currency = frappe.query_builder.functions.Sum(gle.credit_in_account_currency).as_(
"creditCurr"
)
return filters
def get_columns(filters):
columns = [
"JournalCode" + "::90",
"JournalLib" + "::90",
"EcritureNum" + ":Dynamic Link:90",
"EcritureDate" + "::90",
"CompteNum" + ":Link/Account:100",
"CompteLib" + ":Link/Account:200",
"CompAuxNum" + "::90",
"CompAuxLib" + "::90",
"PieceRef" + "::90",
"PieceDate" + "::90",
"EcritureLib" + "::90",
"Debit" + "::90",
"Credit" + "::90",
"EcritureLet" + "::90",
"DateLet" + "::90",
"ValidDate" + "::90",
"Montantdevise" + "::90",
"Idevise" + "::90",
]
return columns
def get_result(filters):
gl_entries = get_gl_entries(filters)
result = get_result_as_list(gl_entries, filters)
return result
def get_gl_entries(filters):
group_by_condition = (
"group by voucher_type, voucher_no, account"
if filters.get("group_by_voucher")
else "group by gl.name"
query = (
frappe.qb.from_(gle)
.left_join(sales_invoice)
.on(gle.voucher_no == sales_invoice.name)
.left_join(purchase_invoice)
.on(gle.voucher_no == purchase_invoice.name)
.left_join(journal_entry)
.on(gle.voucher_no == journal_entry.name)
.left_join(payment_entry)
.on(gle.voucher_no == payment_entry.name)
.left_join(customer)
.on(gle.party == customer.name)
.left_join(supplier)
.on(gle.party == supplier.name)
.left_join(employee)
.on(gle.party == employee.name)
.select(
gle.posting_date.as_("GlPostDate"),
gle.name.as_("GlName"),
gle.account,
gle.transaction_date,
debit,
credit,
debit_currency,
credit_currency,
gle.voucher_type,
gle.voucher_no,
gle.against_voucher_type,
gle.against_voucher,
gle.account_currency,
gle.against,
gle.party_type,
gle.party,
sales_invoice.name.as_("InvName"),
sales_invoice.title.as_("InvTitle"),
sales_invoice.posting_date.as_("InvPostDate"),
purchase_invoice.name.as_("PurName"),
purchase_invoice.title.as_("PurTitle"),
purchase_invoice.posting_date.as_("PurPostDate"),
journal_entry.cheque_no.as_("JnlRef"),
journal_entry.posting_date.as_("JnlPostDate"),
journal_entry.title.as_("JnlTitle"),
payment_entry.name.as_("PayName"),
payment_entry.posting_date.as_("PayPostDate"),
payment_entry.title.as_("PayTitle"),
customer.customer_name,
customer.name.as_("cusName"),
supplier.supplier_name,
supplier.name.as_("supName"),
employee.employee_name,
employee.name.as_("empName"),
)
.where((gle.company == company) & (gle.fiscal_year == fiscal_year))
.groupby(gle.voucher_type, gle.voucher_no, gle.account)
.orderby(gle.posting_date, gle.voucher_no)
)
gl_entries = frappe.db.sql(
"""
select
gl.posting_date as GlPostDate, gl.name as GlName, gl.account, gl.transaction_date,
sum(gl.debit) as debit, sum(gl.credit) as credit,
sum(gl.debit_in_account_currency) as debitCurr, sum(gl.credit_in_account_currency) as creditCurr,
gl.voucher_type, gl.voucher_no, gl.against_voucher_type,
gl.against_voucher, gl.account_currency, gl.against,
gl.party_type, gl.party,
inv.name as InvName, inv.title as InvTitle, inv.posting_date as InvPostDate,
pur.name as PurName, pur.title as PurTitle, pur.posting_date as PurPostDate,
jnl.cheque_no as JnlRef, jnl.posting_date as JnlPostDate, jnl.title as JnlTitle,
pay.name as PayName, pay.posting_date as PayPostDate, pay.title as PayTitle,
cus.customer_name, cus.name as cusName,
sup.supplier_name, sup.name as supName,
emp.employee_name, emp.name as empName,
stu.title as student_name, stu.name as stuName,
member_name, mem.name as memName
from `tabGL Entry` gl
left join `tabSales Invoice` inv on gl.voucher_no = inv.name
left join `tabPurchase Invoice` pur on gl.voucher_no = pur.name
left join `tabJournal Entry` jnl on gl.voucher_no = jnl.name
left join `tabPayment Entry` pay on gl.voucher_no = pay.name
left join `tabCustomer` cus on gl.party = cus.name
left join `tabSupplier` sup on gl.party = sup.name
left join `tabEmployee` emp on gl.party = emp.name
left join `tabStudent` stu on gl.party = stu.name
left join `tabMember` mem on gl.party = mem.name
where gl.company=%(company)s and gl.fiscal_year=%(fiscal_year)s
{group_by_condition}
order by GlPostDate, voucher_no""".format(
group_by_condition=group_by_condition
),
filters,
as_dict=1,
)
return gl_entries
return query.run(as_dict=True)
def get_result_as_list(data, filters):
def get_result(company, fiscal_year):
data = get_gl_entries(company, fiscal_year)
result = []
company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
company_currency = frappe.get_cached_value("Company", company, "default_currency")
accounts = frappe.get_all(
"Account", filters={"Company": filters.company}, fields=["name", "account_number"]
"Account", filters={"Company": company}, fields=["name", "account_number"]
)
for d in data:
JournalCode = re.split("-|/|[0-9]", d.get("voucher_no"))[0]
if d.get("voucher_no").startswith("{0}-".format(JournalCode)) or d.get("voucher_no").startswith(
@ -141,9 +234,7 @@ def get_result_as_list(data, filters):
):
EcritureNum = re.split("-|/", d.get("voucher_no"))[1]
else:
EcritureNum = re.search(
r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE
).group(1)
EcritureNum = re.search(r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE)[1]
EcritureDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd")
@ -185,7 +276,7 @@ def get_result_as_list(data, filters):
ValidDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd")
PieceRef = d.get("voucher_no") if d.get("voucher_no") else "Sans Reference"
PieceRef = d.get("voucher_no") or "Sans Reference"
# EcritureLib is the reference title unless it is an opening entry
if d.get("is_opening") == "Yes":

View File

@ -416,7 +416,6 @@
"fieldname": "items_section",
"fieldtype": "Section Break",
"hide_border": 1,
"label": "Items",
"oldfieldtype": "Section Break",
"options": "fa fa-shopping-cart"
},
@ -424,6 +423,7 @@
"allow_bulk_edit": 1,
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"oldfieldname": "quotation_details",
"oldfieldtype": "Table",
"options": "Quotation Item",
@ -1072,7 +1072,7 @@
"idx": 82,
"is_submittable": 1,
"links": [],
"modified": "2022-12-12 18:32:28.671332",
"modified": "2023-04-14 16:50:44.550098",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",

View File

@ -30,10 +30,6 @@
"cost_center",
"dimension_col_break",
"project",
"column_break_77",
"source",
"campaign",
"custom_dimensions_section",
"currency_and_price_list",
"currency",
"conversion_rate",
@ -162,7 +158,9 @@
"is_internal_customer",
"represents_company",
"column_break_152",
"source",
"inter_company_order_reference",
"campaign",
"party_account_currency",
"connections_tab"
],
@ -1164,12 +1162,6 @@
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_77",
"fieldtype": "Column Break",
"hide_days": 1,
"hide_seconds": 1
},
{
"fieldname": "source",
"fieldtype": "Link",
@ -1612,10 +1604,6 @@
"fieldname": "column_break_92",
"fieldtype": "Column Break"
},
{
"fieldname": "custom_dimensions_section",
"fieldtype": "Section Break"
},
{
"collapsible": 1,
"fieldname": "additional_info_section",
@ -1643,7 +1631,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2022-12-12 18:34:00.681780",
"modified": "2023-04-20 11:14:01.036202",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",

View File

@ -547,7 +547,7 @@ def make_material_request(source_name, target_doc=None):
# qty is for packed items, because packed items don't have stock_qty field
qty = source.get("qty")
target.project = source_parent.project
target.qty = qty - requested_item_qty.get(source.name, 0)
target.qty = qty - requested_item_qty.get(source.name, 0) - source.delivered_qty
target.stock_qty = flt(target.qty) * flt(target.conversion_factor)
args = target.as_dict().copy()
@ -581,7 +581,7 @@ def make_material_request(source_name, target_doc=None):
"doctype": "Material Request Item",
"field_map": {"name": "sales_order_item", "parent": "sales_order"},
"condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code)
and doc.stock_qty > requested_item_qty.get(doc.name, 0),
and (doc.stock_qty - doc.delivered_qty) > requested_item_qty.get(doc.name, 0),
"postprocess": update_item,
},
},
@ -1340,8 +1340,9 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
.select(Sum(wo.qty))
.where(
(wo.production_item == i.item_code)
& (wo.sales_order == so.name) * (wo.sales_order_item == i.name)
& (wo.docstatus.lte(2))
& (wo.sales_order == so.name)
& (wo.sales_order_item == i.name)
& (wo.docstatus.lt(2))
)
.run()[0][0]
)

View File

@ -57,7 +57,7 @@ frappe.listview_settings['Sales Order'] = {
});
listview.page.add_action_item(__("Advance Payment"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Advance Payment");
erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Payment Entry");
});
}

View File

@ -1878,6 +1878,37 @@ class TestSalesOrder(FrappeTestCase):
self.assertEqual(pe.references[1].reference_name, so.name)
self.assertEqual(pe.references[1].allocated_amount, 300)
def test_delivered_item_material_request(self):
"SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."
from erpnext.manufacturing.doctype.work_order.work_order import (
make_stock_entry as make_se_from_wo,
)
from erpnext.stock.doctype.material_request.material_request import raise_work_orders
so = make_sales_order(
item_list=[
{"item_code": "_Test FG Item", "qty": 10, "rate": 100, "warehouse": "Work In Progress - _TC"}
]
)
make_stock_entry(
item_code="_Test FG Item", target="Work In Progress - _TC", qty=4, basic_rate=100
)
dn = make_delivery_note(so.name)
dn.items[0].qty = 4
dn.submit()
so.load_from_db()
self.assertEqual(so.items[0].delivered_qty, 4)
mr = make_material_request(so.name)
mr.material_request_type = "Purchase"
mr.schedule_date = today()
mr.save()
self.assertEqual(mr.items[0].qty, 6)
def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")

View File

@ -559,8 +559,10 @@ erpnext.PointOfSale.Controller = class {
item_row = this.frm.add_child('items', new_item);
if (field === 'qty' && value !== 0 && !this.allow_negative_stock)
await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse);
if (field === 'qty' && value !== 0 && !this.allow_negative_stock) {
const qty_needed = value * item_row.conversion_factor;
await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse);
}
await this.trigger_new_item_events(item_row);

View File

@ -6,7 +6,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.naming import make_autoname, revert_series_if_last
from frappe.query_builder.functions import CurDate, Sum, Timestamp
from frappe.query_builder.functions import CombineDatetime, CurDate, Sum
from frappe.utils import cint, flt, get_link_to_form, nowtime
from frappe.utils.data import add_days
from frappe.utils.jinja import render_template
@ -192,7 +192,8 @@ def get_batch_qty(
posting_time = nowtime()
query = query.where(
Timestamp(sle.posting_date, sle.posting_time) <= Timestamp(posting_date, posting_time)
CombineDatetime(sle.posting_date, sle.posting_time)
<= CombineDatetime(posting_date, posting_time)
)
out = query.run(as_list=True)[0][0] or 0
@ -376,7 +377,7 @@ def get_pos_reserved_batch_qty(filters):
p = frappe.qb.DocType("POS Invoice").as_("p")
item = frappe.qb.DocType("POS Invoice Item").as_("item")
sum_qty = frappe.query_builder.functions.Sum(item.qty).as_("qty")
sum_qty = frappe.query_builder.functions.Sum(item.stock_qty).as_("qty")
reserved_batch_qty = (
frappe.qb.from_(p)

View File

@ -15,6 +15,7 @@
"projected_qty",
"reserved_qty_for_production",
"reserved_qty_for_sub_contract",
"reserved_qty_for_production_plan",
"ma_rate",
"stock_uom",
"fcfs_rate",
@ -165,13 +166,19 @@
"oldfieldname": "stock_value",
"oldfieldtype": "Currency",
"read_only": 1
},
{
"fieldname": "reserved_qty_for_production_plan",
"fieldtype": "Float",
"label": "Reserved Qty for Production Plan",
"read_only": 1
}
],
"hide_toolbar": 1,
"idx": 1,
"in_create": 1,
"links": [],
"modified": "2022-03-30 07:22:23.868602",
"modified": "2023-05-02 23:26:21.806965",
"modified_by": "Administrator",
"module": "Stock",
"name": "Bin",

View File

@ -24,8 +24,30 @@ class Bin(Document):
- flt(self.reserved_qty)
- flt(self.reserved_qty_for_production)
- flt(self.reserved_qty_for_sub_contract)
- flt(self.reserved_qty_for_production_plan)
)
def update_reserved_qty_for_production_plan(self, skip_project_qty_update=False):
"""Update qty reserved for production from Production Plan tables
in open production plan"""
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_reserved_qty_for_production_plan,
)
self.reserved_qty_for_production_plan = get_reserved_qty_for_production_plan(
self.item_code, self.warehouse
)
self.db_set(
"reserved_qty_for_production_plan",
flt(self.reserved_qty_for_production_plan),
update_modified=True,
)
if not skip_project_qty_update:
self.set_projected_qty()
self.db_set("projected_qty", self.projected_qty, update_modified=True)
def update_reserved_qty_for_production(self):
"""Update qty reserved for production from Production Item tables
in open work orders"""
@ -35,11 +57,13 @@ class Bin(Document):
self.item_code, self.warehouse
)
self.set_projected_qty()
self.db_set(
"reserved_qty_for_production", flt(self.reserved_qty_for_production), update_modified=True
)
self.update_reserved_qty_for_production_plan(skip_project_qty_update=True)
self.set_projected_qty()
self.db_set("projected_qty", self.projected_qty, update_modified=True)
def update_reserved_qty_for_sub_contracting(self, subcontract_doctype="Subcontracting Order"):
@ -141,6 +165,7 @@ def get_bin_details(bin_name):
"planned_qty",
"reserved_qty_for_production",
"reserved_qty_for_sub_contract",
"reserved_qty_for_production_plan",
],
as_dict=1,
)
@ -188,6 +213,7 @@ def update_qty(bin_name, args):
- flt(reserved_qty)
- flt(bin_details.reserved_qty_for_production)
- flt(bin_details.reserved_qty_for_sub_contract)
- flt(bin_details.reserved_qty_for_production_plan)
)
frappe.db.set_value(

View File

@ -28,8 +28,6 @@
"column_break_18",
"project",
"dimension_col_break",
"campaign",
"source",
"custom_dimensions_section",
"currency_and_price_list",
"currency",
@ -161,11 +159,12 @@
"inter_company_reference",
"customer_group",
"territory",
"source",
"campaign",
"column_break5",
"excise_page",
"instructions",
"connections_tab",
"column_break_25"
"connections_tab"
],
"fields": [
{
@ -1339,10 +1338,6 @@
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_25",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_30",
"fieldtype": "Section Break",
@ -1403,7 +1398,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2023-02-14 04:45:44.179670",
"modified": "2023-04-21 11:15:23.931084",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",

View File

@ -118,7 +118,7 @@ class DeliveryNote(SellingController):
def so_required(self):
"""check in manage account if sales order required or not"""
if frappe.db.get_value("Selling Settings", None, "so_required") == "Yes":
if frappe.db.get_single_value("Selling Settings", "so_required") == "Yes":
for d in self.get("items"):
if not d.against_sales_order:
frappe.throw(_("Sales Order required for Item {0}").format(d.item_code))
@ -205,7 +205,7 @@ class DeliveryNote(SellingController):
super(DeliveryNote, self).validate_warehouse()
for d in self.get_item_list():
if not d["warehouse"] and frappe.db.get_value("Item", d["item_code"], "is_stock_item") == 1:
if not d["warehouse"] and frappe.get_cached_value("Item", d["item_code"], "is_stock_item") == 1:
frappe.throw(_("Warehouse required for stock Item {0}").format(d["item_code"]))
def update_current_stock(self):
@ -271,6 +271,9 @@ class DeliveryNote(SellingController):
def check_credit_limit(self):
from erpnext.selling.doctype.customer.customer import check_credit_limit
if self.per_billed == 100:
return
extra_amount = 0
validate_against_credit_limit = False
bypass_credit_limit_check_at_sales_order = cint(

View File

@ -629,7 +629,8 @@
"no_copy": 1,
"options": "Sales Order",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "against_sales_invoice",
@ -662,7 +663,8 @@
"label": "Against Sales Invoice Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "installed_qty",
@ -854,7 +856,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-04-06 09:28:29.182053",
"modified": "2023-05-01 21:05:14.175640",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",

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