Merge branch 'frappe:develop' into duplicates-in-tax-category-map
This commit is contained in:
commit
943dcb6dea
@ -7,8 +7,7 @@
|
||||
<p>ERP made simple</p>
|
||||
</p>
|
||||
|
||||
[](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml)
|
||||
[](https://github.com/erpnext/erpnext_ui_tests/actions/workflows/ui-tests.yml)
|
||||
[](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml)
|
||||
[](https://www.codetriage.com/frappe/erpnext)
|
||||
[](https://codecov.io/gh/frappe/erpnext)
|
||||
[](https://hub.docker.com/r/frappe/erpnext-worker)
|
||||
|
@ -9,6 +9,7 @@ from frappe.contacts.address_and_contact import (
|
||||
load_address_and_contact,
|
||||
)
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import comma_and, get_link_to_form
|
||||
|
||||
|
||||
class BankAccount(Document):
|
||||
@ -52,6 +53,17 @@ class BankAccount(Document):
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
self.validate_iban()
|
||||
self.validate_account()
|
||||
|
||||
def validate_account(self):
|
||||
if self.account:
|
||||
if accounts := frappe.db.get_all("Bank Account", filters={"account": self.account}, as_list=1):
|
||||
frappe.throw(
|
||||
_("'{0}' account is already used by {1}. Use another account.").format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
|
||||
)
|
||||
)
|
||||
|
||||
def validate_company(self):
|
||||
if self.is_company_account and not self.company:
|
||||
|
@ -5,7 +5,9 @@
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, getdate
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
|
||||
@ -179,39 +181,62 @@ def get_payment_entries_for_bank_clearance(
|
||||
|
||||
pos_sales_invoices, pos_purchase_invoices = [], []
|
||||
if include_pos_transactions:
|
||||
pos_sales_invoices = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
|
||||
si.posting_date, si.customer as against_account, sip.clearance_date,
|
||||
account.account_currency, 0 as credit
|
||||
from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
|
||||
where
|
||||
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
|
||||
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
|
||||
order by
|
||||
si.posting_date ASC, si.name DESC
|
||||
""",
|
||||
{"account": account, "from": from_date, "to": to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
si_payment = frappe.qb.DocType("Sales Invoice Payment")
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
acc = frappe.qb.DocType("Account")
|
||||
|
||||
pos_purchase_invoices = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
|
||||
pi.posting_date, pi.supplier as against_account, pi.clearance_date,
|
||||
account.account_currency, 0 as debit
|
||||
from `tabPurchase Invoice` pi, `tabAccount` account
|
||||
where
|
||||
pi.cash_bank_account=%(account)s and pi.docstatus=1 and account.name = pi.cash_bank_account
|
||||
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
|
||||
order by
|
||||
pi.posting_date ASC, pi.name DESC
|
||||
""",
|
||||
{"account": account, "from": from_date, "to": to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
pos_sales_invoices = (
|
||||
frappe.qb.from_(si_payment)
|
||||
.inner_join(si)
|
||||
.on(si_payment.parent == si.name)
|
||||
.inner_join(acc)
|
||||
.on(si_payment.account == acc.name)
|
||||
.select(
|
||||
ConstantColumn("Sales Invoice").as_("payment_document"),
|
||||
si.name.as_("payment_entry"),
|
||||
si_payment.reference_no.as_("cheque_number"),
|
||||
si_payment.amount.as_("debit"),
|
||||
si.posting_date,
|
||||
si.customer.as_("against_account"),
|
||||
si_payment.clearance_date,
|
||||
acc.account_currency,
|
||||
ConstantColumn(0).as_("credit"),
|
||||
)
|
||||
.where(
|
||||
(si.docstatus == 1)
|
||||
& (si_payment.account == account)
|
||||
& (si.posting_date >= from_date)
|
||||
& (si.posting_date <= to_date)
|
||||
)
|
||||
.orderby(si.posting_date)
|
||||
.orderby(si.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
|
||||
pos_purchase_invoices = (
|
||||
frappe.qb.from_(pi)
|
||||
.inner_join(acc)
|
||||
.on(pi.cash_bank_account == acc.name)
|
||||
.select(
|
||||
ConstantColumn("Purchase Invoice").as_("payment_document"),
|
||||
pi.name.as_("payment_entry"),
|
||||
pi.paid_amount.as_("credit"),
|
||||
pi.posting_date,
|
||||
pi.supplier.as_("against_account"),
|
||||
pi.clearance_date,
|
||||
acc.account_currency,
|
||||
ConstantColumn(0).as_("debit"),
|
||||
)
|
||||
.where(
|
||||
(pi.docstatus == 1)
|
||||
& (pi.cash_bank_account == account)
|
||||
& (pi.posting_date >= from_date)
|
||||
& (pi.posting_date <= to_date)
|
||||
)
|
||||
.orderby(pi.posting_date)
|
||||
.orderby(pi.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
entries = (
|
||||
list(payment_entries)
|
||||
|
@ -80,7 +80,8 @@ class BankStatementImport(DataImport):
|
||||
from frappe.utils.background_jobs import is_job_enqueued
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
if is_scheduler_inactive() and not frappe.flags.in_test:
|
||||
run_now = frappe.flags.in_test or frappe.conf.developer_mode
|
||||
if is_scheduler_inactive() and not run_now:
|
||||
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
|
||||
|
||||
job_id = f"bank_statement_import::{self.name}"
|
||||
@ -97,7 +98,7 @@ class BankStatementImport(DataImport):
|
||||
google_sheets_url=self.google_sheets_url,
|
||||
bank=self.bank,
|
||||
template_options=self.template_options,
|
||||
now=frappe.conf.developer_mode or frappe.flags.in_test,
|
||||
now=run_now,
|
||||
)
|
||||
return True
|
||||
|
||||
|
@ -32,8 +32,16 @@ class TestBankTransaction(FrappeTestCase):
|
||||
frappe.db.delete(dt)
|
||||
clear_loan_transactions()
|
||||
make_pos_profile()
|
||||
add_transactions()
|
||||
add_vouchers()
|
||||
|
||||
# generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
|
||||
uniq_identifier = frappe.generate_hash(length=10)
|
||||
gl_account = create_gl_account("_Test Bank " + uniq_identifier)
|
||||
bank_account = create_bank_account(
|
||||
gl_account=gl_account, bank_account_name="Checking Account " + uniq_identifier
|
||||
)
|
||||
|
||||
add_transactions(bank_account=bank_account)
|
||||
add_vouchers(gl_account=gl_account)
|
||||
|
||||
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
|
||||
def test_linked_payments(self):
|
||||
@ -219,7 +227,9 @@ def clear_loan_transactions():
|
||||
frappe.db.delete("Loan Repayment")
|
||||
|
||||
|
||||
def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
|
||||
def create_bank_account(
|
||||
bank_name="Citi Bank", gl_account="_Test Bank - _TC", bank_account_name="Checking Account"
|
||||
):
|
||||
try:
|
||||
frappe.get_doc(
|
||||
{
|
||||
@ -231,21 +241,35 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
|
||||
pass
|
||||
|
||||
try:
|
||||
frappe.get_doc(
|
||||
bank_account = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Account",
|
||||
"account_name": "Checking Account",
|
||||
"account_name": bank_account_name,
|
||||
"bank": bank_name,
|
||||
"account": account_name,
|
||||
"account": gl_account,
|
||||
}
|
||||
).insert(ignore_if_duplicate=True)
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
return bank_account.name
|
||||
|
||||
def add_transactions():
|
||||
create_bank_account()
|
||||
|
||||
def create_gl_account(gl_account_name="_Test Bank - _TC"):
|
||||
gl_account = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"company": "_Test Company",
|
||||
"parent_account": "Current Assets - _TC",
|
||||
"account_type": "Bank",
|
||||
"is_group": 0,
|
||||
"account_name": gl_account_name,
|
||||
}
|
||||
).insert()
|
||||
return gl_account.name
|
||||
|
||||
|
||||
def add_transactions(bank_account="_Test Bank - _TC"):
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Transaction",
|
||||
@ -253,7 +277,7 @@ def add_transactions():
|
||||
"date": "2018-10-23",
|
||||
"deposit": 1200,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
@ -265,7 +289,7 @@ def add_transactions():
|
||||
"date": "2018-10-23",
|
||||
"deposit": 1700,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
@ -277,7 +301,7 @@ def add_transactions():
|
||||
"date": "2018-10-26",
|
||||
"withdrawal": 690,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
@ -289,7 +313,7 @@ def add_transactions():
|
||||
"date": "2018-10-27",
|
||||
"deposit": 3900,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
@ -301,13 +325,13 @@ def add_transactions():
|
||||
"date": "2018-10-27",
|
||||
"withdrawal": 109080,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
|
||||
|
||||
def add_vouchers():
|
||||
def add_vouchers(gl_account="_Test Bank - _TC"):
|
||||
try:
|
||||
frappe.get_doc(
|
||||
{
|
||||
@ -323,7 +347,7 @@ def add_vouchers():
|
||||
|
||||
pi = make_purchase_invoice(supplier="Conrad Electronic", qty=1, rate=690)
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
|
||||
pe.reference_no = "Conrad Oct 18"
|
||||
pe.reference_date = "2018-10-24"
|
||||
pe.insert()
|
||||
@ -342,14 +366,14 @@ def add_vouchers():
|
||||
pass
|
||||
|
||||
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1200)
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
|
||||
pe.reference_no = "Herr G Oct 18"
|
||||
pe.reference_date = "2018-10-24"
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1700)
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
|
||||
pe.reference_no = "Herr G Nov 18"
|
||||
pe.reference_date = "2018-11-01"
|
||||
pe.insert()
|
||||
@ -380,10 +404,10 @@ def add_vouchers():
|
||||
pass
|
||||
|
||||
pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1)
|
||||
pi.cash_bank_account = "_Test Bank - _TC"
|
||||
pi.cash_bank_account = gl_account
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
|
||||
pe.reference_no = "Poore Simon's Oct 18"
|
||||
pe.reference_date = "2018-10-28"
|
||||
pe.paid_amount = 690
|
||||
@ -392,7 +416,7 @@ def add_vouchers():
|
||||
pe.submit()
|
||||
|
||||
si = create_sales_invoice(customer="Poore Simon's", qty=1, rate=3900)
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account=gl_account)
|
||||
pe.reference_no = "Poore Simon's Oct 18"
|
||||
pe.reference_date = "2018-10-28"
|
||||
pe.insert()
|
||||
@ -415,16 +439,12 @@ def add_vouchers():
|
||||
if not frappe.db.get_value(
|
||||
"Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}
|
||||
):
|
||||
mode_of_payment.append(
|
||||
"accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"}
|
||||
)
|
||||
mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account})
|
||||
mode_of_payment.save()
|
||||
|
||||
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
|
||||
si.is_pos = 1
|
||||
si.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080}
|
||||
)
|
||||
si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080})
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
|
@ -13,16 +13,9 @@ import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
|
||||
get_dimension_filter_map,
|
||||
)
|
||||
from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.exceptions import (
|
||||
InvalidAccountCurrency,
|
||||
InvalidAccountDimensionError,
|
||||
MandatoryAccountDimensionError,
|
||||
)
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
|
||||
exclude_from_linked_with = True
|
||||
|
||||
@ -98,7 +91,6 @@ class GLEntry(Document):
|
||||
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
|
||||
self.validate_account_details(adv_adj)
|
||||
self.validate_dimensions_for_pl_and_bs()
|
||||
self.validate_allowed_dimensions()
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
|
||||
@ -208,42 +200,6 @@ class GLEntry(Document):
|
||||
)
|
||||
)
|
||||
|
||||
def validate_allowed_dimensions(self):
|
||||
dimension_filter_map = get_dimension_filter_map()
|
||||
for key, value in dimension_filter_map.items():
|
||||
dimension = key[0]
|
||||
account = key[1]
|
||||
|
||||
if self.account == account:
|
||||
if value["is_mandatory"] and not self.get(dimension):
|
||||
frappe.throw(
|
||||
_("{0} is mandatory for account {1}").format(
|
||||
frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)
|
||||
),
|
||||
MandatoryAccountDimensionError,
|
||||
)
|
||||
|
||||
if value["allow_or_restrict"] == "Allow":
|
||||
if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(self.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(self.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
else:
|
||||
if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(self.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(self.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
|
||||
def check_pl_account(self):
|
||||
if (
|
||||
self.is_opening == "Yes"
|
||||
|
@ -1169,7 +1169,9 @@ class JournalEntry(AccountsController):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
|
||||
def get_default_bank_cash_account(
|
||||
company, account_type=None, mode_of_payment=None, account=None, ignore_permissions=False
|
||||
):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
|
||||
|
||||
if mode_of_payment:
|
||||
@ -1207,7 +1209,7 @@ def get_default_bank_cash_account(company, account_type=None, mode_of_payment=No
|
||||
return frappe._dict(
|
||||
{
|
||||
"account": account,
|
||||
"balance": get_balance_on(account),
|
||||
"balance": get_balance_on(account, ignore_account_permission=ignore_permissions),
|
||||
"account_currency": account_details.account_currency,
|
||||
"account_type": account_details.account_type,
|
||||
}
|
||||
|
@ -2220,6 +2220,7 @@ def get_payment_entry(
|
||||
party_type=None,
|
||||
payment_type=None,
|
||||
reference_date=None,
|
||||
ignore_permissions=False,
|
||||
):
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
@ -2242,14 +2243,14 @@ def get_payment_entry(
|
||||
)
|
||||
|
||||
# bank or cash
|
||||
bank = get_bank_cash_account(doc, bank_account)
|
||||
bank = get_bank_cash_account(doc, bank_account, ignore_permissions=ignore_permissions)
|
||||
|
||||
# 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)
|
||||
bank = get_bank_cash_account(doc, account, ignore_permissions=ignore_permissions)
|
||||
|
||||
paid_amount, received_amount = set_paid_amount_and_received_amount(
|
||||
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
|
||||
@ -2389,9 +2390,13 @@ def update_accounting_dimensions(pe, doc):
|
||||
pe.set(dimension, doc.get(dimension))
|
||||
|
||||
|
||||
def get_bank_cash_account(doc, bank_account):
|
||||
def get_bank_cash_account(doc, bank_account, ignore_permissions=False):
|
||||
bank = get_default_bank_cash_account(
|
||||
doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account
|
||||
doc.company,
|
||||
"Bank",
|
||||
mode_of_payment=doc.get("mode_of_payment"),
|
||||
account=bank_account,
|
||||
ignore_permissions=ignore_permissions,
|
||||
)
|
||||
|
||||
if not bank:
|
||||
|
@ -4,9 +4,13 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import getdate
|
||||
|
||||
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
|
||||
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import (
|
||||
create_bank_account,
|
||||
create_gl_account,
|
||||
)
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
get_payment_entry,
|
||||
make_payment_order,
|
||||
@ -14,28 +18,32 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
|
||||
|
||||
class TestPaymentOrder(unittest.TestCase):
|
||||
class TestPaymentOrder(FrappeTestCase):
|
||||
def setUp(self):
|
||||
create_bank_account()
|
||||
# generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
|
||||
uniq_identifier = frappe.generate_hash(length=10)
|
||||
self.gl_account = create_gl_account("_Test Bank " + uniq_identifier)
|
||||
self.bank_account = create_bank_account(
|
||||
gl_account=self.gl_account, bank_account_name="Checking Account " + uniq_identifier
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
for bt in frappe.get_all("Payment Order"):
|
||||
doc = frappe.get_doc("Payment Order", bt.name)
|
||||
doc.cancel()
|
||||
doc.delete()
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_payment_order_creation_against_payment_entry(self):
|
||||
purchase_invoice = make_purchase_invoice()
|
||||
payment_entry = get_payment_entry(
|
||||
"Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC"
|
||||
"Purchase Invoice", purchase_invoice.name, bank_account=self.gl_account
|
||||
)
|
||||
payment_entry.reference_no = "_Test_Payment_Order"
|
||||
payment_entry.reference_date = getdate()
|
||||
payment_entry.party_bank_account = "Checking Account - Citi Bank"
|
||||
payment_entry.party_bank_account = self.bank_account
|
||||
payment_entry.insert()
|
||||
payment_entry.submit()
|
||||
|
||||
doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry")
|
||||
doc = create_payment_order_against_payment_entry(
|
||||
payment_entry, "Payment Entry", self.bank_account
|
||||
)
|
||||
reference_doc = doc.get("references")[0]
|
||||
self.assertEqual(reference_doc.reference_name, payment_entry.name)
|
||||
self.assertEqual(reference_doc.reference_doctype, "Payment Entry")
|
||||
@ -43,13 +51,13 @@ class TestPaymentOrder(unittest.TestCase):
|
||||
self.assertEqual(reference_doc.amount, 250)
|
||||
|
||||
|
||||
def create_payment_order_against_payment_entry(ref_doc, order_type):
|
||||
def create_payment_order_against_payment_entry(ref_doc, order_type, bank_account):
|
||||
payment_order = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Payment Order",
|
||||
company="_Test Company",
|
||||
payment_order_type=order_type,
|
||||
company_bank_account="Checking Account - Citi Bank",
|
||||
company_bank_account=bank_account,
|
||||
)
|
||||
)
|
||||
doc = make_payment_order(ref_doc.name, payment_order)
|
||||
|
@ -8,6 +8,7 @@
|
||||
"default",
|
||||
"mode_of_payment",
|
||||
"amount",
|
||||
"reference_no",
|
||||
"column_break_3",
|
||||
"account",
|
||||
"type",
|
||||
@ -75,11 +76,16 @@
|
||||
"hidden": 1,
|
||||
"label": "Default",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference No"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-03 12:45:39.986598",
|
||||
"modified": "2024-01-23 16:20:06.436979",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Payment",
|
||||
@ -87,5 +93,6 @@
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -23,6 +23,7 @@ class SalesInvoicePayment(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
reference_no: DF.Data | None
|
||||
type: DF.ReadOnly | None
|
||||
# end: auto-generated types
|
||||
|
||||
|
@ -13,9 +13,13 @@ import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
|
||||
get_dimension_filter_map,
|
||||
)
|
||||
from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.accounts.utils import create_payment_ledger_entry
|
||||
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
|
||||
|
||||
|
||||
def make_gl_entries(
|
||||
@ -355,6 +359,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
||||
|
||||
process_debit_credit_difference(gl_map)
|
||||
|
||||
dimension_filter_map = get_dimension_filter_map()
|
||||
if gl_map:
|
||||
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
|
||||
is_opening = any(d.get("is_opening") == "Yes" for d in gl_map)
|
||||
@ -362,6 +367,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
||||
validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"])
|
||||
|
||||
for entry in gl_map:
|
||||
validate_allowed_dimensions(entry, dimension_filter_map)
|
||||
make_entry(entry, adv_adj, update_outstanding, from_repost)
|
||||
|
||||
|
||||
@ -700,3 +706,39 @@ def set_as_cancel(voucher_type, voucher_no):
|
||||
where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
|
||||
(now(), frappe.session.user, voucher_type, voucher_no),
|
||||
)
|
||||
|
||||
|
||||
def validate_allowed_dimensions(gl_entry, dimension_filter_map):
|
||||
for key, value in dimension_filter_map.items():
|
||||
dimension = key[0]
|
||||
account = key[1]
|
||||
|
||||
if gl_entry.account == account:
|
||||
if value["is_mandatory"] and not gl_entry.get(dimension):
|
||||
frappe.throw(
|
||||
_("{0} is mandatory for account {1}").format(
|
||||
frappe.bold(frappe.unscrub(dimension)), frappe.bold(gl_entry.account)
|
||||
),
|
||||
MandatoryAccountDimensionError,
|
||||
)
|
||||
|
||||
if value["allow_or_restrict"] == "Allow":
|
||||
if gl_entry.get(dimension) and gl_entry.get(dimension) not in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(gl_entry.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(gl_entry.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
else:
|
||||
if gl_entry.get(dimension) and gl_entry.get(dimension) in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(gl_entry.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(gl_entry.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
|
@ -352,9 +352,6 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
|
||||
if filters.get("to_date"):
|
||||
query = query.where(gle.posting_date <= filters.get("to_date"))
|
||||
|
||||
if bank_accounts:
|
||||
query = query.where(gle.against.notin(bank_accounts))
|
||||
|
||||
if filters.get("party"):
|
||||
party = [filters.get("party")]
|
||||
jv_condition = gle.against.isin(party) | (
|
||||
@ -366,7 +363,14 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
|
||||
(gle.voucher_type == "Journal Entry")
|
||||
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
|
||||
)
|
||||
query = query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
|
||||
|
||||
query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
|
||||
if bank_accounts:
|
||||
query = query.where(
|
||||
gle.against.notin(bank_accounts) & (gle.account.isin(tds_accounts) & jv_condition)
|
||||
| gle.party.isin(party)
|
||||
)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
|
@ -5,9 +5,8 @@ import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.doctype.tax_withholding_category.test_tax_withholding_category import (
|
||||
create_tax_withholding_category,
|
||||
@ -17,7 +16,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
|
||||
class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase):
|
||||
class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.clear_old_entries()
|
||||
@ -27,11 +26,15 @@ class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase):
|
||||
def test_tax_withholding_for_customers(self):
|
||||
si = create_sales_invoice(rate=1000)
|
||||
pe = create_tcs_payment_entry()
|
||||
jv = create_tcs_journal_entry()
|
||||
|
||||
filters = frappe._dict(
|
||||
company="_Test Company", party_type="Customer", from_date=today(), to_date=today()
|
||||
)
|
||||
result = execute(filters)[1]
|
||||
expected_values = [
|
||||
# Check for JV totals using back calculation logic
|
||||
[jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0],
|
||||
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
|
||||
[si.name, "TCS", 0.075, 1000, 0.52, 1000.52],
|
||||
]
|
||||
@ -41,12 +44,15 @@ class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase):
|
||||
for i in range(len(result)):
|
||||
voucher = frappe._dict(result[i])
|
||||
voucher_expected_values = expected_values[i]
|
||||
self.assertEqual(voucher.ref_no, voucher_expected_values[0])
|
||||
self.assertEqual(voucher.section_code, voucher_expected_values[1])
|
||||
self.assertEqual(voucher.rate, voucher_expected_values[2])
|
||||
self.assertEqual(voucher.base_total, voucher_expected_values[3])
|
||||
self.assertAlmostEqual(voucher.tax_amount, voucher_expected_values[4])
|
||||
self.assertAlmostEqual(voucher.grand_total, voucher_expected_values[5])
|
||||
voucher_actual_values = (
|
||||
voucher.ref_no,
|
||||
voucher.section_code,
|
||||
voucher.rate,
|
||||
voucher.base_total,
|
||||
voucher.tax_amount,
|
||||
voucher.grand_total,
|
||||
)
|
||||
self.assertSequenceEqual(voucher_actual_values, voucher_expected_values)
|
||||
|
||||
def tearDown(self):
|
||||
self.clear_old_entries()
|
||||
@ -109,3 +115,32 @@ def create_tcs_payment_entry():
|
||||
)
|
||||
payment_entry.submit()
|
||||
return payment_entry
|
||||
|
||||
|
||||
def create_tcs_journal_entry():
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.posting_date = today()
|
||||
jv.company = "_Test Company"
|
||||
jv.set(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"credit_in_account_currency": 10000,
|
||||
},
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"debit_in_account_currency": 9992.5,
|
||||
},
|
||||
{
|
||||
"account": "TCS - _TC",
|
||||
"debit_in_account_currency": 7.5,
|
||||
},
|
||||
],
|
||||
)
|
||||
jv.insert()
|
||||
return jv.submit()
|
||||
|
@ -78,8 +78,14 @@ frappe.query_reports["Trial Balance"] = {
|
||||
"options": erpnext.get_presentation_currency_list()
|
||||
},
|
||||
{
|
||||
"fieldname": "with_period_closing_entry",
|
||||
"label": __("Period Closing Entry"),
|
||||
"fieldname": "with_period_closing_entry_for_opening",
|
||||
"label": __("With Period Closing Entry For Opening Balances"),
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "with_period_closing_entry_for_current_period",
|
||||
"label": __("Period Closing Entry For Current Period"),
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
},
|
||||
|
@ -116,7 +116,7 @@ def get_data(filters):
|
||||
max_rgt,
|
||||
filters,
|
||||
gl_entries_by_account,
|
||||
ignore_closing_entries=not flt(filters.with_period_closing_entry),
|
||||
ignore_closing_entries=not flt(filters.with_period_closing_entry_for_current_period),
|
||||
ignore_opening_entries=True,
|
||||
)
|
||||
|
||||
@ -249,7 +249,7 @@ def get_opening_balance(
|
||||
):
|
||||
opening_balance = opening_balance.where(closing_balance.posting_date >= filters.year_start_date)
|
||||
|
||||
if not flt(filters.with_period_closing_entry):
|
||||
if not flt(filters.with_period_closing_entry_for_opening):
|
||||
if doctype == "Account Closing Balance":
|
||||
opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0)
|
||||
else:
|
||||
|
@ -457,6 +457,7 @@ class PurchaseOrder(BuyingController):
|
||||
self.update_ordered_qty()
|
||||
self.update_reserved_qty_for_subcontract()
|
||||
self.update_subcontracting_order_status()
|
||||
self.update_blanket_order()
|
||||
self.notify_update()
|
||||
clear_doctype_notifications(self)
|
||||
|
||||
@ -644,6 +645,7 @@ class PurchaseOrder(BuyingController):
|
||||
update_sco_status(sco, "Closed" if self.status == "Closed" else None)
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
|
||||
"""get last purchase rate for an item"""
|
||||
|
||||
|
@ -822,6 +822,30 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
# To test if the PO does NOT have a Blanket Order
|
||||
self.assertEqual(po_doc.items[0].blanket_order, None)
|
||||
|
||||
def test_blanket_order_on_po_close_and_open(self):
|
||||
# Step - 1: Create Blanket Order
|
||||
bo = make_blanket_order(blanket_order_type="Purchasing", quantity=10, rate=10)
|
||||
|
||||
# Step - 2: Create Purchase Order
|
||||
po = create_purchase_order(
|
||||
item_code="_Test Item", qty=5, against_blanket_order=1, against_blanket=bo.name
|
||||
)
|
||||
|
||||
bo.load_from_db()
|
||||
self.assertEqual(bo.items[0].ordered_qty, 5)
|
||||
|
||||
# Step - 3: Close Purchase Order
|
||||
po.update_status("Closed")
|
||||
|
||||
bo.load_from_db()
|
||||
self.assertEqual(bo.items[0].ordered_qty, 0)
|
||||
|
||||
# Step - 4: Re-Open Purchase Order
|
||||
po.update_status("Re-open")
|
||||
|
||||
bo.load_from_db()
|
||||
self.assertEqual(bo.items[0].ordered_qty, 5)
|
||||
|
||||
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
|
||||
create_payment_terms_template,
|
||||
@ -1148,6 +1172,7 @@ def create_purchase_order(**args):
|
||||
"schedule_date": add_days(nowdate(), 1),
|
||||
"include_exploded_items": args.get("include_exploded_items", 1),
|
||||
"against_blanket_order": args.against_blanket_order,
|
||||
"against_blanket": args.against_blanket,
|
||||
"material_request": args.material_request,
|
||||
"material_request_item": args.material_request_item,
|
||||
},
|
||||
|
@ -545,7 +545,6 @@
|
||||
"fieldname": "blanket_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Blanket Order",
|
||||
"no_copy": 1,
|
||||
"options": "Blanket Order"
|
||||
},
|
||||
{
|
||||
@ -553,7 +552,6 @@
|
||||
"fieldname": "blanket_order_rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Blanket Order Rate",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@ -917,7 +915,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-24 13:24:41.298416",
|
||||
"modified": "2024-02-05 11:23:24.859435",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
@ -693,7 +693,7 @@ class AccountsController(TransactionBase):
|
||||
if self.get("is_subcontracted"):
|
||||
args["is_subcontracted"] = self.is_subcontracted
|
||||
|
||||
ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False)
|
||||
ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False)
|
||||
|
||||
for fieldname, value in ret.items():
|
||||
if item.meta.get_field(fieldname) and value is not None:
|
||||
|
@ -729,17 +729,24 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
conditions, bin_conditions = [], []
|
||||
filter_dict = get_doctype_wise_filters(filters)
|
||||
|
||||
query = """select `tabWarehouse`.name,
|
||||
warehouse_field = "name"
|
||||
meta = frappe.get_meta("Warehouse")
|
||||
if meta.get("show_title_field_in_link") and meta.get("title_field"):
|
||||
searchfield = meta.get("title_field")
|
||||
warehouse_field = meta.get("title_field")
|
||||
|
||||
query = """select `tabWarehouse`.`{warehouse_field}`,
|
||||
CONCAT_WS(' : ', 'Actual Qty', ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty
|
||||
from `tabWarehouse` left join `tabBin`
|
||||
on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions}
|
||||
where
|
||||
`tabWarehouse`.`{key}` like {txt}
|
||||
{fcond} {mcond}
|
||||
order by ifnull(`tabBin`.actual_qty, 0) desc
|
||||
order by ifnull(`tabBin`.actual_qty, 0) desc, `tabWarehouse`.`{warehouse_field}` asc
|
||||
limit
|
||||
{page_len} offset {start}
|
||||
""".format(
|
||||
warehouse_field=warehouse_field,
|
||||
bin_conditions=get_filters_cond(
|
||||
doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True
|
||||
),
|
||||
|
@ -599,7 +599,7 @@ class SellingController(StockController):
|
||||
if self.doctype in ["Sales Order", "Quotation"]:
|
||||
for item in self.items:
|
||||
item.gross_profit = flt(
|
||||
((item.base_rate - item.valuation_rate) * item.stock_qty), self.precision("amount", item)
|
||||
((item.base_rate - flt(item.valuation_rate)) * item.stock_qty), self.precision("amount", item)
|
||||
)
|
||||
|
||||
def set_customer_address(self):
|
||||
|
@ -1334,10 +1334,10 @@ def get_sales_orders(self):
|
||||
)
|
||||
|
||||
date_field_mapper = {
|
||||
"from_date": self.from_date >= so.transaction_date,
|
||||
"to_date": self.to_date <= so.transaction_date,
|
||||
"from_delivery_date": self.from_delivery_date >= so_item.delivery_date,
|
||||
"to_delivery_date": self.to_delivery_date <= so_item.delivery_date,
|
||||
"from_date": so.transaction_date >= self.from_date,
|
||||
"to_date": so.transaction_date <= self.to_date,
|
||||
"from_delivery_date": so_item.delivery_date >= self.from_delivery_date,
|
||||
"to_delivery_date": so_item.delivery_date <= self.to_delivery_date,
|
||||
}
|
||||
|
||||
for field, value in date_field_mapper.items():
|
||||
|
@ -1511,14 +1511,14 @@ def get_serial_nos_for_work_order(work_order, production_item):
|
||||
|
||||
|
||||
def validate_operation_data(row):
|
||||
if row.get("qty") <= 0:
|
||||
if flt(row.get("qty")) <= 0:
|
||||
frappe.throw(
|
||||
_("Quantity to Manufacture can not be zero for the operation {0}").format(
|
||||
frappe.bold(row.get("operation"))
|
||||
)
|
||||
)
|
||||
|
||||
if row.get("qty") > row.get("pending_qty"):
|
||||
if flt(row.get("qty")) > flt(row.get("pending_qty")):
|
||||
frappe.throw(
|
||||
_("For operation {0}: Quantity ({1}) can not be greater than pending quantity({2})").format(
|
||||
frappe.bold(row.get("operation")),
|
||||
|
@ -230,6 +230,7 @@ class Customer(TransactionBase):
|
||||
|
||||
if self.flags.is_new_doc:
|
||||
self.link_lead_address_and_contact()
|
||||
self.copy_communication()
|
||||
|
||||
self.update_customer_groups()
|
||||
|
||||
@ -291,6 +292,17 @@ class Customer(TransactionBase):
|
||||
linked_doc.append("links", dict(link_doctype="Customer", link_name=self.name))
|
||||
linked_doc.save(ignore_permissions=self.flags.ignore_permissions)
|
||||
|
||||
def copy_communication(self):
|
||||
if not self.lead_name or not frappe.db.get_single_value(
|
||||
"CRM Settings", "carry_forward_communication_and_comments"
|
||||
):
|
||||
return
|
||||
|
||||
from erpnext.crm.utils import copy_comments, link_communications
|
||||
|
||||
copy_comments("Lead", self.lead_name, self)
|
||||
link_communications("Lead", self.lead_name, self)
|
||||
|
||||
def validate_name_with_customer_group(self):
|
||||
if frappe.db.exists("Customer Group", self.name):
|
||||
frappe.throw(
|
||||
@ -560,15 +572,14 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def send_emails(args):
|
||||
args = json.loads(args)
|
||||
subject = _("Credit limit reached for customer {0}").format(args.get("customer"))
|
||||
def send_emails(customer, customer_outstanding, credit_limit, credit_controller_users_list):
|
||||
if isinstance(credit_controller_users_list, str):
|
||||
credit_controller_users_list = json.loads(credit_controller_users_list)
|
||||
subject = _("Credit limit reached for customer {0}").format(customer)
|
||||
message = _("Credit limit has been crossed for customer {0} ({1}/{2})").format(
|
||||
args.get("customer"), args.get("customer_outstanding"), args.get("credit_limit")
|
||||
)
|
||||
frappe.sendmail(
|
||||
recipients=args.get("credit_controller_users_list"), subject=subject, message=message
|
||||
customer, customer_outstanding, credit_limit
|
||||
)
|
||||
frappe.sendmail(recipients=credit_controller_users_list, subject=subject, message=message)
|
||||
|
||||
|
||||
def get_customer_outstanding(
|
||||
|
@ -149,6 +149,13 @@ class LandedCostVoucher(Document):
|
||||
self.get("items")[item_count - 1].applicable_charges += diff
|
||||
|
||||
def validate_applicable_charges_for_item(self):
|
||||
if self.distribute_charges_based_on == "Distribute Manually" and len(self.taxes) > 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please keep one Applicable Charges, when 'Distribute Charges Based On' is 'Distribute Manually'. For more charges, please create another Landed Cost Voucher."
|
||||
)
|
||||
)
|
||||
|
||||
based_on = self.distribute_charges_based_on.lower()
|
||||
|
||||
if based_on != "distribute manually":
|
||||
|
@ -1360,16 +1360,16 @@ def get_item_account_wise_additional_cost(purchase_document):
|
||||
for lcv in landed_cost_vouchers:
|
||||
landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent)
|
||||
|
||||
based_on_field = None
|
||||
# Use amount field for total item cost for manually cost distributed LCVs
|
||||
if landed_cost_voucher_doc.distribute_charges_based_on == "Distribute Manually":
|
||||
based_on_field = "amount"
|
||||
else:
|
||||
if landed_cost_voucher_doc.distribute_charges_based_on != "Distribute Manually":
|
||||
based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
|
||||
|
||||
total_item_cost = 0
|
||||
|
||||
for item in landed_cost_voucher_doc.items:
|
||||
total_item_cost += item.get(based_on_field)
|
||||
if based_on_field:
|
||||
for item in landed_cost_voucher_doc.items:
|
||||
total_item_cost += item.get(based_on_field)
|
||||
|
||||
for item in landed_cost_voucher_doc.items:
|
||||
if item.receipt_document == purchase_document:
|
||||
|
@ -1785,6 +1785,48 @@ class TestStockEntry(FrappeTestCase):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, se1.cancel)
|
||||
|
||||
def test_auto_reorder_level(self):
|
||||
from erpnext.stock.reorder_item import reorder_item
|
||||
|
||||
item_doc = make_item(
|
||||
"Test Auto Reorder Item - 001",
|
||||
properties={"stock_uom": "Kg", "purchase_uom": "Nos", "is_stock_item": 1},
|
||||
uoms=[{"uom": "Nos", "conversion_factor": 5}],
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Item Reorder", {"parent": item_doc.name}):
|
||||
item_doc.append(
|
||||
"reorder_levels",
|
||||
{
|
||||
"warehouse_reorder_level": 0,
|
||||
"warehouse_reorder_qty": 10,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"material_request_type": "Purchase",
|
||||
},
|
||||
)
|
||||
|
||||
item_doc.save(ignore_permissions=True)
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "auto_indent", 1)
|
||||
|
||||
mr_list = reorder_item()
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "auto_indent", 0)
|
||||
mrs = frappe.get_all(
|
||||
"Material Request Item",
|
||||
fields=["qty", "stock_uom", "stock_qty"],
|
||||
filters={"item_code": item_doc.name, "uom": "Nos"},
|
||||
)
|
||||
|
||||
for mri in mrs:
|
||||
self.assertEqual(mri.stock_uom, "Kg")
|
||||
self.assertEqual(mri.stock_qty, 10)
|
||||
self.assertEqual(mri.qty, 2)
|
||||
|
||||
for mr in mr_list:
|
||||
mr.cancel()
|
||||
mr.delete()
|
||||
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
|
@ -86,7 +86,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
|
||||
|
||||
get_party_item_code(args, item, out)
|
||||
|
||||
set_valuation_rate(out, args)
|
||||
if args.get("doctype") in ["Sales Order", "Quotation"]:
|
||||
set_valuation_rate(out, args)
|
||||
|
||||
update_party_blanket_order(args, out)
|
||||
|
||||
@ -269,7 +270,9 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
if not item:
|
||||
item = frappe.get_doc("Item", args.get("item_code"))
|
||||
|
||||
if item.variant_of and not item.taxes:
|
||||
if (
|
||||
item.variant_of and not item.taxes and frappe.db.exists("Item Tax", {"parent": item.variant_of})
|
||||
):
|
||||
item.update_template_tables()
|
||||
|
||||
item_defaults = get_item_defaults(item.name, args.company)
|
||||
|
@ -34,73 +34,157 @@ def _reorder_item():
|
||||
erpnext.get_default_company() or frappe.db.sql("""select name from tabCompany limit 1""")[0][0]
|
||||
)
|
||||
|
||||
items_to_consider = frappe.db.sql_list(
|
||||
"""select name from `tabItem` item
|
||||
where is_stock_item=1 and has_variants=0
|
||||
and disabled=0
|
||||
and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %(today)s)
|
||||
and (exists (select name from `tabItem Reorder` ir where ir.parent=item.name)
|
||||
or (variant_of is not null and variant_of != ''
|
||||
and exists (select name from `tabItem Reorder` ir where ir.parent=item.variant_of))
|
||||
)""",
|
||||
{"today": nowdate()},
|
||||
)
|
||||
items_to_consider = get_items_for_reorder()
|
||||
|
||||
if not items_to_consider:
|
||||
return
|
||||
|
||||
item_warehouse_projected_qty = get_item_warehouse_projected_qty(items_to_consider)
|
||||
|
||||
def add_to_material_request(
|
||||
item_code, warehouse, reorder_level, reorder_qty, material_request_type, warehouse_group=None
|
||||
):
|
||||
if warehouse not in warehouse_company:
|
||||
def add_to_material_request(**kwargs):
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
if kwargs.warehouse not in warehouse_company:
|
||||
# a disabled warehouse
|
||||
return
|
||||
|
||||
reorder_level = flt(reorder_level)
|
||||
reorder_qty = flt(reorder_qty)
|
||||
reorder_level = flt(kwargs.reorder_level)
|
||||
reorder_qty = flt(kwargs.reorder_qty)
|
||||
|
||||
# projected_qty will be 0 if Bin does not exist
|
||||
if warehouse_group:
|
||||
projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse_group))
|
||||
if kwargs.warehouse_group:
|
||||
projected_qty = flt(
|
||||
item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse_group)
|
||||
)
|
||||
else:
|
||||
projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse))
|
||||
projected_qty = flt(
|
||||
item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse)
|
||||
)
|
||||
|
||||
if (reorder_level or reorder_qty) and projected_qty <= reorder_level:
|
||||
deficiency = reorder_level - projected_qty
|
||||
if deficiency > reorder_qty:
|
||||
reorder_qty = deficiency
|
||||
|
||||
company = warehouse_company.get(warehouse) or default_company
|
||||
company = warehouse_company.get(kwargs.warehouse) or default_company
|
||||
|
||||
material_requests[material_request_type].setdefault(company, []).append(
|
||||
{"item_code": item_code, "warehouse": warehouse, "reorder_qty": reorder_qty}
|
||||
material_requests[kwargs.material_request_type].setdefault(company, []).append(
|
||||
{
|
||||
"item_code": kwargs.item_code,
|
||||
"warehouse": kwargs.warehouse,
|
||||
"reorder_qty": reorder_qty,
|
||||
"item_details": kwargs.item_details,
|
||||
}
|
||||
)
|
||||
|
||||
for item_code in items_to_consider:
|
||||
item = frappe.get_doc("Item", item_code)
|
||||
for item_code, reorder_levels in items_to_consider.items():
|
||||
for d in reorder_levels:
|
||||
if d.has_variants:
|
||||
continue
|
||||
|
||||
if item.variant_of and not item.get("reorder_levels"):
|
||||
item.update_template_tables()
|
||||
|
||||
if item.get("reorder_levels"):
|
||||
for d in item.get("reorder_levels"):
|
||||
add_to_material_request(
|
||||
item_code,
|
||||
d.warehouse,
|
||||
d.warehouse_reorder_level,
|
||||
d.warehouse_reorder_qty,
|
||||
d.material_request_type,
|
||||
warehouse_group=d.warehouse_group,
|
||||
)
|
||||
add_to_material_request(
|
||||
item_code=item_code,
|
||||
warehouse=d.warehouse,
|
||||
reorder_level=d.warehouse_reorder_level,
|
||||
reorder_qty=d.warehouse_reorder_qty,
|
||||
material_request_type=d.material_request_type,
|
||||
warehouse_group=d.warehouse_group,
|
||||
item_details=frappe._dict(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"name": item_code,
|
||||
"item_name": d.item_name,
|
||||
"item_group": d.item_group,
|
||||
"brand": d.brand,
|
||||
"description": d.description,
|
||||
"stock_uom": d.stock_uom,
|
||||
"purchase_uom": d.purchase_uom,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
if material_requests:
|
||||
return create_material_request(material_requests)
|
||||
|
||||
|
||||
def get_items_for_reorder() -> dict[str, list]:
|
||||
reorder_table = frappe.qb.DocType("Item Reorder")
|
||||
item_table = frappe.qb.DocType("Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(reorder_table)
|
||||
.inner_join(item_table)
|
||||
.on(reorder_table.parent == item_table.name)
|
||||
.select(
|
||||
reorder_table.warehouse,
|
||||
reorder_table.warehouse_group,
|
||||
reorder_table.material_request_type,
|
||||
reorder_table.warehouse_reorder_level,
|
||||
reorder_table.warehouse_reorder_qty,
|
||||
item_table.name,
|
||||
item_table.stock_uom,
|
||||
item_table.purchase_uom,
|
||||
item_table.description,
|
||||
item_table.item_name,
|
||||
item_table.item_group,
|
||||
item_table.brand,
|
||||
item_table.variant_of,
|
||||
item_table.has_variants,
|
||||
)
|
||||
.where(
|
||||
(item_table.disabled == 0)
|
||||
& (item_table.is_stock_item == 1)
|
||||
& (
|
||||
(item_table.end_of_life.isnull())
|
||||
| (item_table.end_of_life > nowdate())
|
||||
| (item_table.end_of_life == "0000-00-00")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
itemwise_reorder = frappe._dict({})
|
||||
for d in data:
|
||||
itemwise_reorder.setdefault(d.name, []).append(d)
|
||||
|
||||
itemwise_reorder = get_reorder_levels_for_variants(itemwise_reorder)
|
||||
|
||||
return itemwise_reorder
|
||||
|
||||
|
||||
def get_reorder_levels_for_variants(itemwise_reorder):
|
||||
item_table = frappe.qb.DocType("Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(item_table)
|
||||
.select(
|
||||
item_table.name,
|
||||
item_table.variant_of,
|
||||
)
|
||||
.where(
|
||||
(item_table.disabled == 0)
|
||||
& (item_table.is_stock_item == 1)
|
||||
& (
|
||||
(item_table.end_of_life.isnull())
|
||||
| (item_table.end_of_life > nowdate())
|
||||
| (item_table.end_of_life == "0000-00-00")
|
||||
)
|
||||
& (item_table.variant_of.notnull())
|
||||
)
|
||||
)
|
||||
|
||||
variants_item = query.run(as_dict=True)
|
||||
for row in variants_item:
|
||||
if not itemwise_reorder.get(row.name) and itemwise_reorder.get(row.variant_of):
|
||||
itemwise_reorder.setdefault(row.name, []).extend(itemwise_reorder.get(row.variant_of, []))
|
||||
|
||||
return itemwise_reorder
|
||||
|
||||
|
||||
def get_item_warehouse_projected_qty(items_to_consider):
|
||||
item_warehouse_projected_qty = {}
|
||||
items_to_consider = list(items_to_consider.keys())
|
||||
|
||||
for item_code, warehouse, projected_qty in frappe.db.sql(
|
||||
"""select item_code, warehouse, projected_qty
|
||||
@ -164,7 +248,7 @@ def create_material_request(material_requests):
|
||||
|
||||
for d in items:
|
||||
d = frappe._dict(d)
|
||||
item = frappe.get_doc("Item", d.item_code)
|
||||
item = d.get("item_details")
|
||||
uom = item.stock_uom
|
||||
conversion_factor = 1.0
|
||||
|
||||
@ -190,6 +274,7 @@ def create_material_request(material_requests):
|
||||
"item_code": d.item_code,
|
||||
"schedule_date": add_days(nowdate(), cint(item.lead_time_days)),
|
||||
"qty": qty,
|
||||
"conversion_factor": conversion_factor,
|
||||
"uom": uom,
|
||||
"stock_uom": item.stock_uom,
|
||||
"warehouse": d.warehouse,
|
||||
|
@ -90,8 +90,7 @@ class StockBalanceReport(object):
|
||||
self.opening_data.setdefault(group_by_key, entry)
|
||||
|
||||
def prepare_new_data(self):
|
||||
if not self.sle_entries:
|
||||
return
|
||||
self.item_warehouse_map = self.get_item_warehouse_map()
|
||||
|
||||
if self.filters.get("show_stock_ageing_data"):
|
||||
self.filters["show_warehouse_wise_stock"] = True
|
||||
@ -99,7 +98,8 @@ class StockBalanceReport(object):
|
||||
|
||||
_func = itemgetter(1)
|
||||
|
||||
self.item_warehouse_map = self.get_item_warehouse_map()
|
||||
del self.sle_entries
|
||||
|
||||
sre_details = self.get_sre_reserved_qty_details()
|
||||
|
||||
variant_values = {}
|
||||
@ -143,15 +143,22 @@ class StockBalanceReport(object):
|
||||
item_warehouse_map = {}
|
||||
self.opening_vouchers = self.get_opening_vouchers()
|
||||
|
||||
for entry in self.sle_entries:
|
||||
group_by_key = self.get_group_by_key(entry)
|
||||
if group_by_key not in item_warehouse_map:
|
||||
self.initialize_data(item_warehouse_map, group_by_key, entry)
|
||||
if self.filters.get("show_stock_ageing_data"):
|
||||
self.sle_entries = self.sle_query.run(as_dict=True)
|
||||
|
||||
self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key)
|
||||
with frappe.db.unbuffered_cursor():
|
||||
if not self.filters.get("show_stock_ageing_data"):
|
||||
self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True)
|
||||
|
||||
if self.opening_data.get(group_by_key):
|
||||
del self.opening_data[group_by_key]
|
||||
for entry in self.sle_entries:
|
||||
group_by_key = self.get_group_by_key(entry)
|
||||
if group_by_key not in item_warehouse_map:
|
||||
self.initialize_data(item_warehouse_map, group_by_key, entry)
|
||||
|
||||
self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key)
|
||||
|
||||
if self.opening_data.get(group_by_key):
|
||||
del self.opening_data[group_by_key]
|
||||
|
||||
for group_by_key, entry in self.opening_data.items():
|
||||
if group_by_key not in item_warehouse_map:
|
||||
@ -252,7 +259,8 @@ class StockBalanceReport(object):
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (table.company == self.filters.company)
|
||||
& ((table.to_date <= self.from_date))
|
||||
& (table.to_date <= self.from_date)
|
||||
& (table.status == "Completed")
|
||||
)
|
||||
.orderby(table.to_date, order=Order.desc)
|
||||
.limit(1)
|
||||
@ -305,7 +313,7 @@ class StockBalanceReport(object):
|
||||
if self.filters.get("company"):
|
||||
query = query.where(sle.company == self.filters.get("company"))
|
||||
|
||||
self.sle_entries = query.run(as_dict=True)
|
||||
self.sle_query = query
|
||||
|
||||
def apply_inventory_dimensions_filters(self, query, sle) -> str:
|
||||
inventory_dimension_fields = self.get_inventory_dimension_fields()
|
||||
|
@ -283,6 +283,7 @@ class SerialBatchBundle:
|
||||
if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1)
|
||||
else "Inactive",
|
||||
)
|
||||
.set(sn_table.company, self.sle.company)
|
||||
.where(sn_table.name.isin(serial_nos))
|
||||
).run()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user