Merge branch 'develop' into bank-trans-party-automatch

This commit is contained in:
Marica 2023-05-18 12:57:58 +05:30 committed by GitHub
commit 6fe5264ae2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
150 changed files with 33948 additions and 32591 deletions

View File

@ -204,8 +204,11 @@ class Account(NestedSet):
) )
def validate_account_currency(self): def validate_account_currency(self):
self.currency_explicitly_specified = True
if not self.account_currency: if not self.account_currency:
self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency") self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
self.currency_explicitly_specified = False
gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency") gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency")
@ -251,8 +254,10 @@ class Account(NestedSet):
{ {
"company": company, "company": company,
# parent account's currency should be passed down to child account's curreny # parent account's currency should be passed down to child account's curreny
# if it is None, it picks it up from default company currency, which might be unintended # if currency explicitly specified by user, child will inherit. else, default currency will be used.
"account_currency": erpnext.get_company_currency(company), "account_currency": self.account_currency
if self.currency_explicitly_specified
else erpnext.get_company_currency(company),
"parent_account": parent_acc_name_map[company], "parent_account": parent_acc_name_map[company],
} }
) )

View File

@ -5,10 +5,13 @@
import unittest import unittest
import frappe import frappe
from frappe.test_runner import make_test_records
from erpnext.accounts.doctype.account.account import merge_account, update_account_number from erpnext.accounts.doctype.account.account import merge_account, update_account_number
from erpnext.stock import get_company_default_inventory_account, get_warehouse_account from erpnext.stock import get_company_default_inventory_account, get_warehouse_account
test_dependencies = ["Company"]
class TestAccount(unittest.TestCase): class TestAccount(unittest.TestCase):
def test_rename_account(self): def test_rename_account(self):
@ -188,6 +191,58 @@ class TestAccount(unittest.TestCase):
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4") frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4")
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC5") frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC5")
def test_account_currency_sync(self):
"""
In a parent->child company setup, child should inherit parent account currency if explicitly specified.
"""
make_test_records("Company")
frappe.local.flags.pop("ignore_root_company_validation", None)
def create_bank_account():
acc = frappe.new_doc("Account")
acc.account_name = "_Test Bank JPY"
acc.parent_account = "Temporary Accounts - _TC6"
acc.company = "_Test Company 6"
return acc
acc = create_bank_account()
# Explicitly set currency
acc.account_currency = "JPY"
acc.insert()
self.assertTrue(
frappe.db.exists(
{
"doctype": "Account",
"account_name": "_Test Bank JPY",
"account_currency": "JPY",
"company": "_Test Company 7",
}
)
)
frappe.delete_doc("Account", "_Test Bank JPY - _TC6")
frappe.delete_doc("Account", "_Test Bank JPY - _TC7")
acc = create_bank_account()
# default currency is used
acc.insert()
self.assertTrue(
frappe.db.exists(
{
"doctype": "Account",
"account_name": "_Test Bank JPY",
"account_currency": "USD",
"company": "_Test Company 7",
}
)
)
frappe.delete_doc("Account", "_Test Bank JPY - _TC6")
frappe.delete_doc("Account", "_Test Bank JPY - _TC7")
def test_child_company_account_rename_sync(self): def test_child_company_account_rename_sync(self):
frappe.local.flags.pop("ignore_root_company_validation", None) frappe.local.flags.pop("ignore_root_company_validation", None)
@ -297,7 +352,7 @@ def _make_test_records(verbose=None):
# fixed asset depreciation # fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None], ["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", 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], ["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
# Receivable / Payable Account # Receivable / Payable Account
["_Test Receivable", "Current Assets", 0, "Receivable", None], ["_Test Receivable", "Current Assets", 0, "Receivable", None],

View File

@ -56,7 +56,7 @@ class BankClearance(Document):
select select
"Payment Entry" as payment_document, name as payment_entry, "Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date, 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, 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, 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 if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency

View File

@ -53,19 +53,20 @@ class BankStatementImport(DataImport):
if "Bank Account" not in json.dumps(preview["columns"]): if "Bank Account" not in json.dumps(preview["columns"]):
frappe.throw(_("Please add the Bank Account column")) frappe.throw(_("Please add the Bank Account column"))
from frappe.utils.background_jobs import is_job_queued from frappe.utils.background_jobs import is_job_enqueued
from frappe.utils.scheduler import is_scheduler_inactive from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test: if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")) frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
if not is_job_queued(self.name): job_id = f"bank_statement_import::{self.name}"
if not is_job_enqueued(job_id):
enqueue( enqueue(
start_import, start_import,
queue="default", queue="default",
timeout=6000, timeout=6000,
event="data_import", event="data_import",
job_name=self.name, job_id=job_id,
data_import=self.name, data_import=self.name,
bank_account=self.bank_account, bank_account=self.bank_account,
import_file_path=self.import_file, import_file_path=self.import_file,

View File

@ -330,10 +330,13 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
) )
elif payment_entry.payment_document == "Journal Entry": elif payment_entry.payment_document == "Journal Entry":
return frappe.db.get_value( return abs(
"Journal Entry Account", frappe.db.get_value(
{"parent": payment_entry.payment_entry, "account": gl_bank_account}, "Journal Entry Account",
"sum(credit_in_account_currency)", {"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": elif payment_entry.payment_document == "Expense Claim":

View File

@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", { frappe.ui.form.on("Journal Entry", {
setup: function(frm) { setup: function(frm) {
frm.add_fetch("bank_account", "account", "account"); 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) { refresh: function(frm) {

View File

@ -69,6 +69,7 @@ class JournalEntry(AccountsController):
self.validate_empty_accounts_table() self.validate_empty_accounts_table()
self.set_account_and_party_balance() self.set_account_and_party_balance()
self.validate_inter_company_accounts() self.validate_inter_company_accounts()
self.validate_depr_entry_voucher_type()
if self.docstatus == 0: if self.docstatus == 0:
self.apply_tax_withholding() 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: 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")) 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): def validate_stock_accounts(self):
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name) stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
for account in stock_accounts: for account in stock_accounts:
@ -233,25 +241,30 @@ class JournalEntry(AccountsController):
self.remove(d) self.remove(d)
def update_asset_value(self): def update_asset_value(self):
if self.voucher_type != "Depreciation Entry": if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
return return
processed_assets = []
for d in self.get("accounts"): for d in self.get("accounts"):
if ( 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) asset = frappe.get_doc("Asset", d.reference_name)
if asset.calculate_depreciation: if asset.calculate_depreciation:
continue fb_idx = 1
if self.finance_book:
depr_value = d.debit or d.credit for fb_row in asset.get("finance_books"):
if fb_row.finance_book == self.finance_book:
asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value) 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() asset.set_status()
@ -316,42 +329,47 @@ class JournalEntry(AccountsController):
if self.voucher_type != "Depreciation Entry": if self.voucher_type != "Depreciation Entry":
return return
processed_assets = []
for d in self.get("accounts"): for d in self.get("accounts"):
if ( 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) asset = frappe.get_doc("Asset", d.reference_name)
if asset.calculate_depreciation: if asset.calculate_depreciation:
je_found = False je_found = False
for row in asset.get("finance_books"): for fb_row in asset.get("finance_books"):
if je_found: if je_found:
break 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 []: for s in depr_schedule or []:
if s.journal_entry == self.name: if s.journal_entry == self.name:
s.db_set("journal_entry", None) s.db_set("journal_entry", None)
row.value_after_depreciation += s.depreciation_amount fb_row.value_after_depreciation += d.debit
row.db_update() fb_row.db_update()
asset.set_status()
je_found = True je_found = True
break 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: else:
depr_value = d.debit or d.credit asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit)
asset.set_status()
asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value)
asset.set_status()
def unlink_inter_company_jv(self): def unlink_inter_company_jv(self):
if ( if (

View File

@ -2,6 +2,21 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Journal Entry Template", { 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) { refresh: function(frm) {
frappe.model.set_default_values(frm.doc); frappe.model.set_default_values(frm.doc);
@ -19,18 +34,6 @@ frappe.ui.form.on("Journal Entry Template", {
return { filters: filters }; 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) { voucher_type: function(frm) {
var add_accounts = function(doc, r) { var add_accounts = function(doc, r) {

View File

@ -4,7 +4,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils.background_jobs import is_job_queued from frappe.utils.background_jobs import is_job_enqueued
from erpnext.accounts.doctype.account.account import merge_account from erpnext.accounts.doctype.account.account import merge_account
@ -17,13 +17,14 @@ class LedgerMerge(Document):
if is_scheduler_inactive() and not frappe.flags.in_test: if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot merge accounts."), title=_("Scheduler Inactive")) frappe.throw(_("Scheduler is inactive. Cannot merge accounts."), title=_("Scheduler Inactive"))
if not is_job_queued(self.name): job_id = f"ledger_merge::{self.name}"
if not is_job_enqueued(job_id):
enqueue( enqueue(
start_merge, start_merge,
queue="default", queue="default",
timeout=6000, timeout=6000,
event="ledger_merge", event="ledger_merge",
job_name=self.name, job_id=job_id,
docname=self.name, docname=self.name,
now=frappe.conf.developer_mode or frappe.flags.in_test, now=frappe.conf.developer_mode or frappe.flags.in_test,
) )

View File

@ -6,7 +6,7 @@ import frappe
from frappe import _, scrub from frappe import _, scrub
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, nowdate from frappe.utils import flt, nowdate
from frappe.utils.background_jobs import enqueue, is_job_queued from frappe.utils.background_jobs import enqueue, is_job_enqueued
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
@ -212,13 +212,15 @@ class OpeningInvoiceCreationTool(Document):
if is_scheduler_inactive() and not frappe.flags.in_test: if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")) frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
if not is_job_queued(self.name): job_id = f"opening_invoice::{self.name}"
if not is_job_enqueued(job_id):
enqueue( enqueue(
start_import, start_import,
queue="default", queue="default",
timeout=6000, timeout=6000,
event="opening_invoice_creation", event="opening_invoice_creation",
job_name=self.name, job_id=job_id,
invoices=invoices, invoices=invoices,
now=frappe.conf.developer_mode or frappe.flags.in_test, now=frappe.conf.developer_mode or frappe.flags.in_test,
) )

View File

@ -904,7 +904,7 @@ frappe.ui.form.on('Payment Entry', {
function(d) { return flt(d.amount) })); function(d) { return flt(d.amount) }));
frm.set_value("difference_amount", difference_amount - total_deductions + frm.set_value("difference_amount", difference_amount - total_deductions +
frm.doc.base_total_taxes_and_charges); flt(frm.doc.base_total_taxes_and_charges));
frm.events.hide_unhide_fields(frm); frm.events.hide_unhide_fields(frm);
}, },

View File

@ -654,6 +654,28 @@ class PaymentEntry(AccountsController):
self.precision("base_received_amount"), 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): def set_total_allocated_amount(self):
if self.payment_type == "Internal Transfer": if self.payment_type == "Internal Transfer":
return return
@ -662,9 +684,7 @@ class PaymentEntry(AccountsController):
for d in self.get("references"): for d in self.get("references"):
if d.allocated_amount: if d.allocated_amount:
total_allocated_amount += flt(d.allocated_amount) total_allocated_amount += flt(d.allocated_amount)
base_total_allocated_amount += flt( base_total_allocated_amount += self.calculate_base_allocated_amount_for_reference(d)
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
)
self.total_allocated_amount = abs(total_allocated_amount) self.total_allocated_amount = abs(total_allocated_amount)
self.base_total_allocated_amount = abs(base_total_allocated_amount) self.base_total_allocated_amount = abs(base_total_allocated_amount)
@ -881,9 +901,7 @@ class PaymentEntry(AccountsController):
} }
) )
allocated_amount_in_company_currency = flt( allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("paid_amount")
)
gle.update( gle.update(
{ {
@ -1693,7 +1711,10 @@ def get_payment_entry(
): ):
reference_doc = None reference_doc = None
doc = frappe.get_doc(dt, dn) 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)) frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
if not party_type: if not party_type:
@ -1712,6 +1733,13 @@ def get_payment_entry(
# bank or cash # bank or cash
bank = get_bank_cash_account(doc, bank_account) 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( paid_amount, received_amount = set_paid_amount_and_received_amount(
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
) )
@ -1928,19 +1956,27 @@ def set_paid_amount_and_received_amount(
paid_amount = received_amount = 0 paid_amount = received_amount = 0
if party_account_currency == bank.account_currency: if party_account_currency == bank.account_currency:
paid_amount = received_amount = abs(outstanding_amount) 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: else:
received_amount = abs(outstanding_amount) company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency")
if bank_amount: if payment_type == "Receive":
paid_amount = bank_amount 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: else:
# if party account currency and bank currency is different then populate paid amount as well received_amount = abs(outstanding_amount)
paid_amount = received_amount * doc.get("conversion_rate", 1) 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 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") so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
self.assertEqual(so_advance_paid, 0) 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): def test_payment_entry_for_blocked_supplier_invoice(self):
supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1 supplier.on_hold = 1

View File

@ -205,7 +205,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
self.assertRaises(frappe.ValidationError, jv1.submit) 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 `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 `tabPeriod Closing Voucher` where company='Test PCV Company'")
frappe.db.sql("delete from `tabAccount Closing Balance` 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, 500)
self.assertEqual(cc2_closing_balance.credit_in_account_currency, 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): def make_period_closing_voucher(self, posting_date=None, submit=True):
surplus_account = create_account() surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1") cost_center = create_cost_center("Test Cost Center 1")

View File

@ -9,7 +9,7 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import map_child_doc, map_doc from frappe.model.mapper import map_child_doc, map_doc
from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
from frappe.utils.background_jobs import enqueue, is_job_queued from frappe.utils.background_jobs import enqueue, is_job_enqueued
from frappe.utils.scheduler import is_scheduler_inactive from frappe.utils.scheduler import is_scheduler_inactive
@ -483,15 +483,15 @@ def enqueue_job(job, **kwargs):
closing_entry = kwargs.get("closing_entry") or {} closing_entry = kwargs.get("closing_entry") or {}
job_name = closing_entry.get("name") job_id = "pos_invoice_merge::" + str(closing_entry.get("name"))
if not is_job_queued(job_name): if not is_job_enqueued(job_id):
enqueue( enqueue(
job, job,
**kwargs, **kwargs,
queue="long", queue="long",
timeout=10000, timeout=10000,
event="processing_merge_logs", event="processing_merge_logs",
job_name=job_name, job_id=job_id,
now=frappe.conf.developer_mode or frappe.flags.in_test now=frappe.conf.developer_mode or frappe.flags.in_test
) )

View File

@ -164,7 +164,7 @@ def trigger_reconciliation_for_queued_docs():
Fetch queued docs and start reconciliation process for each one Fetch queued docs and start reconciliation process for each one
""" """
if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"): if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
frappe.throw( frappe.msgprint(
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format( _("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
get_link_to_form("Accounts Settings", "Accounts Settings") get_link_to_form("Accounts Settings", "Accounts Settings")
) )

View File

@ -15,7 +15,12 @@
</div> </div>
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2> <h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
<div> <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;"> <h5 style="float: right;">
{{ _("Date: ") }} {{ _("Date: ") }}
<b>{{ frappe.format(filters.from_date, 'Date')}} <b>{{ frappe.format(filters.from_date, 'Date')}}

View File

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

View File

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

View File

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

View File

@ -303,7 +303,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
apply_tds(frm) { apply_tds(frm) {
var me = this; var me = this;
me.frm.set_value("tax_withheld_vouchers", []);
if (!me.frm.doc.apply_tds) { if (!me.frm.doc.apply_tds) {
me.frm.set_value("tax_withholding_category", ''); me.frm.set_value("tax_withholding_category", '');
me.frm.set_df_property("tax_withholding_category", "hidden", 1); me.frm.set_df_property("tax_withholding_category", "hidden", 1);

View File

@ -89,6 +89,7 @@
"column_break8", "column_break8",
"grand_total", "grand_total",
"rounding_adjustment", "rounding_adjustment",
"use_company_roundoff_cost_center",
"rounded_total", "rounded_total",
"in_words", "in_words",
"total_advance", "total_advance",
@ -1368,6 +1369,7 @@
"options": "Warehouse", "options": "Warehouse",
"print_hide": 1, "print_hide": 1,
"print_width": "50px", "print_width": "50px",
"ignore_user_permissions": 1,
"width": "50px" "width": "50px"
}, },
{ {
@ -1559,13 +1561,19 @@
"fieldname": "only_include_allocated_payments", "fieldname": "only_include_allocated_payments",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Only Include Allocated Payments" "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", "icon": "fa fa-file-text",
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-04-03 22:57:14.074982", "modified": "2023-04-29 12:57:50.832598",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -978,7 +978,7 @@ class PurchaseInvoice(BuyingController):
def make_precision_loss_gl_entry(self, gl_entries): def make_precision_loss_gl_entry(self, gl_entries):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( 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( precision_loss = self.get("base_net_total") - flt(
@ -992,7 +992,9 @@ class PurchaseInvoice(BuyingController):
"account": round_off_account, "account": round_off_account,
"against": self.supplier, "against": self.supplier,
"credit": precision_loss, "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"), "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 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( 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( gl_entries.append(
@ -1396,7 +1398,9 @@ class PurchaseInvoice(BuyingController):
"against": self.supplier, "against": self.supplier,
"debit_in_account_currency": self.rounding_adjustment, "debit_in_account_currency": self.rounding_adjustment,
"debit": self.base_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, item=self,
) )

View File

@ -79,6 +79,7 @@
"column_break5", "column_break5",
"grand_total", "grand_total",
"rounding_adjustment", "rounding_adjustment",
"use_company_roundoff_cost_center",
"rounded_total", "rounded_total",
"in_words", "in_words",
"total_advance", "total_advance",
@ -2135,6 +2136,12 @@
"fieldname": "only_include_allocated_payments", "fieldname": "only_include_allocated_payments",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Only Include Allocated Payments" "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", "icon": "fa fa-file-text",
@ -2147,7 +2154,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2023-04-03 22:55:14.206473", "modified": "2023-04-28 14:15:59.901154",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -1464,7 +1464,7 @@ class SalesInvoice(SellingController):
and not self.is_internal_transfer() and not self.is_internal_transfer()
): ):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( 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( gl_entries.append(
@ -1476,7 +1476,9 @@ class SalesInvoice(SellingController):
self.rounding_adjustment, self.precision("rounding_adjustment") self.rounding_adjustment, self.precision("rounding_adjustment")
), ),
"credit": flt(self.base_rounding_adjustment, self.precision("base_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, item=self,
) )

View File

@ -124,6 +124,7 @@
"fieldname": "rate", "fieldname": "rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rate", "label": "Rate",
"options": "Company:company:default_currency",
"reqd": 1 "reqd": 1
}, },
{ {
@ -147,6 +148,7 @@
"fieldname": "amount", "fieldname": "amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Amount", "label": "Amount",
"options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
}, },
{ {

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( ldc_name = frappe.db.get_value(
"Lower Deduction Certificate", "Lower Deduction Certificate",
{ {
@ -223,6 +223,7 @@ def get_lower_deduction_certificate(tax_details, pan_no):
"tax_withholding_category": tax_details.tax_withholding_category, "tax_withholding_category": tax_details.tax_withholding_category,
"valid_from": (">=", tax_details.from_date), "valid_from": (">=", tax_details.from_date),
"valid_upto": ("<=", tax_details.to_date), "valid_upto": ("<=", tax_details.to_date),
"company": company,
}, },
"name", "name",
) )
@ -255,7 +256,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_amount = 0 tax_amount = 0
if party_type == "Supplier": 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: if tax_deducted:
net_total = inv.tax_withholding_net_total net_total = inv.tax_withholding_net_total
if ldc: if ldc:
@ -301,7 +302,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
"docstatus": 1, "docstatus": 1,
} }
if not tax_details.get("consider_party_ledger_amount") and doctype != "Sales Invoice": if doctype != "Sales Invoice":
filters.update( filters.update(
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")} {"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
) )

View File

@ -110,9 +110,9 @@ class TestTaxWithholdingCategory(unittest.TestCase):
invoices.append(pi1) invoices.append(pi1)
# Cumulative threshold is 30000 # Cumulative threshold is 30000
# Threshold calculation should be on both the invoices # Threshold calculation should be only on the Second invoice
# TDS should be applied only on 1000 # Second didn't breach, no TDS should be applied
self.assertEqual(pi1.taxes[0].tax_amount, 1000) self.assertEqual(pi1.taxes, [])
for d in reversed(invoices): for d in reversed(invoices):
d.cancel() d.cancel()

View File

@ -475,7 +475,9 @@ def update_accounting_dimensions(round_off_gle):
round_off_gle[dimension] = dimension_values.get(dimension) 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( round_off_account, round_off_cost_center = frappe.get_cached_value(
"Company", company, ["round_off_account", "round_off_cost_center"] "Company", company, ["round_off_account", "round_off_cost_center"]
) or [None, None] ) 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) meta = frappe.get_meta(voucher_type)
# Give first preference to parent cost center for round off GLE # 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") parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center")
if parent_cost_center: if parent_cost_center:
round_off_cost_center = parent_cost_center round_off_cost_center = parent_cost_center

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_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.asset_category, 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 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 gle.debit
else else
@ -160,7 +138,7 @@ def get_assets(filters):
aca.parent = a.asset_category and aca.company_name = %(company)s aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on join `tabCompany` company on
company.name = %(company)s 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 group by a.asset_category
union union
SELECT a.asset_category, SELECT a.asset_category,

View File

@ -80,7 +80,7 @@ def get_entries(filters):
payment_entries = frappe.db.sql( payment_entries = frappe.db.sql(
"""SELECT """SELECT
"Payment Entry", name, posting_date, reference_no, clearance_date, party, "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 FROM
`tabPayment Entry` `tabPayment Entry`
WHERE WHERE

View File

@ -538,13 +538,21 @@ def apply_additional_conditions(doctype, query, from_date, ignore_closing_entrie
query = query.where(gl_entry.cost_center.isin(filters.cost_center)) query = query.where(gl_entry.cost_center.isin(filters.cost_center))
if filters.get("include_default_book_entries"): 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( 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()) | (gl_entry.finance_book.isnull())
) )
else: else:
query = query.where( 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: if accounting_dimensions:

View File

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

View File

@ -244,13 +244,23 @@ def get_conditions(filters):
if filters.get("project"): if filters.get("project"):
conditions.append("project in %(project)s") conditions.append("project in %(project)s")
if filters.get("finance_book"): if filters.get("include_default_book_entries"):
if filters.get("include_default_book_entries"): if filters.get("finance_book"):
conditions.append( if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr(
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" 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: 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 in ('') OR finance_book IS NULL)")
if not filters.get("show_cancelled_entries"): if not filters.get("show_cancelled_entries"):
conditions.append("is_cancelled = 0") 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) { if (column.fieldname == "sales_invoice" && column.options == "Item" && data && data.indent == 0) {
column._options = "Sales Invoice"; column._options = "Sales Invoice";
} else { } else {
column._options = "Item"; column._options = "";
} }
value = default_formatter(value, row, column, data); value = default_formatter(value, row, column, data);

View File

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

View File

@ -19,14 +19,19 @@ def execute(filters=None):
return _execute(filters) return _execute(filters)
def _execute(filters=None, additional_table_columns=None, additional_query_columns=None): def _execute(
filters=None,
additional_table_columns=None,
additional_query_columns=None,
additional_conditions=None,
):
if not filters: if not filters:
filters = {} filters = {}
columns = get_columns(additional_table_columns, filters) columns = get_columns(additional_table_columns, filters)
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
item_list = get_items(filters, additional_query_columns) item_list = get_items(filters, additional_query_columns, additional_conditions)
if item_list: if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency) itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
@ -328,7 +333,7 @@ def get_columns(additional_table_columns, filters):
return columns return columns
def get_conditions(filters): def get_conditions(filters, additional_conditions=None):
conditions = "" conditions = ""
for opts in ( for opts in (
@ -341,6 +346,9 @@ def get_conditions(filters):
if filters.get(opts[0]): if filters.get(opts[0]):
conditions += opts[1] conditions += opts[1]
if additional_conditions:
conditions += additional_conditions
if filters.get("mode_of_payment"): if filters.get("mode_of_payment"):
conditions += """ and exists(select name from `tabSales Invoice Payment` conditions += """ and exists(select name from `tabSales Invoice Payment`
where parent=`tabSales Invoice`.name where parent=`tabSales Invoice`.name
@ -376,8 +384,8 @@ def get_group_by_conditions(filters, doctype):
return "ORDER BY `tab{0}`.{1}".format(doctype, frappe.scrub(filters.get("group_by"))) return "ORDER BY `tab{0}`.{1}".format(doctype, frappe.scrub(filters.get("group_by")))
def get_items(filters, additional_query_columns): def get_items(filters, additional_query_columns, additional_conditions=None):
conditions = get_conditions(filters) conditions = get_conditions(filters, additional_conditions)
if additional_query_columns: if additional_query_columns:
additional_query_columns = ", " + ", ".join(additional_query_columns) additional_query_columns = ", " + ", ".join(additional_query_columns)

View File

@ -4,7 +4,6 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt
def execute(filters=None): def execute(filters=None):
@ -66,12 +65,6 @@ def get_result(
else: else:
total_amount_credited += entry.credit 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: if tds_deducted:
row = { row = {
"pan" "pan"

View File

@ -248,8 +248,15 @@ def get_opening_balance(
opening_balance = opening_balance.where(closing_balance.project == filters.project) opening_balance = opening_balance.where(closing_balance.project == filters.project)
if filters.get("include_default_book_entries"): 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( 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()) | (closing_balance.finance_book.isnull())
) )
else: else:

View File

@ -1374,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 if wh_details.account == account and not wh_details.is_group
] ]
total_stock_value = 0.0 total_stock_value = get_stock_value_on(related_warehouses, posting_date)
for warehouse in related_warehouses:
value = get_stock_value_on(warehouse, posting_date)
total_stock_value += value
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses 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.voucher_type = "Depreciation Entry"
je.naming_series = depreciation_series je.naming_series = depreciation_series
je.company = asset.company 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( je.append(
"accounts", "accounts",

View File

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

View File

@ -691,7 +691,7 @@ class TestDepreciationMethods(AssetSetup):
) )
self.assertEqual(asset.status, "Draft") self.assertEqual(asset.status, "Draft")
expected_schedules = [["2032-12-31", 30000.0, 77095.89], ["2033-06-06", 12904.11, 90000.0]] expected_schedules = [["2032-12-31", 42904.11, 90000.0]]
schedules = [ schedules = [
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] [cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Draft") for d in get_depr_schedule(asset.name, "Draft")
@ -1511,7 +1511,7 @@ class TestDepreciationBasics(AssetSetup):
) )
self.assertEqual(asset.status, "Submitted") 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( jv = make_journal_entry(
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False "_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
@ -1524,12 +1524,68 @@ class TestDepreciationBasics(AssetSetup):
jv.submit() jv.submit()
asset.reload() asset.reload()
self.assertEqual(asset.get("value_after_depreciation"), 99900) self.assertEqual(asset.get_value_after_depreciation(), 99900)
jv.cancel() jv.cancel()
asset.reload() 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(): def create_asset_data():

View File

@ -337,7 +337,7 @@ class AssetDepreciationSchedule(Document):
depreciation_amount += value_after_depreciation - row.expected_value_after_useful_life depreciation_amount += value_after_depreciation - row.expected_value_after_useful_life
skip_row = True skip_row = True
if depreciation_amount > 0: if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0:
self.add_depr_schedule_row( self.add_depr_schedule_row(
schedule_date, schedule_date,
depreciation_amount, depreciation_amount,
@ -521,9 +521,11 @@ def get_straight_line_or_manual_depr_amount(asset, row):
) )
# if the Depreciation Schedule is being prepared for the first time # if the Depreciation Schedule is being prepared for the first time
else: else:
return (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / flt( return (
row.total_number_of_depreciations flt(asset.gross_purchase_amount)
) - flt(asset.opening_accumulated_depreciation)
- flt(row.expected_value_after_useful_life)
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
def get_wdv_or_dd_depr_amount( def get_wdv_or_dd_depr_amount(

View File

@ -150,7 +150,9 @@ class AssetValueAdjustment(Document):
if d.depreciation_method in ("Straight Line", "Manual"): if d.depreciation_method in ("Straight Line", "Manual"):
end_date = max(s.schedule_date for s in depr_schedule) end_date = max(s.schedule_date for s in depr_schedule)
total_days = date_diff(end_date, self.date) 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 from_date = self.date
else: else:
no_of_depreciations = len([s.name for s in depr_schedule if not s.journal_entry]) 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"), label: __("Finance Book"),
fieldtype: "Link", fieldtype: "Link",
options: "Finance Book", options: "Finance Book",
depends_on: "eval: doc.only_depreciable_assets == 1", depends_on: "eval: doc.filter_by_finance_book == 1",
}, },
{ {
fieldname:"only_depreciable_assets", fieldname:"filter_by_finance_book",
label: __("Only depreciable assets"), label: __("Filter by Finance Book"),
fieldtype: "Check" fieldtype: "Check"
}, },
{ {

View File

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

View File

@ -1171,6 +1171,7 @@
"depends_on": "is_internal_supplier", "depends_on": "is_internal_supplier",
"fieldname": "set_from_warehouse", "fieldname": "set_from_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Set From Warehouse", "label": "Set From Warehouse",
"options": "Warehouse" "options": "Warehouse"
}, },
@ -1271,7 +1272,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-04-14 16:42:29.448464", "modified": "2023-05-07 20:18:09.196799",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@ -43,7 +43,7 @@ frappe.listview_settings['Purchase Order'] = {
}); });
listview.page.add_action_item(__("Advance Payment"), ()=>{ 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

@ -392,6 +392,9 @@ class AccountsController(TransactionBase):
) )
def validate_inter_company_reference(self): def validate_inter_company_reference(self):
if self.get("is_return"):
return
if self.doctype not in ("Purchase Invoice", "Purchase Receipt"): if self.doctype not in ("Purchase Invoice", "Purchase Receipt"):
return return
@ -1662,7 +1665,10 @@ class AccountsController(TransactionBase):
) )
self.append("payment_schedule", data) 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"): for d in self.get("payment_schedule"):
if d.invoice_portion: if d.invoice_portion:
d.payment_amount = flt( d.payment_amount = flt(
@ -1676,6 +1682,9 @@ class AccountsController(TransactionBase):
d.base_payment_amount = flt( d.base_payment_amount = flt(
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount") d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
) )
else:
self.fetch_payment_terms_from_order(po_or_so, doctype)
self.ignore_default_payment_terms_template = 1
def get_order_details(self): def get_order_details(self):
if self.doctype == "Sales Invoice": if self.doctype == "Sales Invoice":
@ -1901,12 +1910,14 @@ class AccountsController(TransactionBase):
reconcilation_entry.party = secondary_party reconcilation_entry.party = secondary_party
reconcilation_entry.reference_type = self.doctype reconcilation_entry.reference_type = self.doctype
reconcilation_entry.reference_name = self.name 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.account = primary_account
advance_entry.party_type = primary_party_type advance_entry.party_type = primary_party_type
advance_entry.party = primary_party 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" advance_entry.is_advance = "Yes"
if self.doctype == "Sales Invoice": if self.doctype == "Sales Invoice":

View File

@ -53,13 +53,17 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
doctype = "Lead" doctype = "Lead"
fields = get_fields(doctype, ["name", "lead_name", "company_name"]) fields = get_fields(doctype, ["name", "lead_name", "company_name"])
searchfields = frappe.get_meta(doctype).get_search_fields()
searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
return frappe.db.sql( return frappe.db.sql(
"""select {fields} from `tabLead` """select {fields} from `tabLead`
where docstatus < 2 where docstatus < 2
and ifnull(status, '') != 'Converted' and ifnull(status, '') != 'Converted'
and ({key} like %(txt)s and ({key} like %(txt)s
or lead_name like %(txt)s or lead_name like %(txt)s
or company_name like %(txt)s) or company_name like %(txt)s
or {scond})
{mcond} {mcond}
order by order by
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end), (case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
@ -68,7 +72,12 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
idx desc, idx desc,
name, lead_name name, lead_name
limit %(page_len)s offset %(start)s""".format( limit %(page_len)s offset %(start)s""".format(
**{"fields": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)} **{
"fields": ", ".join(fields),
"key": searchfield,
"scond": searchfields,
"mcond": get_match_cond(doctype),
}
), ),
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
) )
@ -576,7 +585,9 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @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 ( from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
get_dimension_filter_map, get_dimension_filter_map,
) )
@ -617,7 +628,12 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters)
query_filters.append(["name", query_selector, dimensions]) query_filters.append(["name", query_selector, dimensions])
output = frappe.get_list( 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)] return [tuple(d) for d in set(output)]

View File

@ -168,7 +168,7 @@ class SellingController(StockController):
self.round_floats_in(sales_person) self.round_floats_in(sales_person)
sales_person.allocated_amount = flt( sales_person.allocated_amount = flt(
self.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0, flt(self.amount_eligible_for_commission) * sales_person.allocated_percentage / 100.0,
self.precision("allocated_amount", sales_person), self.precision("allocated_amount", sales_person),
) )

View File

@ -329,9 +329,10 @@ class StockController(AccountsController):
"""Create batches if required. Called before submit""" """Create batches if required. Called before submit"""
for d in self.items: for d in self.items:
if d.get(warehouse_field) and not d.batch_no: 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"] "Item", d.item_code, ["has_batch_no", "create_new_batch"]
) )
if has_batch_no and create_new_batch: if has_batch_no and create_new_batch:
d.batch_no = ( d.batch_no = (
frappe.get_doc( frappe.get_doc(
@ -414,7 +415,7 @@ class StockController(AccountsController):
"voucher_no": self.name, "voucher_no": self.name,
"voucher_detail_no": d.name, "voucher_detail_no": d.name,
"actual_qty": (self.docstatus == 1 and 1 or -1) * flt(d.get("stock_qty")), "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" "Item", args.get("item_code") or d.get("item_code"), "stock_uom"
), ),
"incoming_rate": 0, "incoming_rate": 0,
@ -441,7 +442,43 @@ class StockController(AccountsController):
if not dimension: if not dimension:
continue continue
if row.get(dimension.source_fieldname): if self.doctype in [
"Purchase Invoice",
"Purchase Receipt",
"Sales Invoice",
"Delivery Note",
"Stock Entry",
]:
if (
(
sl_dict.actual_qty > 0
and not self.get("is_return")
or sl_dict.actual_qty < 0
and self.get("is_return")
)
and self.doctype in ["Purchase Invoice", "Purchase Receipt"]
) or (
(
sl_dict.actual_qty < 0
and not self.get("is_return")
or sl_dict.actual_qty > 0
and self.get("is_return")
)
and self.doctype in ["Sales Invoice", "Delivery Note", "Stock Entry"]
):
sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
else:
fieldname_start_with = "to"
if self.doctype in ["Purchase Invoice", "Purchase Receipt"]:
fieldname_start_with = "from"
fieldname = f"{fieldname_start_with}_{dimension.source_fieldname}"
sl_dict[dimension.target_fieldname] = row.get(fieldname)
if not sl_dict.get(dimension.target_fieldname):
sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
elif row.get(dimension.source_fieldname):
sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname) sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
if not sl_dict.get(dimension.target_fieldname) and dimension.fetch_from_parent: if not sl_dict.get(dimension.target_fieldname) and dimension.fetch_from_parent:
@ -609,7 +646,7 @@ class StockController(AccountsController):
def validate_customer_provided_item(self): def validate_customer_provided_item(self):
for d in self.get("items"): for d in self.get("items"):
# Customer Provided parts will have zero valuation rate # 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 d.allow_zero_valuation_rate = 1
def set_rate_of_stock_uom(self): def set_rate_of_stock_uom(self):
@ -722,7 +759,7 @@ class StockController(AccountsController):
message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link) message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link)
return message return message
def repost_future_sle_and_gle(self): def repost_future_sle_and_gle(self, force=False):
args = frappe._dict( args = frappe._dict(
{ {
"posting_date": self.posting_date, "posting_date": self.posting_date,
@ -733,7 +770,10 @@ class StockController(AccountsController):
} }
) )
if future_sle_exists(args) or repost_required_for_queue(self): if self.docstatus == 2:
force = True
if force or future_sle_exists(args) or repost_required_for_queue(self):
item_based_reposting = cint( item_based_reposting = cint(
frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting") frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")
) )
@ -894,9 +934,6 @@ def future_sle_exists(args, sl_entries=None):
) )
for d in data: 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 frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row
return len(data) return len(data)
@ -919,7 +956,7 @@ def validate_future_sle_not_exists(args, key, sl_entries=None):
def get_cached_data(args, key): def get_cached_data(args, key):
if key not in frappe.local.future_sle: if key not in frappe.local.future_sle:
return False frappe.local.future_sle[key] = frappe._dict({})
if args.get("item_code"): if args.get("item_code"):
item_key = (args.get("item_code"), args.get("warehouse")) item_key = (args.get("item_code"), args.get("warehouse"))

View File

@ -741,7 +741,7 @@ class SubcontractingController(StockController):
sco_doc = frappe.get_doc("Subcontracting Order", sco) sco_doc = frappe.get_doc("Subcontracting Order", sco)
sco_doc.update_status() sco_doc.update_status()
def set_missing_values_in_additional_costs(self): def calculate_additional_costs(self):
self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs")) self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs"))
if self.total_additional_costs: if self.total_additional_costs:

View File

@ -36,7 +36,7 @@ class TestSubcontractingController(FrappeTestCase):
sco.remove_empty_rows() sco.remove_empty_rows()
self.assertEqual((len_before - 1), len(sco.service_items)) self.assertEqual((len_before - 1), len(sco.service_items))
def test_set_missing_values_in_additional_costs(self): def test_calculate_additional_costs(self):
sco = get_subcontracting_order(do_not_submit=1) sco = get_subcontracting_order(do_not_submit=1)
rate_without_additional_cost = sco.items[0].rate rate_without_additional_cost = sco.items[0].rate

View File

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

View File

@ -165,6 +165,7 @@
"fieldname": "slide_3_content_align", "fieldname": "slide_3_content_align",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Content Align", "label": "Content Align",
"options": "Left\nCentre\nRight",
"reqd": 0 "reqd": 0
}, },
{ {
@ -214,6 +215,7 @@
"fieldname": "slide_4_content_align", "fieldname": "slide_4_content_align",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Content Align", "label": "Content Align",
"options": "Left\nCentre\nRight",
"reqd": 0 "reqd": 0
}, },
{ {
@ -263,6 +265,7 @@
"fieldname": "slide_5_content_align", "fieldname": "slide_5_content_align",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Content Align", "label": "Content Align",
"options": "Left\nCentre\nRight",
"reqd": 0 "reqd": 0
}, },
{ {
@ -274,7 +277,7 @@
} }
], ],
"idx": 2, "idx": 2,
"modified": "2021-02-24 15:57:05.889709", "modified": "2023-05-12 15:03:57.604060",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "E-commerce", "module": "E-commerce",
"name": "Hero Slider", "name": "Hero Slider",

View File

@ -48,7 +48,8 @@ frappe.ui.form.on("BOM", {
return { return {
query: "erpnext.manufacturing.doctype.bom.bom.item_query", query: "erpnext.manufacturing.doctype.bom.bom.item_query",
filters: { filters: {
"item_code": doc.item "include_item_in_manufacturing": 1,
"is_fixed_asset": 0
} }
}; };
}); });
@ -652,7 +653,7 @@ frappe.ui.form.on("BOM Operation", "operation", function(frm, cdt, cdn) {
frappe.ui.form.on("BOM Operation", "workstation", function(frm, cdt, cdn) { frappe.ui.form.on("BOM Operation", "workstation", function(frm, cdt, cdn) {
var d = locals[cdt][cdn]; var d = locals[cdt][cdn];
if(!d.workstation) return;
frappe.call({ frappe.call({
"method": "frappe.client.get", "method": "frappe.client.get",
args: { args: {

View File

@ -1317,7 +1317,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if not field in searchfields 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 = {} or_cond_filters = {}
if txt: if txt:
@ -1339,8 +1339,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if not has_variants: if not has_variants:
query_filters["has_variants"] = 0 query_filters["has_variants"] = 0
if filters and filters.get("is_stock_item"): if filters:
query_filters["is_stock_item"] = 1 for fieldname, value in filters.items():
query_filters[fieldname] = value
return frappe.get_list( return frappe.get_list(
"Item", "Item",

View File

@ -698,6 +698,45 @@ class TestBOM(FrappeTestCase):
bom.update_cost() bom.update_cost()
self.assertFalse(bom.flags.cost_updated) self.assertFalse(bom.flags.cost_updated)
def test_do_not_include_manufacturing_and_fixed_items(self):
from erpnext.manufacturing.doctype.bom.bom import item_query
if not frappe.db.exists("Asset Category", "Computers-Test"):
doc = frappe.get_doc({"doctype": "Asset Category", "asset_category_name": "Computers-Test"})
doc.flags.ignore_mandatory = True
doc.insert()
for item_code, properties in {
"_Test RM Item 1 Do Not Include In Manufacture": {
"is_stock_item": 1,
"include_item_in_manufacturing": 0,
},
"_Test RM Item 2 Fixed Asset Item": {
"is_fixed_asset": 1,
"is_stock_item": 0,
"asset_category": "Computers-Test",
},
"_Test RM Item 3 Manufacture Item": {"is_stock_item": 1, "include_item_in_manufacturing": 1},
}.items():
make_item(item_code, properties)
data = item_query(
"Item",
txt="_Test RM Item",
searchfield="name",
start=0,
page_len=20000,
filters={"include_item_in_manufacturing": 1, "is_fixed_asset": 0},
)
items = []
for row in data:
items.append(row[0])
self.assertTrue("_Test RM Item 1 Do Not Include In Manufacture" not in items)
self.assertTrue("_Test RM Item 2 Fixed Asset Item" not in items)
self.assertTrue("_Test RM Item 3 Manufacture Item" in items)
def get_default_bom(item_code="_Test FG Item 2"): def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})

View File

@ -87,6 +87,12 @@ class JobCard(Document):
frappe.db.get_value("Work Order Operation", self.operation_id, "completed_qty") 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_qty = frappe.get_all(
"Job Card", "Job Card",
fields=["sum(for_quantity)"], fields=["sum(for_quantity)"],
@ -101,8 +107,17 @@ class JobCard(Document):
job_card_qty = flt(job_card_qty[0][0]) if job_card_qty else 0 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): if job_card_qty and ((job_card_qty - completed_qty) > wo_qty):
msg = f"""Job Card quantity cannot be greater than form_link = get_link_to_form("Manufacturing Settings", "Manufacturing Settings")
Work Order quantity for the operation {self.operation}"""
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")) frappe.throw(_(msg), title=_("Extra Job Card Quantity"))
def set_sub_operations(self): def set_sub_operations(self):
@ -547,6 +562,7 @@ class JobCard(Document):
) )
def update_work_order_data(self, for_quantity, time_in_mins, wo): def update_work_order_data(self, for_quantity, time_in_mins, wo):
workstation_hour_rate = frappe.get_value("Workstation", self.workstation, "hour_rate")
jc = frappe.qb.DocType("Job Card") jc = frappe.qb.DocType("Job Card")
jctl = frappe.qb.DocType("Job Card Time Log") jctl = frappe.qb.DocType("Job Card Time Log")
@ -572,6 +588,7 @@ class JobCard(Document):
if data.get("workstation") != self.workstation: if data.get("workstation") != self.workstation:
# workstations can change in a job card # workstations can change in a job card
data.workstation = self.workstation data.workstation = self.workstation
data.hour_rate = flt(workstation_hour_rate)
wo.flags.ignore_validate_update_after_submit = True wo.flags.ignore_validate_update_after_submit = True
wo.update_operation_status() wo.update_operation_status()

View File

@ -272,6 +272,42 @@ class TestJobCard(FrappeTestCase):
transfer_entry_2.insert() transfer_entry_2.insert()
self.assertRaises(JobCardOverTransferError, transfer_entry_2.submit) self.assertRaises(JobCardOverTransferError, transfer_entry_2.submit)
@change_settings("Manufacturing Settings", {"job_card_excess_transfer": 0})
def test_job_card_excess_material_transfer_with_no_reference(self):
self.transfer_material_against = "Job Card"
self.source_warehouse = "Stores - _TC"
self.generate_required_stock(self.work_order)
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
# fully transfer both RMs
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
row = transfer_entry_1.items[0]
# Add new row without reference of the job card item
transfer_entry_1.append(
"items",
{
"item_code": row.item_code,
"item_name": row.item_name,
"item_group": row.item_group,
"qty": row.qty,
"uom": row.uom,
"conversion_factor": row.conversion_factor,
"stock_uom": row.stock_uom,
"basic_rate": row.basic_rate,
"basic_amount": row.basic_amount,
"expense_account": row.expense_account,
"cost_center": row.cost_center,
"s_warehouse": row.s_warehouse,
"t_warehouse": row.t_warehouse,
},
)
self.assertRaises(frappe.ValidationError, transfer_entry_1.insert)
def test_job_card_partial_material_transfer(self): def test_job_card_partial_material_transfer(self):
"Test partial material transfer against Job Card" "Test partial material transfer against Job Card"
self.transfer_material_against = "Job Card" self.transfer_material_against = "Job Card"

View File

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

View File

@ -336,10 +336,6 @@ frappe.ui.form.on('Production Plan', {
}, },
get_items_for_material_requests(frm, warehouses) { 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({ frappe.call({
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests", method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests",
freeze: true, freeze: true,
@ -352,11 +348,11 @@ frappe.ui.form.on('Production Plan', {
frm.set_value('mr_items', []); frm.set_value('mr_items', []);
r.message.forEach(row => { r.message.forEach(row => {
let d = frm.add_child('mr_items'); let d = frm.add_child('mr_items');
set_fields.forEach(field => { for (let field in row) {
if (row[field]) { if (field !== 'name') {
d[field] = row[field]; d[field] = row[field];
} }
}); }
}); });
} }
refresh_field('mr_items'); 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.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.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.get_item_details import get_conversion_factor 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 from erpnext.utilities.transaction_base import validate_uom_is_integer
@ -398,9 +399,20 @@ class ProductionPlan(Document):
self.set_status() self.set_status()
self.db_set("status", self.status) self.db_set("status", self.status)
def on_submit(self):
self.update_bin_qty()
def on_cancel(self): def on_cancel(self):
self.db_set("status", "Cancelled") self.db_set("status", "Cancelled")
self.delete_draft_work_order() 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): def delete_draft_work_order(self):
for d in frappe.get_all( for d in frappe.get_all(
@ -575,6 +587,7 @@ class ProductionPlan(Document):
"production_plan_sub_assembly_item": row.name, "production_plan_sub_assembly_item": row.name,
"bom": row.bom_no, "bom": row.bom_no,
"production_plan": self.name, "production_plan": self.name,
"fg_item_qty": row.qty,
} }
for field in [ for field in [
@ -1068,6 +1081,7 @@ def get_material_request_items(
"item_code": row.item_code, "item_code": row.item_code,
"item_name": row.item_name, "item_name": row.item_name,
"quantity": required_qty / conversion_factor, "quantity": required_qty / conversion_factor,
"conversion_factor": conversion_factor,
"required_bom_qty": total_qty, "required_bom_qty": total_qty,
"stock_uom": row.get("stock_uom"), "stock_uom": row.get("stock_uom"),
"warehouse": warehouse "warehouse": warehouse
@ -1474,3 +1488,34 @@ def set_default_warehouses(row, default_warehouses):
for field in ["wip_warehouse", "fg_warehouse"]: for field in ["wip_warehouse", "fg_warehouse"]:
if not row.get(field): if not row.get(field):
row[field] = default_warehouses.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

@ -307,6 +307,43 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(plan.sub_assembly_items[0].supplier, "_Test Supplier") self.assertEqual(plan.sub_assembly_items[0].supplier, "_Test Supplier")
def test_production_plan_for_subcontracting_po(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
bom_tree_1 = {"Test Laptop 1": {"Test Motherboard 1": {"Test Motherboard Wires 1": {}}}}
create_nested_bom(bom_tree_1, prefix="")
item_doc = frappe.get_doc("Item", "Test Motherboard 1")
company = "_Test Company"
item_doc.is_sub_contracted_item = 1
for row in item_doc.item_defaults:
if row.company == company and not row.default_supplier:
row.default_supplier = "_Test Supplier"
if not item_doc.item_defaults:
item_doc.append("item_defaults", {"company": company, "default_supplier": "_Test Supplier"})
item_doc.save()
plan = create_production_plan(
item_code="Test Laptop 1", planned_qty=10, use_multi_level_bom=1, do_not_submit=True
)
plan.get_sub_assembly_items()
plan.set_default_supplier_for_subcontracting_order()
plan.submit()
self.assertEqual(plan.sub_assembly_items[0].supplier, "_Test Supplier")
plan.make_work_order()
po = frappe.db.get_value("Purchase Order Item", {"production_plan": plan.name}, "parent")
po_doc = frappe.get_doc("Purchase Order", po)
self.assertEqual(po_doc.supplier, "_Test Supplier")
self.assertEqual(po_doc.items[0].qty, 10.0)
self.assertEqual(po_doc.items[0].fg_item_qty, 10.0)
self.assertEqual(po_doc.items[0].fg_item_qty, 10.0)
self.assertEqual(po_doc.items[0].fg_item, "Test Motherboard 1")
def test_production_plan_combine_subassembly(self): def test_production_plan_combine_subassembly(self):
""" """
Test combining Sub assembly items belonging to the same BOM in Prod Plan. Test combining Sub assembly items belonging to the same BOM in Prod Plan.
@ -868,6 +905,27 @@ class TestProductionPlan(FrappeTestCase):
for item_code in mr_items: for item_code in mr_items:
self.assertTrue(item_code in validate_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): def create_production_plan(**args):
""" """

View File

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

View File

@ -1649,6 +1649,14 @@ class TestWorkOrder(FrappeTestCase):
job_card2 = frappe.copy_doc(job_card_doc) job_card2 = frappe.copy_doc(job_card_doc)
self.assertRaises(frappe.ValidationError, job_card2.save) 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(): def prepare_data_for_workstation_type_check():
from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.manufacturing.doctype.operation.test_operation import make_operation

View File

@ -558,12 +558,19 @@ class WorkOrder(Document):
and self.production_plan_item and self.production_plan_item
and not self.production_plan_sub_assembly_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: query = (
qty += self.qty frappe.qb.from_(table)
elif self.docstatus == 2: .select(Sum(table.qty))
qty -= self.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) 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 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""" """Get total reserved quantity for any item in specified warehouse"""
wo = frappe.qb.DocType("Work Order") wo = frappe.qb.DocType("Work Order")
wo_item = frappe.qb.DocType("Work Order Item") wo_item = frappe.qb.DocType("Work Order Item")
return ( query = (
frappe.qb.from_(wo) frappe.qb.from_(wo)
.from_(wo_item) .from_(wo_item)
.select( .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) | (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() @frappe.whitelist()

View File

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

View File

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

View File

@ -7,4 +7,6 @@ def execute():
frappe.reload_doc("manufacturing", "doctype", "work_order") frappe.reload_doc("manufacturing", "doctype", "work_order")
frappe.reload_doc("manufacturing", "doctype", "work_order_item") frappe.reload_doc("manufacturing", "doctype", "work_order_item")
frappe.db.sql("""UPDATE `tabWork Order Item` SET amount = rate * required_qty""") frappe.db.sql(
"""UPDATE `tabWork Order Item` SET amount = ifnull(rate, 0.0) * ifnull(required_qty, 0.0)"""
)

View File

@ -11,6 +11,8 @@ from erpnext.accounts.utils import get_fiscal_year
def execute(): def execute():
frappe.db.truncate("Account Closing Balance")
company_wise_order = {} company_wise_order = {}
get_opening_entries = True get_opening_entries = True
for pcv in frappe.db.get_all( for pcv in frappe.db.get_all(
@ -26,8 +28,29 @@ def execute():
pcv_doc.year_start_date = get_fiscal_year( pcv_doc.year_start_date = get_fiscal_year(
pcv.posting_date, pcv.fiscal_year, company=pcv.company pcv.posting_date, pcv.fiscal_year, company=pcv.company
)[1] )[1]
gl_entries = pcv_doc.get_gl_entries()
closing_entries = pcv_doc.get_grouped_gl_entries(get_opening_entries=get_opening_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 = frappe.db.get_all(
"GL Entry",
filters={
"is_cancelled": 0,
"voucher_no": ["!=", pcv.name],
"posting_date": ["<=", pcv.posting_date],
},
fields=["*"],
)
for entry in closing_entries:
entry["closing_date"] = pcv_doc.posting_date
entry["period_closing_voucher"] = pcv_doc.name
make_closing_entries(gl_entries + closing_entries, voucher_name=pcv.name) make_closing_entries(gl_entries + closing_entries, voucher_name=pcv.name)
company_wise_order[pcv.company].append(pcv.posting_date) company_wise_order[pcv.company].append(pcv.posting_date)
get_opening_entries = False get_opening_entries = False

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

@ -374,6 +374,7 @@ def make_sales_invoice(source_name, item_code=None, customer=None, currency=None
billing_rate = billing_amount / hours billing_rate = billing_amount / hours
target.company = timesheet.company target.company = timesheet.company
target.project = timesheet.parent_project
if customer: if customer:
target.customer = customer target.customer = customer

View File

@ -91,6 +91,12 @@ frappe.ui.form.on("Sales Invoice", {
}); });
frappe.ui.form.on('Purchase Invoice', { frappe.ui.form.on('Purchase Invoice', {
setup: (frm) => {
frm.make_methods = {
'Landed Cost Voucher': function () { frm.trigger('create_landedcost_voucher') },
}
},
mode_of_payment: function(frm) { mode_of_payment: function(frm) {
get_payment_mode_account(frm, frm.doc.mode_of_payment, function(account){ get_payment_mode_account(frm, frm.doc.mode_of_payment, function(account){
frm.set_value('cash_bank_account', account); frm.set_value('cash_bank_account', account);
@ -99,6 +105,20 @@ frappe.ui.form.on('Purchase Invoice', {
payment_terms_template: function() { payment_terms_template: function() {
cur_frm.trigger("disable_due_date"); cur_frm.trigger("disable_due_date");
},
create_landedcost_voucher: function (frm) {
let lcv = frappe.model.get_new_doc('Landed Cost Voucher');
lcv.company = frm.doc.company;
let lcv_receipt = frappe.model.get_new_doc('Landed Cost Purchase Invoice');
lcv_receipt.receipt_document_type = 'Purchase Invoice';
lcv_receipt.receipt_document = frm.doc.name;
lcv_receipt.supplier = frm.doc.supplier;
lcv_receipt.grand_total = frm.doc.grand_total;
lcv.purchase_receipts = [lcv_receipt];
frappe.set_route("Form", lcv.doctype, lcv.name);
} }
}); });

View File

@ -92,7 +92,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
_calculate_taxes_and_totals() { _calculate_taxes_and_totals() {
const is_quotation = this.frm.doc.doctype == "Quotation"; const is_quotation = this.frm.doc.doctype == "Quotation";
this.frm.doc._items = is_quotation ? this.filtered_items() : this.frm.doc.items; this.frm._items = is_quotation ? this.filtered_items() : this.frm.doc.items;
this.validate_conversion_rate(); this.validate_conversion_rate();
this.calculate_item_values(); this.calculate_item_values();
@ -125,7 +125,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
calculate_item_values() { calculate_item_values() {
var me = this; var me = this;
if (!this.discount_amount_applied) { if (!this.discount_amount_applied) {
for (const item of this.frm.doc._items || []) { for (const item of this.frm._items || []) {
frappe.model.round_floats_in(item); frappe.model.round_floats_in(item);
item.net_rate = item.rate; item.net_rate = item.rate;
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty; item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
@ -209,7 +209,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}); });
if(has_inclusive_tax==false) return; if(has_inclusive_tax==false) return;
$.each(me.frm.doc._items || [], function(n, item) { $.each(me.frm._items || [], function(n, item) {
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
var cumulated_tax_fraction = 0.0; var cumulated_tax_fraction = 0.0;
var total_inclusive_tax_amount_per_qty = 0; var total_inclusive_tax_amount_per_qty = 0;
@ -280,13 +280,13 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
var me = this; var me = this;
this.frm.doc.total_qty = this.frm.doc.total = this.frm.doc.base_total = this.frm.doc.net_total = this.frm.doc.base_net_total = 0.0; this.frm.doc.total_qty = this.frm.doc.total = this.frm.doc.base_total = this.frm.doc.net_total = this.frm.doc.base_net_total = 0.0;
$.each(this.frm.doc._items || [], function(i, item) { $.each(this.frm._items || [], function(i, item) {
me.frm.doc.total += item.amount; me.frm.doc.total += item.amount;
me.frm.doc.total_qty += item.qty; me.frm.doc.total_qty += item.qty;
me.frm.doc.base_total += item.base_amount; me.frm.doc.base_total += item.base_amount;
me.frm.doc.net_total += item.net_amount; me.frm.doc.net_total += item.net_amount;
me.frm.doc.base_net_total += item.base_net_amount; me.frm.doc.base_net_total += item.base_net_amount;
}); });
} }
calculate_shipping_charges() { calculate_shipping_charges() {
@ -333,7 +333,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
} }
}); });
$.each(this.frm.doc._items || [], function(n, item) { $.each(this.frm._items || [], function(n, item) {
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
$.each(me.frm.doc["taxes"] || [], function(i, tax) { $.each(me.frm.doc["taxes"] || [], function(i, tax) {
// tax_amount represents the amount of tax for the current step // tax_amount represents the amount of tax for the current step
@ -342,7 +342,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
// Adjust divisional loss to the last item // Adjust divisional loss to the last item
if (tax.charge_type == "Actual") { if (tax.charge_type == "Actual") {
actual_tax_dict[tax.idx] -= current_tax_amount; actual_tax_dict[tax.idx] -= current_tax_amount;
if (n == me.frm.doc._items.length - 1) { if (n == me.frm._items.length - 1) {
current_tax_amount += actual_tax_dict[tax.idx]; current_tax_amount += actual_tax_dict[tax.idx];
} }
} }
@ -379,7 +379,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
} }
// set precision in the last item iteration // set precision in the last item iteration
if (n == me.frm.doc._items.length - 1) { if (n == me.frm._items.length - 1) {
me.round_off_totals(tax); me.round_off_totals(tax);
me.set_in_company_currency(tax, me.set_in_company_currency(tax,
["tax_amount", "tax_amount_after_discount_amount"]); ["tax_amount", "tax_amount_after_discount_amount"]);
@ -602,7 +602,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
_cleanup() { _cleanup() {
this.frm.doc.base_in_words = this.frm.doc.in_words = ""; this.frm.doc.base_in_words = this.frm.doc.in_words = "";
let items = this.frm.doc._items; let items = this.frm._items;
if(items && items.length) { if(items && items.length) {
if(!frappe.meta.get_docfield(items[0].doctype, "item_tax_amount", this.frm.doctype)) { if(!frappe.meta.get_docfield(items[0].doctype, "item_tax_amount", this.frm.doctype)) {
@ -659,7 +659,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
var net_total = 0; var net_total = 0;
// calculate item amount after Discount Amount // calculate item amount after Discount Amount
if (total_for_discount_amount) { if (total_for_discount_amount) {
$.each(this.frm.doc._items || [], function(i, item) { $.each(this.frm._items || [], function(i, item) {
distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount; distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
item.net_amount = flt(item.net_amount - distributed_amount, item.net_amount = flt(item.net_amount - distributed_amount,
precision("base_amount", item)); precision("base_amount", item));
@ -667,7 +667,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
// discount amount rounding loss adjustment if no taxes // discount amount rounding loss adjustment if no taxes
if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total")) if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total"))
&& i == (me.frm.doc._items || []).length - 1) { && i == (me.frm._items || []).length - 1) {
var discount_amount_loss = flt(me.frm.doc.net_total - net_total var discount_amount_loss = flt(me.frm.doc.net_total - net_total
- me.frm.doc.discount_amount, precision("net_total")); - me.frm.doc.discount_amount, precision("net_total"));
item.net_amount = flt(item.net_amount + discount_amount_loss, item.net_amount = flt(item.net_amount + discount_amount_loss,

View File

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

View File

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

View File

@ -617,11 +617,15 @@ def get_credit_limit(customer, company):
if not credit_limit: if not credit_limit:
customer_group = frappe.get_cached_value("Customer", customer, "customer_group") customer_group = frappe.get_cached_value("Customer", customer, "customer_group")
credit_limit = frappe.db.get_value(
result = frappe.db.get_values(
"Customer Credit Limit", "Customer Credit Limit",
{"parent": customer_group, "parenttype": "Customer Group", "company": company}, {"parent": customer_group, "parenttype": "Customer Group", "company": company},
"credit_limit", fieldname=["credit_limit", "bypass_credit_limit_check"],
as_dict=True,
) )
if result and not result[0].bypass_credit_limit_check:
credit_limit = result[0].credit_limit
if not credit_limit: if not credit_limit:
credit_limit = frappe.get_cached_value("Company", company, "credit_limit") credit_limit = frappe.get_cached_value("Company", company, "credit_limit")

View File

@ -286,6 +286,18 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
target.commission_rate = frappe.get_value( target.commission_rate = frappe.get_value(
"Sales Partner", source.referral_sales_partner, "commission_rate" "Sales Partner", source.referral_sales_partner, "commission_rate"
) )
# sales team
for d in customer.get("sales_team", []):
target.append(
"sales_team",
{
"sales_person": d.sales_person,
"allocated_percentage": d.allocated_percentage or None,
"commission_rate": d.commission_rate,
},
)
target.flags.ignore_permissions = ignore_permissions target.flags.ignore_permissions = ignore_permissions
target.run_method("set_missing_values") target.run_method("set_missing_values")
target.run_method("calculate_taxes_and_totals") target.run_method("calculate_taxes_and_totals")

View File

@ -264,7 +264,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
} }
} }
// payment request // payment request
if(flt(doc.per_billed)<100) { if(flt(doc.per_billed, precision('per_billed', doc)) < 100 + frappe.boot.sysdefaults.over_billing_allowance) {
this.frm.add_custom_button(__('Payment Request'), () => this.make_payment_request(), __('Create')); this.frm.add_custom_button(__('Payment Request'), () => this.make_payment_request(), __('Create'));
this.frm.add_custom_button(__('Payment'), () => this.make_payment_entry(), __('Create')); this.frm.add_custom_button(__('Payment'), () => this.make_payment_entry(), __('Create'));
} }

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 is for packed items, because packed items don't have stock_qty field
qty = source.get("qty") qty = source.get("qty")
target.project = source_parent.project 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) target.stock_qty = flt(target.qty) * flt(target.conversion_factor)
args = target.as_dict().copy() args = target.as_dict().copy()
@ -581,7 +581,7 @@ def make_material_request(source_name, target_doc=None):
"doctype": "Material Request Item", "doctype": "Material Request Item",
"field_map": {"name": "sales_order_item", "parent": "sales_order"}, "field_map": {"name": "sales_order_item", "parent": "sales_order"},
"condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code) "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, "postprocess": update_item,
}, },
}, },
@ -620,6 +620,8 @@ def make_project(source_name, target_doc=None):
@frappe.whitelist() @frappe.whitelist()
def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
def set_missing_values(source, target): def set_missing_values(source, target):
target.run_method("set_missing_values") target.run_method("set_missing_values")
target.run_method("set_po_nos") target.run_method("set_po_nos")
@ -634,6 +636,8 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
if target.company_address: if target.company_address:
target.update(get_fetch_values("Delivery Note", "company_address", target.company_address)) target.update(get_fetch_values("Delivery Note", "company_address", target.company_address))
make_packing_list(target)
def update_item(source, target, source_parent): def update_item(source, target, source_parent):
target.base_amount = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.base_rate) target.base_amount = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.base_rate)
target.amount = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.rate) target.amount = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.rate)
@ -1340,8 +1344,9 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
.select(Sum(wo.qty)) .select(Sum(wo.qty))
.where( .where(
(wo.production_item == i.item_code) (wo.production_item == i.item_code)
& (wo.sales_order == so.name) * (wo.sales_order_item == i.name) & (wo.sales_order == so.name)
& (wo.docstatus.lte(2)) & (wo.sales_order_item == i.name)
& (wo.docstatus.lt(2))
) )
.run()[0][0] .run()[0][0]
) )

View File

@ -57,7 +57,7 @@ frappe.listview_settings['Sales Order'] = {
}); });
listview.page.add_action_item(__("Advance Payment"), ()=>{ 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,106 @@ class TestSalesOrder(FrappeTestCase):
self.assertEqual(pe.references[1].reference_name, so.name) self.assertEqual(pe.references[1].reference_name, so.name)
self.assertEqual(pe.references[1].allocated_amount, 300) 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 test_packed_items_for_partial_sales_order(self):
# test Update Items with product bundle
for product_bundle in [
"_Test Product Bundle Item Partial 1",
"_Test Product Bundle Item Partial 2",
]:
if not frappe.db.exists("Item", product_bundle):
bundle_item = make_item(product_bundle, {"is_stock_item": 0})
bundle_item.append(
"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
)
bundle_item.save(ignore_permissions=True)
for product_bundle in ["_Packed Item Partial 1", "_Packed Item Partial 2"]:
if not frappe.db.exists("Item", product_bundle):
make_item(product_bundle, {"is_stock_item": 1, "stock_uom": "Nos"})
make_stock_entry(item=product_bundle, target="_Test Warehouse - _TC", qty=2, rate=10)
make_product_bundle("_Test Product Bundle Item Partial 1", ["_Packed Item Partial 1"], 1)
make_product_bundle("_Test Product Bundle Item Partial 2", ["_Packed Item Partial 2"], 1)
so = make_sales_order(
item_code="_Test Product Bundle Item Partial 1",
warehouse="_Test Warehouse - _TC",
qty=1,
uom="Nos",
stock_uom="Nos",
conversion_factor=1,
transaction_date=nowdate(),
delivery_note=nowdate(),
do_not_submit=1,
)
so.append(
"items",
{
"item_code": "_Test Product Bundle Item Partial 2",
"warehouse": "_Test Warehouse - _TC",
"qty": 1,
"uom": "Nos",
"stock_uom": "Nos",
"conversion_factor": 1,
"delivery_note": nowdate(),
},
)
so.save()
so.submit()
dn = make_delivery_note(so.name)
dn.remove(dn.items[1])
dn.save()
dn.submit()
self.assertEqual(len(dn.items), 1)
self.assertEqual(len(dn.packed_items), 1)
self.assertEqual(dn.items[0].item_code, "_Test Product Bundle Item Partial 1")
so.load_from_db()
dn = make_delivery_note(so.name)
dn.save()
self.assertEqual(len(dn.items), 1)
self.assertEqual(len(dn.packed_items), 1)
self.assertEqual(dn.items[0].item_code, "_Test Product Bundle Item Partial 2")
def automatically_fetch_payment_terms(enable=1): def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings = frappe.get_doc("Accounts Settings")

View File

@ -299,7 +299,8 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
} }
batch_no(doc, cdt, cdn) { batch_no(doc, cdt, cdn) {
var me = this; super.batch_no(doc, cdt, cdn);
var item = frappe.get_doc(cdt, cdn); var item = frappe.get_doc(cdt, cdn);
if (item.serial_no) { if (item.serial_no) {
@ -378,10 +379,6 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
} }
} }
batch_no(doc, cdt, cdn) {
super.batch_no(doc, cdt, cdn);
}
qty(doc, cdt, cdn) { qty(doc, cdt, cdn) {
super.qty(doc, cdt, cdn); super.qty(doc, cdt, cdn);

View File

@ -80,5 +80,30 @@
"chart_of_accounts": "Standard", "chart_of_accounts": "Standard",
"enable_perpetual_inventory": 1, "enable_perpetual_inventory": 1,
"default_holiday_list": "_Test Holiday List" "default_holiday_list": "_Test Holiday List"
},
{
"abbr": "_TC6",
"company_name": "_Test Company 6",
"is_group": 1,
"country": "India",
"default_currency": "INR",
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 0
},
{
"abbr": "_TC7",
"company_name": "_Test Company 7",
"parent_company": "_Test Company 6",
"is_group": 1,
"country": "United States",
"default_currency": "USD",
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 0
} }
] ]

View File

@ -257,7 +257,9 @@ def get_employee_email(employee_doc):
def get_holiday_list_for_employee(employee, raise_exception=True): def get_holiday_list_for_employee(employee, raise_exception=True):
if employee: if employee:
holiday_list, company = frappe.db.get_value("Employee", employee, ["holiday_list", "company"]) holiday_list, company = frappe.get_cached_value(
"Employee", employee, ["holiday_list", "company"]
)
else: else:
holiday_list = "" holiday_list = ""
company = frappe.db.get_single_value("Global Defaults", "default_company") company = frappe.db.get_single_value("Global Defaults", "default_company")

View File

@ -115,6 +115,8 @@ def is_holiday(holiday_list, date=None):
if date is None: if date is None:
date = today() date = today()
if holiday_list: if holiday_list:
return bool(frappe.get_all("Holiday List", dict(name=holiday_list, holiday_date=date))) return bool(
frappe.db.exists("Holiday", {"parent": holiday_list, "holiday_date": date}, cache=True)
)
else: else:
return False return False

View File

@ -22,24 +22,18 @@
"creation": "2021-11-22 12:19:15.888642", "creation": "2021-11-22 12:19:15.888642",
"docstatus": 0, "docstatus": 0,
"doctype": "Module Onboarding", "doctype": "Module Onboarding",
"documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/company-setup", "documentation_url": "https://docs.erpnext.com/docs/v14/user/manual/en/setting-up/company-setup",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"modified": "2022-06-07 14:31:00.575193", "modified": "2023-05-16 13:13:24.043792",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Home", "name": "Home",
"owner": "Administrator", "owner": "Administrator",
"steps": [ "steps": [
{
"step": "Company Set Up"
},
{ {
"step": "Navigation Help" "step": "Navigation Help"
}, },
{
"step": "Data import"
},
{ {
"step": "Create an Item" "step": "Create an Item"
}, },
@ -51,12 +45,9 @@
}, },
{ {
"step": "Create a Quotation" "step": "Create a Quotation"
},
{
"step": "Letterhead"
} }
], ],
"subtitle": "Company, Item, Customer, Supplier, Navigation Help, Data Import, Letter Head, Quotation", "subtitle": "Item, Customer, Supplier, Navigation Help and Quotation",
"success_message": "Masters are all set up!", "success_message": "You're ready to start your journey with ERPNext",
"title": "Let's Set Up Some Masters" "title": "Let's begin your journey with ERPNext"
} }

View File

@ -5,11 +5,11 @@
"description": "# Set Up a Company\n\nA company is a legal entity for which you will set up your books of account and create accounting transactions. In ERPNext, you can create multiple companies, and establish relationships (group/subsidiary) among them.\n\nWithin the company master, you can capture various default accounts for that Company and set crucial settings related to the accounting methodology followed for a company.\n", "description": "# Set Up a Company\n\nA company is a legal entity for which you will set up your books of account and create accounting transactions. In ERPNext, you can create multiple companies, and establish relationships (group/subsidiary) among them.\n\nWithin the company master, you can capture various default accounts for that Company and set crucial settings related to the accounting methodology followed for a company.\n",
"docstatus": 0, "docstatus": 0,
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 1,
"is_complete": 0, "is_complete": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2021-12-15 14:22:18.317423", "modified": "2023-05-15 09:18:42.895537",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Company Set Up", "name": "Company Set Up",
"owner": "Administrator", "owner": "Administrator",

View File

@ -5,17 +5,17 @@
"description": "# Create a Customer\n\nThe Customer master is at the heart of your sales transactions. Customers are linked in Quotations, Sales Orders, Invoices, and Payments. Customers can be either numbered or identified by name (you would typically do this based on the number of customers you have).\n\nThrough Customer\u2019s master, you can effectively track essentials like:\n - Customer\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n", "description": "# Create a Customer\n\nThe Customer master is at the heart of your sales transactions. Customers are linked in Quotations, Sales Orders, Invoices, and Payments. Customers can be either numbered or identified by name (you would typically do this based on the number of customers you have).\n\nThrough Customer\u2019s master, you can effectively track essentials like:\n - Customer\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n",
"docstatus": 0, "docstatus": 0,
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 1,
"is_complete": 0, "is_complete": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2021-12-15 14:20:31.197564", "modified": "2023-05-16 12:54:54.112364",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Create a Customer", "name": "Create a Customer",
"owner": "Administrator", "owner": "Administrator",
"reference_document": "Customer", "reference_document": "Customer",
"show_form_tour": 0, "show_form_tour": 0,
"show_full_form": 0, "show_full_form": 0,
"title": "Manage Customers", "title": "Create a Customer",
"validate_action": 1 "validate_action": 1
} }

View File

@ -5,11 +5,11 @@
"description": "# Create a Quotation\n\nLet\u2019s get started with business transactions by creating your first Quotation. You can create a Quotation for an existing customer or a prospect. It will be an approved document, with items you sell and the proposed price + taxes applied. After completing the instructions, you will get a Quotation in a ready to share print format.", "description": "# Create a Quotation\n\nLet\u2019s get started with business transactions by creating your first Quotation. You can create a Quotation for an existing customer or a prospect. It will be an approved document, with items you sell and the proposed price + taxes applied. After completing the instructions, you will get a Quotation in a ready to share print format.",
"docstatus": 0, "docstatus": 0,
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 1,
"is_complete": 0, "is_complete": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2021-12-15 14:21:31.675330", "modified": "2023-05-15 09:18:42.984170",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Create a Quotation", "name": "Create a Quotation",
"owner": "Administrator", "owner": "Administrator",

View File

@ -5,17 +5,17 @@
"description": "# Create a Supplier\n\nAlso known as Vendor, is a master at the center of your purchase transactions. Suppliers are linked in Request for Quotation, Purchase Orders, Receipts, and Payments. Suppliers can be either numbered or identified by name.\n\nThrough Supplier\u2019s master, you can effectively track essentials like:\n - Supplier\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n", "description": "# Create a Supplier\n\nAlso known as Vendor, is a master at the center of your purchase transactions. Suppliers are linked in Request for Quotation, Purchase Orders, Receipts, and Payments. Suppliers can be either numbered or identified by name.\n\nThrough Supplier\u2019s master, you can effectively track essentials like:\n - Supplier\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n",
"docstatus": 0, "docstatus": 0,
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 1,
"is_complete": 0, "is_complete": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2021-12-15 14:21:23.518301", "modified": "2023-05-16 12:55:08.610113",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Create a Supplier", "name": "Create a Supplier",
"owner": "Administrator", "owner": "Administrator",
"reference_document": "Supplier", "reference_document": "Supplier",
"show_form_tour": 0, "show_form_tour": 0,
"show_full_form": 0, "show_full_form": 0,
"title": "Manage Suppliers", "title": "Create a Supplier",
"validate_action": 1 "validate_action": 1
} }

View File

@ -6,18 +6,18 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"form_tour": "Item General", "form_tour": "Item General",
"idx": 0, "idx": 1,
"intro_video_url": "", "intro_video_url": "",
"is_complete": 0, "is_complete": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2021-12-15 14:19:56.297772", "modified": "2023-05-16 12:56:40.355878",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Create an Item", "name": "Create an Item",
"owner": "Administrator", "owner": "Administrator",
"reference_document": "Item", "reference_document": "Item",
"show_form_tour": 1, "show_form_tour": 1,
"show_full_form": 1, "show_full_form": 0,
"title": "Manage Items", "title": "Create an Item",
"validate_action": 1 "validate_action": 1
} }

View File

@ -5,11 +5,11 @@
"description": "# Import Data from Spreadsheet\n\nIn ERPNext, you can easily migrate your historical data using spreadsheets. You can use it for migrating not just masters (like Customer, Supplier, Items), but also for transactions like (outstanding invoices, opening stock and accounting entries, etc).", "description": "# Import Data from Spreadsheet\n\nIn ERPNext, you can easily migrate your historical data using spreadsheets. You can use it for migrating not just masters (like Customer, Supplier, Items), but also for transactions like (outstanding invoices, opening stock and accounting entries, etc).",
"docstatus": 0, "docstatus": 0,
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 1,
"is_complete": 0, "is_complete": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2022-06-07 14:28:51.390813", "modified": "2023-05-15 09:18:42.962231",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Data import", "name": "Data import",
"owner": "Administrator", "owner": "Administrator",

View File

@ -5,11 +5,11 @@
"description": "# Create a Letter Head\n\nA Letter Head contains your organization's name, logo, address, etc which appears at the header and footer portion in documents. You can learn more about Setting up Letter Head in ERPNext here.\n", "description": "# Create a Letter Head\n\nA Letter Head contains your organization's name, logo, address, etc which appears at the header and footer portion in documents. You can learn more about Setting up Letter Head in ERPNext here.\n",
"docstatus": 0, "docstatus": 0,
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 1,
"is_complete": 0, "is_complete": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2021-12-15 14:21:39.037742", "modified": "2023-05-15 09:18:42.995184",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Letterhead", "name": "Letterhead",
"owner": "Administrator", "owner": "Administrator",

View File

@ -2,14 +2,14 @@
"action": "Watch Video", "action": "Watch Video",
"action_label": "Learn about Navigation options", "action_label": "Learn about Navigation options",
"creation": "2021-11-22 12:09:52.233872", "creation": "2021-11-22 12:09:52.233872",
"description": "# Navigation in ERPNext\n\nEase of navigating and browsing around the ERPNext is one of our core strengths. In the following video, you will learn how to reach a specific feature in ERPNext via module page or awesome bar\u2019s shortcut.\n", "description": "# Navigation in ERPNext\n\nEase of navigating and browsing around the ERPNext is one of our core strengths. In the following video, you will learn how to reach a specific feature in ERPNext via module page or AwesomeBar.",
"docstatus": 0, "docstatus": 0,
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 1,
"is_complete": 0, "is_complete": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2022-06-07 14:28:00.901082", "modified": "2023-05-16 12:53:25.939908",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Navigation Help", "name": "Navigation Help",
"owner": "Administrator", "owner": "Administrator",

View File

@ -21,6 +21,10 @@ def boot_session(bootinfo):
bootinfo.sysdefaults.allow_stale = cint( bootinfo.sysdefaults.allow_stale = cint(
frappe.db.get_single_value("Accounts Settings", "allow_stale") frappe.db.get_single_value("Accounts Settings", "allow_stale")
) )
bootinfo.sysdefaults.over_billing_allowance = frappe.db.get_single_value(
"Accounts Settings", "over_billing_allowance"
)
bootinfo.sysdefaults.quotation_valid_till = cint( bootinfo.sysdefaults.quotation_valid_till = cint(
frappe.db.get_single_value("CRM Settings", "default_valid_till") frappe.db.get_single_value("CRM Settings", "default_valid_till")
) )

View File

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

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