Merge pull request #44103 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2024-11-13 20:31:36 +05:30
committed by GitHub
42 changed files with 1089 additions and 213 deletions

View File

@@ -4,21 +4,21 @@
# the repo. Unless a later match takes precedence,
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @anandbaburajan @deepeshgarg007
erpnext/assets/ @khushi8112 @deepeshgarg007
erpnext/regional @deepeshgarg007 @ruthra-kumar
erpnext/selling @deepeshgarg007 @ruthra-kumar
erpnext/support/ @deepeshgarg007
pos*
erpnext/buying/ @rohitwaghchaure @s-aga-r
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
erpnext/quality_management/ @rohitwaghchaure @s-aga-r
erpnext/stock/ @rohitwaghchaure @s-aga-r
erpnext/subcontracting @rohitwaghchaure @s-aga-r
erpnext/buying/ @rohitwaghchaure
erpnext/maintenance/ @rohitwaghchaure
erpnext/manufacturing/ @rohitwaghchaure
erpnext/quality_management/ @rohitwaghchaure
erpnext/stock/ @rohitwaghchaure
erpnext/subcontracting @rohitwaghchaure
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
erpnext/patches/ @deepeshgarg007
.github/ @deepeshgarg007
pyproject.toml @phot0n
pyproject.toml @akhilnarang

View File

@@ -1,7 +1,7 @@
import json
import frappe
from frappe import _
from frappe import _, qb
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import flt, nowdate
@@ -564,11 +564,24 @@ def make_payment_request(**args):
# fetches existing payment request `grand_total` amount
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
if existing_payment_request_amount:
def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount):
grand_total -= existing_payment_request_amount
if not grand_total:
frappe.throw(_("Payment Request is already created"))
return grand_total
if existing_payment_request_amount:
if args.order_type == "Shopping Cart":
# If Payment Request is in an advanced stage, then create for remaining amount.
if get_existing_payment_request_amount(
ref_doc.doctype, ref_doc.name, ["Initiated", "Partially Paid", "Payment Ordered", "Paid"]
):
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
else:
# If PR's are processed, cancel all of them.
cancel_old_payment_requests(ref_doc.doctype, ref_doc.name)
else:
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
if draft_payment_request:
frappe.db.set_value(
@@ -678,21 +691,65 @@ def get_amount(ref_doc, payment_account=None):
frappe.throw(_("Payment Entry is already created"))
def get_existing_payment_request_amount(ref_dt, ref_dn):
def get_irequest_status(payment_requests: None | list = None) -> list:
IR = frappe.qb.DocType("Integration Request")
res = []
if payment_requests:
res = (
frappe.qb.from_(IR)
.select(IR.name)
.where(IR.reference_doctype.eq("Payment Request"))
.where(IR.reference_docname.isin(payment_requests))
.where(IR.status.isin(["Authorized", "Completed"]))
.run(as_dict=True)
)
return res
def cancel_old_payment_requests(ref_dt, ref_dn):
PR = frappe.qb.DocType("Payment Request")
if res := (
frappe.qb.from_(PR)
.select(PR.name)
.where(PR.reference_doctype == ref_dt)
.where(PR.reference_name == ref_dn)
.where(PR.docstatus == 1)
.where(PR.status.isin(["Draft", "Requested"]))
.run(as_dict=True)
):
if get_irequest_status([x.name for x in res]):
frappe.throw(_("Another Payment Request is already processed"))
else:
for x in res:
doc = frappe.get_doc("Payment Request", x.name)
doc.flags.ignore_permissions = True
doc.cancel()
if ireqs := get_irequests_of_payment_request(doc.name):
for ireq in ireqs:
frappe.db.set_value("Integration Request", ireq.name, "status", "Cancelled")
def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None = None) -> list:
"""
Return the total amount of Payment Requests against a reference document.
"""
PR = frappe.qb.DocType("Payment Request")
response = (
query = (
frappe.qb.from_(PR)
.select(Sum(PR.grand_total))
.where(PR.reference_doctype == ref_dt)
.where(PR.reference_name == ref_dn)
.where(PR.docstatus == 1)
.run()
)
if statuses:
query = query.where(PR.status.isin(statuses))
response = query.run()
return response[0][0] if response[0] else 0
@@ -915,3 +972,17 @@ def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len,
)
for pr in open_payment_requests
]
def get_irequests_of_payment_request(doc: str | None = None) -> list:
res = []
if doc:
res = frappe.db.get_all(
"Integration Request",
{
"reference_doctype": "Payment Request",
"reference_docname": doc,
"status": "Queued",
},
)
return res

View File

@@ -65,7 +65,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
super.refresh();
if (doc.docstatus == 1 && !doc.is_return) {
this.frm.add_custom_button(__("Return"), this.make_sales_return, __("Create"));
this.frm.add_custom_button(__("Return"), this.make_sales_return.bind(this), __("Create"));
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
}

View File

@@ -93,7 +93,7 @@ class TestPOSInvoice(unittest.TestCase):
inv.save()
self.assertEqual(inv.net_total, 4298.25)
self.assertEqual(inv.net_total, 4298.24)
self.assertEqual(inv.grand_total, 4900.00)
def test_tax_calculation_with_multiple_items(self):

View File

@@ -343,7 +343,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
inv.load_from_db()
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
self.assertEqual(consolidated_invoice.status, "Return")
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001)
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.002)
finally:
frappe.set_user("Administrator")

View File

@@ -863,6 +863,7 @@ class PurchaseInvoice(BuyingController):
self.make_tax_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
self.make_gl_entries_for_tax_withholding(gl_entries)
gl_entries = make_regional_gl_entries(gl_entries, self)
@@ -896,32 +897,37 @@ class PurchaseInvoice(BuyingController):
)
if grand_total and not self.is_internal_transfer():
against_voucher = self.name
if self.is_return and self.return_against and not self.update_outstanding_for_self:
against_voucher = self.return_against
self.add_supplier_gl_entry(gl_entries, base_grand_total, grand_total)
# Did not use base_grand_total to book rounding loss gle
gl_entries.append(
self.get_gl_dict(
{
"account": self.credit_to,
"party_type": "Supplier",
"party": self.supplier,
"due_date": self.due_date,
"against": self.against_expense_account,
"credit": base_grand_total,
"credit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency
else grand_total,
"against_voucher": against_voucher,
"against_voucher_type": self.doctype,
"project": self.project,
"cost_center": self.cost_center,
},
self.party_account_currency,
item=self,
)
)
def add_supplier_gl_entry(
self, gl_entries, base_grand_total, grand_total, against_account=None, remarks=None, skip_merge=False
):
against_voucher = self.name
if self.is_return and self.return_against and not self.update_outstanding_for_self:
against_voucher = self.return_against
# Did not use base_grand_total to book rounding loss gle
gl = {
"account": self.credit_to,
"party_type": "Supplier",
"party": self.supplier,
"due_date": self.due_date,
"against": against_account or self.against_expense_account,
"credit": base_grand_total,
"credit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency
else grand_total,
"against_voucher": against_voucher,
"against_voucher_type": self.doctype,
"project": self.project,
"cost_center": self.cost_center,
"_skip_merge": skip_merge,
}
if remarks:
gl["remarks"] = remarks
gl_entries.append(self.get_gl_dict(gl, self.party_account_currency, item=self))
def make_item_gl_entries(self, gl_entries):
# item gl entries
@@ -1413,6 +1419,31 @@ class PurchaseInvoice(BuyingController):
)
)
def make_gl_entries_for_tax_withholding(self, gl_entries):
"""
Tax withholding amount is not part of supplier invoice.
Separate supplier GL Entry for correct reporting.
"""
if not self.apply_tds:
return
for row in self.get("taxes"):
if not row.is_tax_withholding_account or not row.tax_amount:
continue
base_tds_amount = row.base_tax_amount_after_discount_amount
tds_amount = row.tax_amount_after_discount_amount
self.add_supplier_gl_entry(gl_entries, base_tds_amount, tds_amount)
self.add_supplier_gl_entry(
gl_entries,
-base_tds_amount,
-tds_amount,
against_account=row.account_head,
remarks=_("TDS Deducted"),
skip_merge=True,
)
def make_payment_gl_entries(self, gl_entries):
# Make Cash GL Entries
if cint(self.is_paid) and self.cash_bank_account and self.paid_amount:

View File

@@ -1544,6 +1544,61 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
payment_entry.load_from_db()
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
def test_purchase_gl_with_tax_withholding_tax(self):
company = "_Test Company"
tds_account_args = {
"doctype": "Account",
"account_name": "TDS Payable",
"account_type": "Tax",
"parent_account": frappe.db.get_value(
"Account", {"account_name": "Duties and Taxes", "company": company}
),
"company": company,
}
tds_account = create_account(**tds_account_args)
tax_withholding_category = "Test TDS - 194 - Dividends - Individual"
# Update tax withholding category with current fiscal year and rate details
create_tax_witholding_category(tax_withholding_category, company, tds_account)
# create a new supplier to test
supplier = create_supplier(
supplier_name="_Test TDS Advance Supplier",
tax_withholding_category=tax_withholding_category,
)
pi = make_purchase_invoice(
supplier=supplier.name,
rate=3000,
qty=1,
item="_Test Non Stock Item",
do_not_submit=1,
)
pi.apply_tds = 1
pi.tax_withholding_category = tax_withholding_category
pi.save()
pi.submit()
self.assertEqual(pi.taxes[0].tax_amount, 300)
self.assertEqual(pi.taxes[0].account_head, tds_account)
gl_entries = frappe.get_all(
"GL Entry",
filters={"voucher_no": pi.name, "voucher_type": "Purchase Invoice", "account": "Creditors - _TC"},
fields=["account", "against", "debit", "credit"],
)
for gle in gl_entries:
if gle.debit:
# GL Entry with TDS Amount
self.assertEqual(gle.against, tds_account)
self.assertEqual(gle.debit, 300)
else:
# GL Entry with Purchase Invoice Amount
self.assertEqual(gle.credit, 3000)
def test_provisional_accounting_entry(self):
setup_provisional_accounting()

View File

@@ -314,7 +314,8 @@ class TestSalesInvoice(FrappeTestCase):
si.insert()
# with inclusive tax
self.assertEqual(si.items[0].net_amount, 3947.368421052631)
self.assertEqual(si.items[0].net_amount, 3947.37)
self.assertEqual(si.net_total, si.base_net_total)
self.assertEqual(si.net_total, 3947.37)
self.assertEqual(si.grand_total, 5000)
@@ -658,7 +659,7 @@ class TestSalesInvoice(FrappeTestCase):
62.5,
625.0,
50,
499.97600115194473,
499.98,
],
"_Test Item Home Desktop 200": [
190.66,
@@ -669,7 +670,7 @@ class TestSalesInvoice(FrappeTestCase):
190.66,
953.3,
150,
749.9968530500239,
750,
],
}
@@ -682,20 +683,21 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(d.get(k), expected_values[d.item_code][i])
# check net total
self.assertEqual(si.net_total, 1249.97)
self.assertEqual(si.base_net_total, si.net_total)
self.assertEqual(si.net_total, 1249.98)
self.assertEqual(si.total, 1578.3)
# check tax calculation
expected_values = {
"keys": ["tax_amount", "total"],
"_Test Account Excise Duty - _TC": [140, 1389.97],
"_Test Account Education Cess - _TC": [2.8, 1392.77],
"_Test Account S&H Education Cess - _TC": [1.4, 1394.17],
"_Test Account CST - _TC": [27.88, 1422.05],
"_Test Account VAT - _TC": [156.25, 1578.30],
"_Test Account Customs Duty - _TC": [125, 1703.30],
"_Test Account Shipping Charges - _TC": [100, 1803.30],
"_Test Account Discount - _TC": [-180.33, 1622.97],
"_Test Account Excise Duty - _TC": [140, 1389.98],
"_Test Account Education Cess - _TC": [2.8, 1392.78],
"_Test Account S&H Education Cess - _TC": [1.4, 1394.18],
"_Test Account CST - _TC": [27.88, 1422.06],
"_Test Account VAT - _TC": [156.25, 1578.31],
"_Test Account Customs Duty - _TC": [125, 1703.31],
"_Test Account Shipping Charges - _TC": [100, 1803.31],
"_Test Account Discount - _TC": [-180.33, 1622.98],
}
for d in si.get("taxes"):
@@ -731,7 +733,7 @@ class TestSalesInvoice(FrappeTestCase):
"base_rate": 2500,
"base_amount": 25000,
"net_rate": 40,
"net_amount": 399.9808009215558,
"net_amount": 399.98,
"base_net_rate": 2000,
"base_net_amount": 19999,
},
@@ -745,7 +747,7 @@ class TestSalesInvoice(FrappeTestCase):
"base_rate": 7500,
"base_amount": 37500,
"net_rate": 118.01,
"net_amount": 590.0531205155963,
"net_amount": 590.05,
"base_net_rate": 5900.5,
"base_net_amount": 29502.5,
},
@@ -783,8 +785,13 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(si.base_grand_total, 60795)
self.assertEqual(si.grand_total, 1215.90)
self.assertEqual(si.rounding_adjustment, 0.01)
self.assertEqual(si.base_rounding_adjustment, 0.50)
# no rounding adjustment as the Smallest Currency Fraction Value of USD is 0.01
if frappe.db.get_value("Currency", "USD", "smallest_currency_fraction_value") < 0.01:
self.assertEqual(si.rounding_adjustment, 0.10)
self.assertEqual(si.base_rounding_adjustment, 5.0)
else:
self.assertEqual(si.rounding_adjustment, 0.0)
self.assertEqual(si.base_rounding_adjustment, 0.0)
def test_outstanding(self):
w = self.make()
@@ -2172,7 +2179,7 @@ class TestSalesInvoice(FrappeTestCase):
def test_rounding_adjustment_2(self):
si = create_sales_invoice(rate=400, do_not_save=True)
for rate in [400, 600, 100]:
for rate in [400.25, 600.30, 100.65]:
si.append(
"items",
{
@@ -2198,18 +2205,19 @@ class TestSalesInvoice(FrappeTestCase):
)
si.save()
si.submit()
self.assertEqual(si.net_total, 1271.19)
self.assertEqual(si.grand_total, 1500)
self.assertEqual(si.total_taxes_and_charges, 228.82)
self.assertEqual(si.rounding_adjustment, -0.01)
self.assertEqual(si.net_total, si.base_net_total)
self.assertEqual(si.net_total, 1272.20)
self.assertEqual(si.grand_total, 1501.20)
self.assertEqual(si.total_taxes_and_charges, 229)
self.assertEqual(si.rounding_adjustment, -0.20)
round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account")
expected_values = {
"_Test Account Service Tax - _TC": [0.0, 114.41],
"_Test Account VAT - _TC": [0.0, 114.41],
si.debit_to: [1500, 0.0],
round_off_account: [0.01, 0.01],
"Sales - _TC": [0.0, 1271.18],
"_Test Account Service Tax - _TC": [0.0, 114.50],
"_Test Account VAT - _TC": [0.0, 114.50],
si.debit_to: [1501, 0.0],
round_off_account: [0.20, 0.0],
"Sales - _TC": [0.0, 1272.20],
}
gl_entries = frappe.db.sql(
@@ -2267,7 +2275,8 @@ class TestSalesInvoice(FrappeTestCase):
si.save()
si.submit()
self.assertEqual(si.net_total, 4007.16)
self.assertEqual(si.net_total, si.base_net_total)
self.assertEqual(si.net_total, 4007.15)
self.assertEqual(si.grand_total, 4488.02)
self.assertEqual(si.total_taxes_and_charges, 480.86)
self.assertEqual(si.rounding_adjustment, -0.02)
@@ -2280,7 +2289,7 @@ class TestSalesInvoice(FrappeTestCase):
["_Test Account Service Tax - _TC", 0.0, 240.43],
["_Test Account VAT - _TC", 0.0, 240.43],
["Sales - _TC", 0.0, 4007.15],
[round_off_account, 0.02, 0.01],
[round_off_account, 0.01, 0.0],
]
)

View File

@@ -74,11 +74,17 @@ class TestTaxWithholdingCategory(FrappeTestCase):
self.assertEqual(pi.grand_total, 18000)
# check gl entry for the purchase invoice
gl_entries = frappe.db.get_all("GL Entry", filters={"voucher_no": pi.name}, fields=["*"])
gl_entries = frappe.db.get_all(
"GL Entry",
filters={"voucher_no": pi.name},
fields=["account", "sum(debit) as debit", "sum(credit) as credit"],
group_by="account",
)
self.assertEqual(len(gl_entries), 3)
for d in gl_entries:
if d.account == pi.credit_to:
self.assertEqual(d.credit, 18000)
self.assertEqual(d.credit, 20000)
self.assertEqual(d.debit, 2000)
elif d.account == pi.items[0].get("expense_account"):
self.assertEqual(d.debit, 20000)
elif d.account == pi.taxes[0].get("account_head"):

View File

@@ -234,6 +234,10 @@ def merge_similar_entries(gl_map, precision=None):
merge_properties = get_merge_properties(accounting_dimensions)
for entry in gl_map:
if entry._skip_merge:
merged_gl_map.append(entry)
continue
entry.merge_key = get_merge_key(entry, merge_properties)
# if there is already an entry in this account then just add it
# to that entry

View File

@@ -0,0 +1,179 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import getdate, today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.sales_register.sales_register import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
self.create_child_cost_center()
def tearDown(self):
frappe.db.rollback()
def create_child_cost_center(self):
cc_name = "South Wing"
if frappe.db.exists("Cost Center", cc_name):
cc = frappe.get_doc("Cost Center", cc_name)
else:
parent = frappe.db.get_value("Cost Center", self.cost_center, "parent_cost_center")
cc = frappe.get_doc(
{
"doctype": "Cost Center",
"company": self.company,
"is_group": False,
"parent_cost_center": parent,
"cost_center_name": cc_name,
}
)
cc = cc.save()
self.south_cc = cc.name
def create_sales_invoice(self, rate=100, do_not_submit=False):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=rate,
price_list_rate=rate,
do_not_save=1,
)
si = si.save()
if not do_not_submit:
si = si.submit()
return si
def test_basic_report_output(self):
si = self.create_sales_invoice(rate=98)
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
report = execute(filters)
res = [x for x in report[1] if x.get("voucher_no") == si.name]
expected_result = {
"voucher_type": si.doctype,
"voucher_no": si.name,
"posting_date": getdate(),
"customer": self.customer,
"receivable_account": self.debit_to,
"net_total": 98.0,
"grand_total": 98.0,
"debit": 98.0,
}
report_output = {k: v for k, v in res[0].items() if k in expected_result}
self.assertDictEqual(report_output, expected_result)
def test_journal_with_cost_center_filter(self):
je1 = frappe.get_doc(
{
"doctype": "Journal Entry",
"voucher_type": "Journal Entry",
"company": self.company,
"posting_date": getdate(),
"accounts": [
{
"account": self.debit_to,
"party_type": "Customer",
"party": self.customer,
"credit_in_account_currency": 77,
"credit": 77,
"is_advance": "Yes",
"cost_center": self.cost_center,
},
{
"account": self.cash,
"debit_in_account_currency": 77,
"debit": 77,
},
],
}
)
je1.submit()
je2 = frappe.get_doc(
{
"doctype": "Journal Entry",
"voucher_type": "Journal Entry",
"company": self.company,
"posting_date": getdate(),
"accounts": [
{
"account": self.debit_to,
"party_type": "Customer",
"party": self.customer,
"credit_in_account_currency": 98,
"credit": 98,
"is_advance": "Yes",
"cost_center": self.south_cc,
},
{
"account": self.cash,
"debit_in_account_currency": 98,
"debit": 98,
},
],
}
)
je2.submit()
filters = frappe._dict(
{
"from_date": today(),
"to_date": today(),
"company": self.company,
"include_payments": True,
"customer": self.customer,
"cost_center": self.cost_center,
}
)
report_output = execute(filters)[1]
filtered_output = [x for x in report_output if x.get("voucher_no") == je1.name]
self.assertEqual(len(filtered_output), 1)
expected_result = {
"voucher_type": je1.doctype,
"voucher_no": je1.name,
"posting_date": je1.posting_date,
"customer": self.customer,
"receivable_account": self.debit_to,
"net_total": 77.0,
"credit": 77.0,
}
result_fields = {k: v for k, v in filtered_output[0].items() if k in expected_result}
self.assertDictEqual(result_fields, expected_result)
filters = frappe._dict(
{
"from_date": today(),
"to_date": today(),
"company": self.company,
"include_payments": True,
"customer": self.customer,
"cost_center": self.south_cc,
}
)
report_output = execute(filters)[1]
filtered_output = [x for x in report_output if x.get("voucher_no") == je2.name]
self.assertEqual(len(filtered_output), 1)
expected_result = {
"voucher_type": je2.doctype,
"voucher_no": je2.name,
"posting_date": je2.posting_date,
"customer": self.customer,
"receivable_account": self.debit_to,
"net_total": 98.0,
"credit": 98.0,
}
result_output = {k: v for k, v in filtered_output[0].items() if k in expected_result}
self.assertDictEqual(result_output, expected_result)

View File

@@ -255,7 +255,9 @@ def get_journal_entries(filters, args):
)
.orderby(je.posting_date, je.name, order=Order.desc)
)
query = apply_common_conditions(filters, query, doctype="Journal Entry", payments=True)
query = apply_common_conditions(
filters, query, doctype="Journal Entry", child_doctype="Journal Entry Account", payments=True
)
journal_entries = query.run(as_dict=True)
return journal_entries
@@ -306,7 +308,9 @@ def apply_common_conditions(filters, query, doctype, child_doctype=None, payment
query = query.where(parent_doc.posting_date <= filters.to_date)
if payments:
if filters.get("cost_center"):
if doctype == "Journal Entry" and filters.get("cost_center"):
query = query.where(child_doc.cost_center == filters.cost_center)
elif filters.get("cost_center"):
query = query.where(parent_doc.cost_center == filters.cost_center)
else:
if filters.get("cost_center"):

View File

@@ -415,7 +415,6 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p
stock_ledger_entry.batch_no,
Sum(stock_ledger_entry.actual_qty).as_("qty"),
)
.where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull()))
.where(stock_ledger_entry.is_cancelled == 0)
.where(
(stock_ledger_entry.item_code == filters.get("item_code"))
@@ -428,6 +427,9 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p
.limit(page_len)
)
if not filters.get("include_expired_batches"):
query = query.where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull()))
query = query.select(
Concat("MFG-", batch_table.manufacturing_date).as_("manufacturing_date"),
Concat("EXP-", batch_table.expiry_date).as_("expiry_date"),
@@ -466,7 +468,6 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0
bundle.batch_no,
Sum(bundle.qty).as_("qty"),
)
.where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull()))
.where(stock_ledger_entry.is_cancelled == 0)
.where(
(stock_ledger_entry.item_code == filters.get("item_code"))
@@ -479,6 +480,11 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0
.limit(page_len)
)
if not filters.get("include_expired_batches"):
bundle_query = bundle_query.where(
(batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())
)
bundle_query = bundle_query.select(
Concat("MFG-", batch_table.manufacturing_date),
Concat("EXP-", batch_table.expiry_date),

View File

@@ -8,6 +8,7 @@ import frappe
from frappe import _, scrub
from frappe.model.document import Document
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
from frappe.utils.deprecations import deprecated
import erpnext
from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate
@@ -74,7 +75,7 @@ class calculate_taxes_and_totals:
self.calculate_net_total()
self.calculate_tax_withholding_net_total()
self.calculate_taxes()
self.manipulate_grand_total_for_inclusive_tax()
self.adjust_grand_total_for_inclusive_tax()
self.calculate_totals()
self._cleanup()
self.calculate_total_net_weight()
@@ -286,7 +287,7 @@ class calculate_taxes_and_totals:
):
amount = flt(item.amount) - total_inclusive_tax_amount_per_qty
item.net_amount = flt(amount / (1 + cumulated_tax_fraction))
item.net_amount = flt(amount / (1 + cumulated_tax_fraction), item.precision("net_amount"))
item.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate"))
item.discount_percentage = flt(
item.discount_percentage, item.precision("discount_percentage")
@@ -531,7 +532,12 @@ class calculate_taxes_and_totals:
tax.base_tax_amount = round(tax.base_tax_amount, 0)
tax.base_tax_amount_after_discount_amount = round(tax.base_tax_amount_after_discount_amount, 0)
@deprecated
def manipulate_grand_total_for_inclusive_tax(self):
# for backward compatablility - if in case used by an external application
return self.adjust_grand_total_for_inclusive_tax()
def adjust_grand_total_for_inclusive_tax(self):
# if fully inclusive taxes and diff
if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")):
last_tax = self.doc.get("taxes")[-1]
@@ -553,17 +559,21 @@ class calculate_taxes_and_totals:
diff = flt(diff, self.doc.precision("rounding_adjustment"))
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
self.doc.rounding_adjustment = diff
self.doc.grand_total_diff = diff
else:
self.doc.grand_total_diff = 0
def calculate_totals(self):
if self.doc.get("taxes"):
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(self.doc.rounding_adjustment)
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(
self.doc.get("grand_total_diff")
)
else:
self.doc.grand_total = flt(self.doc.net_total)
if self.doc.get("taxes"):
self.doc.total_taxes_and_charges = flt(
self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment),
self.doc.grand_total - self.doc.net_total - flt(self.doc.get("grand_total_diff")),
self.doc.precision("total_taxes_and_charges"),
)
else:
@@ -626,8 +636,8 @@ class calculate_taxes_and_totals:
self.doc.grand_total, self.doc.currency, self.doc.precision("rounded_total")
)
# if print_in_rate is set, we would have already calculated rounding adjustment
self.doc.rounding_adjustment += flt(
# rounding adjustment should always be the difference vetween grand and rounded total
self.doc.rounding_adjustment = flt(
self.doc.rounded_total - self.doc.grand_total, self.doc.precision("rounding_adjustment")
)

View File

@@ -742,11 +742,8 @@ class BOM(WebsiteGenerator):
base_total_rm_cost = 0
for d in self.get("items"):
if not d.is_stock_item and self.rm_cost_as_per == "Valuation Rate":
continue
old_rate = d.rate
if not self.bom_creator:
if not self.bom_creator and d.is_stock_item:
d.rate = self.get_rm_rate(
{
"company": self.company,

View File

@@ -37,7 +37,7 @@ frappe.ui.form.on("Job Card", {
frappe.flags.resume_job = 0;
let has_items = frm.doc.items && frm.doc.items.length;
if (!frm.is_new() && frm.doc.__onload.work_order_closed) {
if (!frm.is_new() && frm.doc.__onload?.work_order_closed) {
frm.disable_save();
return;
}

View File

@@ -87,17 +87,17 @@ frappe.ui.form.on("Production Plan", {
if (frm.doc.docstatus === 1) {
frm.trigger("show_progress");
if (frm.doc.status !== "Completed") {
frm.add_custom_button(
__("Production Plan Summary"),
() => {
frappe.set_route("query-report", "Production Plan Summary", {
production_plan: frm.doc.name,
});
},
__("View")
);
frm.add_custom_button(
__("Production Plan Summary"),
() => {
frappe.set_route("query-report", "Production Plan Summary", {
production_plan: frm.doc.name,
});
},
__("View")
);
if (frm.doc.status !== "Completed") {
if (frm.doc.status === "Closed") {
frm.add_custom_button(
__("Re-open"),

View File

@@ -27,32 +27,51 @@ def get_data(filters):
def get_production_plan_item_details(filters, data, order_details):
itemwise_indent = {}
production_plan_doc = frappe.get_cached_doc("Production Plan", filters.get("production_plan"))
for row in production_plan_doc.po_items:
work_order = frappe.get_value(
work_orders = frappe.get_all(
"Work Order",
{"production_plan_item": row.name, "bom_no": row.bom_no, "production_item": row.item_code},
"name",
filters={
"production_plan_item": row.name,
"bom_no": row.bom_no,
"production_item": row.item_code,
},
pluck="name",
)
if row.item_code not in itemwise_indent:
itemwise_indent.setdefault(row.item_code, {})
order_qty = row.planned_qty
total_produced_qty = 0.0
pending_qty = 0.0
for work_order in work_orders:
produced_qty = flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0))
pending_qty = flt(order_qty) - produced_qty
total_produced_qty += produced_qty
data.append(
{
"indent": 0,
"item_code": row.item_code,
"sales_order": row.get("sales_order"),
"item_name": frappe.get_cached_value("Item", row.item_code, "item_name"),
"qty": order_qty,
"document_type": "Work Order",
"document_name": work_order or "",
"bom_level": 0,
"produced_qty": produced_qty,
"pending_qty": pending_qty,
}
)
order_qty = pending_qty
data.append(
{
"indent": 0,
"item_code": row.item_code,
"sales_order": row.get("sales_order"),
"item_name": frappe.get_cached_value("Item", row.item_code, "item_name"),
"indent": 0,
"qty": row.planned_qty,
"document_type": "Work Order",
"document_name": work_order or "",
"bom_level": 0,
"produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0),
"pending_qty": flt(row.planned_qty)
- flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)),
"produced_qty": total_produced_qty,
"pending_qty": pending_qty,
}
)

View File

@@ -153,14 +153,14 @@ class Task(NestedSet):
def validate_parent_template_task(self):
if self.parent_task:
if not frappe.db.get_value("Task", self.parent_task, "is_template"):
parent_task_format = f"""<a href="#Form/Task/{self.parent_task}">{self.parent_task}</a>"""
parent_task_format = f"""<a href="/app/task/{self.parent_task}">{self.parent_task}</a>"""
frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
def validate_depends_on_tasks(self):
if self.depends_on:
for task in self.depends_on:
if not frappe.db.get_value("Task", task.task, "is_template"):
dependent_task_format = f"""<a href="#Form/Task/{task.task}">{task.task}</a>"""
dependent_task_format = f"""<a href="/app/task/{task.task}">{task.task}</a>"""
frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
def validate_completed_on(self):

View File

@@ -160,7 +160,7 @@ erpnext.accounts.taxes = {
let tax = frappe.get_doc(cdt, cdn);
try {
me.validate_taxes_and_charges(cdt, cdn);
me.validate_inclusive_tax(tax);
me.validate_inclusive_tax(tax, frm);
} catch(e) {
tax.included_in_print_rate = 0;
refresh_field("included_in_print_rate", tax.name, tax.parentfield);
@@ -170,7 +170,8 @@ erpnext.accounts.taxes = {
});
},
validate_inclusive_tax: function(tax) {
validate_inclusive_tax: function(tax, frm) {
this.frm = this.frm || frm;
let actual_type_error = function() {
var msg = __("Actual type tax cannot be included in Item rate in row {0}", [tax.idx])
frappe.throw(msg);
@@ -186,12 +187,12 @@ erpnext.accounts.taxes = {
if(tax.charge_type == "Actual") {
// inclusive tax cannot be of type Actual
actual_type_error();
} else if(tax.charge_type == "On Previous Row Amount" &&
} else if(tax.charge_type == "On Previous Row Amount" && this.frm &&
!cint(this.frm.doc["taxes"][tax.row_id - 1].included_in_print_rate)
) {
// referred row should also be an inclusive tax
on_previous_row_error(tax.row_id);
} else if(tax.charge_type == "On Previous Row Total") {
} else if(tax.charge_type == "On Previous Row Total" && this.frm) {
var taxes_not_included = $.map(this.frm.doc["taxes"].slice(0, tax.row_id),
function(t) { return cint(t.included_in_print_rate) ? null : t; });
if(taxes_not_included.length > 0) {

View File

@@ -103,7 +103,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
this.determine_exclusive_rate();
this.calculate_net_total();
this.calculate_taxes();
this.manipulate_grand_total_for_inclusive_tax();
this.adjust_grand_total_for_inclusive_tax();
this.calculate_totals();
this._cleanup();
}
@@ -185,7 +185,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (!this.discount_amount_applied) {
erpnext.accounts.taxes.validate_taxes_and_charges(tax.doctype, tax.name);
erpnext.accounts.taxes.validate_inclusive_tax(tax);
erpnext.accounts.taxes.validate_inclusive_tax(tax, this.frm);
}
frappe.model.round_floats_in(tax);
});
@@ -250,7 +250,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if(!me.discount_amount_applied && item.qty && (total_inclusive_tax_amount_per_qty || cumulated_tax_fraction)) {
var amount = flt(item.amount) - total_inclusive_tax_amount_per_qty;
item.net_amount = flt(amount / (1 + cumulated_tax_fraction));
item.net_amount = flt(amount / (1 + cumulated_tax_fraction), precision("net_amount", item));
item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0;
me.set_in_company_currency(item, ["net_rate", "net_amount"]);
@@ -305,6 +305,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
me.frm.doc.net_total += item.net_amount;
me.frm.doc.base_net_total += item.base_net_amount;
});
frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]);
}
calculate_shipping_charges() {
@@ -523,7 +525,15 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
}
/**
* @deprecated Use adjust_grand_total_for_inclusive_tax instead.
*/
manipulate_grand_total_for_inclusive_tax() {
// for backward compatablility - if in case used by an external application
this.adjust_grand_total_for_inclusive_tax()
}
adjust_grand_total_for_inclusive_tax() {
var me = this;
// if fully inclusive taxes and diff
if (this.frm.doc["taxes"] && this.frm.doc["taxes"].length) {
@@ -550,7 +560,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
diff = flt(diff, precision("rounding_adjustment"));
if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) {
me.frm.doc.rounding_adjustment = diff;
me.frm.doc.grand_total_diff = diff;
} else {
me.frm.doc.grand_total_diff = 0;
}
}
}
@@ -561,7 +573,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
var me = this;
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0;
this.frm.doc.grand_total = flt(tax_count
? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.rounding_adjustment)
? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.grand_total_diff)
: this.frm.doc.net_total);
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
@@ -621,7 +633,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if(frappe.meta.get_docfield(this.frm.doc.doctype, "rounded_total", this.frm.doc.name)) {
this.frm.doc.rounded_total = round_based_on_smallest_currency_fraction(this.frm.doc.grand_total,
this.frm.doc.currency, precision("rounded_total"));
this.frm.doc.rounding_adjustment += flt(this.frm.doc.rounded_total - this.frm.doc.grand_total,
this.frm.doc.rounding_adjustment = flt(this.frm.doc.rounded_total - this.frm.doc.grand_total,
precision("rounding_adjustment"));
this.set_in_company_currency(this.frm.doc, ["rounding_adjustment", "rounded_total"]);
@@ -689,8 +701,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (total_for_discount_amount) {
$.each(this.frm._items || [], function(i, item) {
distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
item.net_amount = flt(item.net_amount - distributed_amount,
precision("base_amount", item));
item.net_amount = flt(item.net_amount - distributed_amount, precision("net_amount", item));
net_total += item.net_amount;
// discount amount rounding loss adjustment if no taxes
@@ -833,13 +844,13 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
);
}
if(!this.frm.doc.is_return){
this.frm.doc.payments.find(payment => {
if (payment.default) {
payment.amount = total_amount_to_pay;
}
});
}
this.frm.doc.payments.find(payment => {
if (payment.default) {
payment.amount = total_amount_to_pay;
} else {
payment.amount = 0
}
});
this.frm.refresh_fields();
}

View File

@@ -956,9 +956,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doctype)) {
erpnext.utils.get_shipping_address(this.frm, function() {
set_party_account(set_pricing);
});
let is_drop_ship = me.frm.doc.items.some(item => item.delivered_by_supplier);
if (!is_drop_ship) {
console.log('get_shipping_address');
erpnext.utils.get_shipping_address(this.frm, function() {
set_party_account(set_pricing);
});
}
} else {
set_party_account(set_pricing);
@@ -2446,7 +2451,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
payment_terms_template() {
var me = this;
const doc = this.frm.doc;
if(doc.payment_terms_template && doc.doctype !== 'Delivery Note' && doc.is_return == 0) {
if(doc.payment_terms_template && doc.doctype !== 'Delivery Note' && !doc.is_return) {
var posting_date = doc.posting_date || doc.transaction_date;
frappe.call({
method: "erpnext.controllers.accounts_controller.get_payment_terms",

View File

@@ -437,6 +437,11 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
fieldname: "batch_no",
label: __("Batch No"),
in_list_view: 1,
get_route_options_for_new_doc: () => {
return {
item: this.item.item_code,
};
},
change() {
let doc = this.doc;
if (!doc.qty && me.item.type_of_transaction === "Outward") {
@@ -457,6 +462,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
is_inward = true;
}
let include_expired_batches = me.include_expired_batches();
return {
query: "erpnext.controllers.queries.get_batch_no",
filters: {
@@ -464,6 +471,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
warehouse:
this.item.s_warehouse || this.item.t_warehouse || this.item.warehouse,
is_inward: is_inward,
include_expired_batches: include_expired_batches,
},
};
},
@@ -492,6 +500,14 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
return fields;
}
include_expired_batches() {
return (
this.frm.doc.doctype === "Stock Reconciliation" ||
(this.frm.doc.doctype === "Stock Entry" &&
["Material Receipt", "Material Transfer", "Material Issue"].includes(this.frm.doc.purpose))
);
}
get_auto_data() {
let { qty, based_on } = this.dialog.get_values();

View File

@@ -1402,9 +1402,17 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
target.payment_schedule = []
if is_drop_ship_order(target):
target.customer = source.customer
target.customer_name = source.customer_name
target.shipping_address = source.shipping_address_name
if source.shipping_address_name:
target.shipping_address = source.shipping_address_name
target.shipping_address_display = source.shipping_address
else:
target.shipping_address = source.customer_address
target.shipping_address_display = source.address_display
target.customer_contact_person = source.contact_person
target.customer_contact_display = source.contact_display
target.customer_contact_mobile = source.contact_mobile
target.customer_contact_email = source.contact_email
else:
target.customer = target.customer_name = target.shipping_address = None

View File

@@ -35,6 +35,7 @@ def search_by_term(search_term, warehouse, price_list):
"description": item_doc.description,
"is_stock_item": item_doc.is_stock_item,
"item_code": item_doc.name,
"item_group": item_doc.item_group,
"item_image": item_doc.image,
"item_name": item_doc.item_name,
"serial_no": serial_no,
@@ -92,6 +93,12 @@ def search_by_term(search_term, warehouse, price_list):
return {"items": [item]}
def filter_result_items(result, pos_profile):
if result and result.get("items"):
pos_item_groups = frappe.db.get_all("POS Item Group", {"parent": pos_profile}, pluck="item_group")
result["items"] = [item for item in result.get("items") if item.get("item_group") in pos_item_groups]
@frappe.whitelist()
def get_items(start, page_length, price_list, item_group, pos_profile, search_term=""):
warehouse, hide_unavailable_items = frappe.db.get_value(
@@ -102,6 +109,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
if search_term:
result = search_by_term(search_term, warehouse, price_list) or []
filter_result_items(result, pos_profile)
if result:
return result
@@ -159,6 +167,8 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
if not items_data:
return result
current_date = frappe.utils.today()
for item in items_data:
uoms = frappe.get_doc("Item", item.item_code).get("uoms", [])
@@ -167,12 +177,16 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
item_price = frappe.get_all(
"Item Price",
fields=["price_list_rate", "currency", "uom", "batch_no"],
fields=["price_list_rate", "currency", "uom", "batch_no", "valid_from", "valid_upto"],
filters={
"price_list": price_list,
"item_code": item.item_code,
"selling": True,
"valid_from": ["<=", current_date],
"valid_upto": ["in", [None, "", current_date]],
},
order_by="valid_from desc",
limit=1,
)
if not item_price:

View File

@@ -99,7 +99,7 @@ erpnext.PointOfSale.ItemSelector = class {
return `<div class="item-qty-pill">
<span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span>
</div>
<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
<div class="flex items-center justify-center border-b-grey text-6xl text-grey-100" style="height:8rem; min-height:8rem">
<img
onerror="cur_pos.item_selector.handle_broken_image(this)"
class="h-full item-img" src="${item_image}"
@@ -138,7 +138,6 @@ erpnext.PointOfSale.ItemSelector = class {
make_search_bar() {
const me = this;
const doc = me.events.get_frm().doc;
this.$component.find(".search-field").html("");
this.$component.find(".item-group-field").html("");
@@ -163,6 +162,7 @@ erpnext.PointOfSale.ItemSelector = class {
me.filter_items();
},
get_query: function () {
const doc = me.events.get_frm().doc;
return {
query: "erpnext.selling.page.point_of_sale.point_of_sale.item_group_query",
filters: {

View File

@@ -449,18 +449,355 @@
},
"France": {
"France VAT 20%": {
"account_name": "VAT 20%",
"tax_rate": 20,
"default": 1
},
"France VAT 10%": {
"account_name": "VAT 10%",
"tax_rate": 10
},
"France VAT 5.5%": {
"account_name": "VAT 5.5%",
"tax_rate": 5.5
"chart_of_accounts": {
"France - Plan Comptable General avec code": {
"sales_tax_templates": [
{
"title": "TVA 20% Collectée",
"tax_category": "Vente Domestique",
"taxes": [
{
"account_head": {
"account_name": "TVA 20% Collectée",
"account_number": "445720",
"root_type": "Liability",
"tax_rate": 20.0
},
"description": "TVA 20%",
"rate": 20
}
]
},
{
"title": "TVA 10% Collectée",
"tax_category": "Vente Domestique",
"taxes": [
{
"account_head": {
"account_name": "TVA 10% Collectée",
"account_number": "445710",
"root_type": "Liability",
"tax_rate": 10.0
},
"description": "TVA 10%",
"rate": 10
}
]
},
{
"title": "TVA 5.5% Collectée",
"tax_category": "Vente Domestique",
"taxes": [
{
"account_head": {
"account_name": "TVA 5.5% Collectée",
"account_number": "445755",
"root_type": "Liability",
"tax_rate": 5.5
},
"description": "TVA 5.5%",
"rate": 5.5
}
]
},
{
"title": "TVA 2.1% Collectée",
"tax_category": "Vente Domestique",
"taxes": [
{
"account_head": {
"account_name": "TVA 2.1% Collectée",
"account_number": "445721",
"root_type": "Liability",
"tax_rate": 2.10
},
"description": "TVA 2.1%",
"rate": 2.1
}
]
}
],
"purchase_tax_templates": [
{
"title": "TVA 20% Déductible",
"tax_category": "Achat Domestique",
"taxes": [
{
"account_head": {
"account_name": "TVA 20% Déductible",
"account_number": "445620",
"root_type": "Asset",
"tax_rate": 20.0
},
"description": "TVA 20%",
"rate": 20
}
]
},
{
"title": "TVA 10% Déductible",
"tax_category": "Achat Domestique",
"taxes": [
{
"account_head": {
"account_name": "TVA 10% Déductible",
"account_number": "445610",
"root_type": "Asset",
"tax_rate": 10.0
},
"description": "TVA 10%",
"rate": 10
}
]
},
{
"title": "TVA 5.5% Déductible",
"tax_category": "Achat Domestique",
"taxes": [
{
"account_head": {
"account_name": "TVA 5.5% Déductible",
"account_number": "445655",
"root_type": "Asset",
"tax_rate": 5.5
},
"description": "TVA 5.5%",
"rate": 5.5
}
]
},
{
"title": "TVA 2.1% Déductible",
"tax_category": "Achat Domestique",
"taxes": [
{
"account_head": {
"account_name": "TVA 2.1% Déductible",
"account_number": "445621",
"root_type": "Asset",
"tax_rate": 2.1
},
"description": "TVA 2.1%",
"rate": 2.1
}
]
},
{
"title": "TVA 20% Déductible - Incluse dans le prix",
"taxes": [
{
"account_head": {
"account_name": "TVA 20% Déductible",
"account_number": "445620",
"root_type": "Asset",
"tax_rate": 20.0
},
"included_in_print_rate": 1,
"description": "TVA 20%",
"rate": 20
}
]
},
{
"title": "TVA 10% Déductible - Incluse dans le prix",
"taxes": [
{
"account_head": {
"account_name": "TVA 10% Déductible",
"account_number": "445610",
"root_type": "Asset",
"tax_rate": 10.0
},
"included_in_print_rate": 1,
"description": "TVA 10%",
"rate": 10
}
]
},
{
"title": "TVA 5.5% Déductible - Incluse dans le prix",
"taxes": [
{
"account_head": {
"account_name": "TVA 5.5% Déductible",
"account_number": "445655",
"root_type": "Asset",
"tax_rate": 5.5
},
"included_in_print_rate": 1,
"description": "TVA 5.5%",
"rate": 5.5
}
]
},
{
"title": "TVA 2.1% Déductible - Incluse dans le prix",
"taxes": [
{
"account_head": {
"account_name": "TVA 2.1% Déductible",
"account_number": "445621",
"root_type": "Asset",
"tax_rate": 2.1
},
"included_in_print_rate": 1,
"description": "TVA 2.1%",
"rate": 2.1
}
]
},
{
"title": "TVA Intracommunautaire",
"tax_category": "Achat - EU",
"taxes": [
{
"account_head": {
"account_name": "TVA déductible sur acquisition intracommunautaires",
"account_number": "445662",
"root_type": "Asset",
"tax_rate": 20.0,
"add_deduct_tax": "Add"
},
"description": "TVA déductible sur acquisition intracommunautaires",
"rate": 20
},
{
"account_head": {
"account_name": "TVA due intracommunautaire",
"account_number": "445200",
"root_type": "Asset",
"tax_rate": 20.0,
"add_deduct_tax": "Deduct"
},
"description": "TVA due intracommunautaire",
"rate": 20
}
]
}
],
"item_tax_templates": [
{
"title": "TVA 20% Déductible - Achat",
"taxes": [
{
"tax_type": {
"account_name": "TVA 20% Déductible",
"account_number": "445620",
"root_type": "Asset",
"tax_rate": 20.0
},
"description": "TVA 20%",
"tax_rate": 20
}
]
},
{
"title": "TVA 10% Déductible - Achat",
"taxes": [
{
"tax_type": {
"account_name": "TVA 10% Déductible",
"account_number": "445610",
"root_type": "Asset",
"tax_rate": 10.0
},
"description": "TVA 10%",
"tax_rate": 10
}
]
},
{
"title": "TVA 5.5% Déductible - Achat",
"taxes": [
{
"tax_type": {
"account_name": "TVA 5.5% Déductible",
"account_number": "445655",
"root_type": "Asset",
"tax_rate": 5.5
},
"description": "TVA 5.5%",
"tax_rate": 5.5
}
]
},
{
"title": "TVA 2.1% Déductible - Achat",
"taxes": [
{
"tax_type": {
"account_name": "TVA 2.1% Déductible",
"account_number": "445621",
"root_type": "Asset",
"tax_rate": 2.1
},
"description": "TVA 2.1%",
"tax_rate": 2.1
}
]
},
{
"title": "TVA 20% Collecté - Vente",
"taxes": [
{
"tax_type": {
"account_name": "TVA 20% Collecté",
"account_number": "445720",
"root_type": "Liability",
"tax_rate": 20.0
},
"description": "TVA 20%",
"tax_rate": 20
}
]
},
{
"title": "TVA 10% Collecté - Vente",
"taxes": [
{
"tax_type": {
"account_name": "TVA 10% Collecté",
"account_number": "445710",
"root_type": "Liability",
"tax_rate": 10.0
},
"description": "TVA 10%",
"tax_rate": 10
}
]
},
{
"title": "TVA 5.5% Collecté - Vente",
"taxes": [
{
"tax_type": {
"account_name": "TVA 5.5% Collecté",
"account_number": "445755",
"root_type": "Liability",
"tax_rate": 5.5
},
"description": "TVA 5.5%",
"tax_rate": 5.5
}
]
},
{
"title": "TVA 2.1% Collecté - Vente",
"taxes": [
{
"tax_type": {
"account_name": "TVA 2.1% Collecté",
"account_number": "445721",
"root_type": "Liability",
"tax_rate": 2.1
},
"description": "TVA 2.1%",
"tax_rate": 2.1
}
]
}
]
}
}
},

View File

@@ -86,7 +86,10 @@ def simple_to_detailed(templates):
def from_detailed_data(company_name, data):
"""Create Taxes and Charges Templates from detailed data."""
coa_name = frappe.db.get_value("Company", company_name, "chart_of_accounts")
charts_company_name = company_name
if frappe.db.get_value("Company", company_name, "create_chart_of_accounts_based_on"):
charts_company_name = frappe.db.get_value("Company", company_name, "existing_company")
coa_name = frappe.db.get_value("Company", charts_company_name, "chart_of_accounts")
coa_data = data.get("chart_of_accounts", {})
tax_templates = coa_data.get(coa_name) or coa_data.get("*", {})
tax_categories = data.get("tax_categories")

View File

@@ -16,6 +16,7 @@ from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import InventoryDimensionNegativeStockError
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
@@ -426,39 +427,49 @@ class TestInventoryDimension(FrappeTestCase):
warehouse = create_warehouse("Negative Stock Warehouse")
# Try issuing 10 qty, more than available stock against inventory dimension
doc = make_stock_entry(item_code=item_code, source=warehouse, qty=10, do_not_submit=True)
doc.items[0].inv_site = "Site 1"
self.assertRaises(frappe.ValidationError, doc.submit)
self.assertRaises(InventoryDimensionNegativeStockError, doc.submit)
# cancel the stock entry
doc.reload()
if doc.docstatus == 1:
doc.cancel()
# Receive 10 qty against inventory dimension
doc = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True)
doc.items[0].to_inv_site = "Site 1"
doc.submit()
# check inventory dimension value in stock ledger entry
site_name = frappe.get_all(
"Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"]
)[0].inv_site
self.assertEqual(site_name, "Site 1")
# Receive another 100 qty without inventory dimension
doc = make_stock_entry(item_code=item_code, target=warehouse, qty=100)
# Try issuing 100 qty, more than available stock against inventory dimension
# Note: total available qty for the item is 110, but against inventory dimension, only 10 qty is available
doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True)
doc.items[0].inv_site = "Site 1"
self.assertRaises(frappe.ValidationError, doc.submit)
self.assertRaises(InventoryDimensionNegativeStockError, doc.submit)
# disable validate_negative_stock for inventory dimension
inv_dimension.reload()
inv_dimension.db_set("validate_negative_stock", 0)
frappe.local.inventory_dimensions = {}
# Try issuing 100 qty, more than available stock against inventory dimension
doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True)
doc.items[0].inv_site = "Site 1"
doc.submit()
self.assertEqual(doc.docstatus, 1)
# check inventory dimension value in stock ledger entry
site_name = frappe.get_all(
"Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"]
)[0].inv_site

View File

@@ -109,7 +109,7 @@ class PickList(Document):
"actual_qty",
)
if row.qty > bin_qty:
if row.qty > flt(bin_qty):
frappe.throw(
_(
"At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}."

View File

@@ -889,7 +889,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "\nDraft\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed",
"options": "\nDraft\nPartly Billed\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed",
"print_hide": 1,
"print_width": "150px",
"read_only": 1,
@@ -1273,7 +1273,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
"modified": "2024-07-04 14:50:10.538472",
"modified": "2024-11-13 16:55:14.129055",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",

View File

@@ -112,7 +112,9 @@ class PurchaseReceipt(BuyingController):
shipping_address: DF.Link | None
shipping_address_display: DF.SmallText | None
shipping_rule: DF.Link | None
status: DF.Literal["", "Draft", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed"]
status: DF.Literal[
"", "Draft", "Partly Billed", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed"
]
subcontracting_receipt: DF.Link | None
supplied_items: DF.Table[PurchaseReceiptItemSupplied]
supplier: DF.Link
@@ -1059,6 +1061,8 @@ def get_billed_amount_against_po(po_items):
def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False):
# Update Billing % based on pending accepted qty
buying_settings = frappe.get_single("Buying Settings")
total_amount, total_billed_amount = 0, 0
item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
@@ -1066,10 +1070,15 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
returned_qty = flt(item_wise_returned_qty.get(item.name))
returned_amount = flt(returned_qty) * flt(item.rate)
pending_amount = flt(item.amount) - returned_amount
total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt
if buying_settings.bill_for_rejected_quantity_in_purchase_invoice:
pending_amount = flt(item.amount)
total_billable_amount = abs(flt(item.amount))
if pending_amount > 0:
total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt
total_amount += total_billable_amount
total_billed_amount += flt(item.billed_amt)
total_billed_amount += abs(flt(item.billed_amt))
if pr_doc.get("is_return") and not total_amount and total_billed_amount:
total_amount = total_billed_amount

View File

@@ -3900,6 +3900,54 @@ class TestPurchaseReceipt(FrappeTestCase):
for incoming_rate in bundle_data:
self.assertEqual(incoming_rate, 0)
def test_purchase_return_partial_debit_note(self):
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1",
supplier_warehouse="Work In Progress - TCP1",
)
return_pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1",
supplier_warehouse="Work In Progress - TCP1",
is_return=1,
return_against=pr.name,
qty=-2,
do_not_submit=1,
)
return_pr.items[0].purchase_receipt_item = pr.items[0].name
return_pr.submit()
# because new_doc isn't considering is_return portion of status_updater
returned = frappe.get_doc("Purchase Receipt", return_pr.name)
returned.update_prevdoc_status()
pr.load_from_db()
# Check if Original PR updated
self.assertEqual(pr.items[0].returned_qty, 2)
self.assertEqual(pr.per_returned, 40)
# Create first partial debit_note
pi_1 = make_purchase_invoice(return_pr.name)
pi_1.items[0].qty = -1
pi_1.submit()
# Check if the first partial debit billing percentage got updated
return_pr.reload()
self.assertEqual(return_pr.per_billed, 50)
self.assertEqual(return_pr.status, "Partly Billed")
# Create second partial debit_note to complete the debit note
pi_2 = make_purchase_invoice(return_pr.name)
pi_2.items[0].qty = -1
pi_2.submit()
# Check if the second partial debit note billing percentage got updated
return_pr.reload()
self.assertEqual(return_pr.per_billed, 100)
self.assertEqual(return_pr.status, "Completed")
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -689,6 +689,9 @@ class SerialandBatchBundle(Document):
serial_batches = {}
for row in self.entries:
if not row.qty and row.batch_no and not row.serial_no:
if self.voucher_type == "Stock Reconciliation" and self.type_of_transaction == "Inward":
continue
frappe.throw(
_("At row {0}: Qty is mandatory for the batch {1}").format(
bold(row.idx), bold(row.batch_no)

View File

@@ -117,6 +117,10 @@ frappe.ui.form.on("Stock Entry", {
filters["is_inward"] = 1;
}
if (["Material Receipt", "Material Transfer", "Material Issue"].includes(doc.purpose)) {
filters["include_expired_batches"] = 1;
}
return {
query: "erpnext.controllers.queries.get_batch_no",
filters: filters,

View File

@@ -795,9 +795,6 @@ class StockEntry(StockController):
self.set_total_incoming_outgoing_value()
self.set_total_amount()
if not reset_outgoing_rate:
self.set_serial_and_batch_bundle()
def set_basic_rate(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
"""
Set rate for outgoing, scrapped and finished items

View File

@@ -8,6 +8,7 @@ import frappe
from frappe import _, bold
from frappe.core.doctype.role.role import get_users
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate
from erpnext.accounts.utils import get_fiscal_year
@@ -25,6 +26,10 @@ class BackDatedStockTransaction(frappe.ValidationError):
pass
class InventoryDimensionNegativeStockError(frappe.ValidationError):
pass
exclude_from_linked_with = True
@@ -104,61 +109,56 @@ class StockLedgerEntry(Document):
self.posting_datetime = get_combine_datetime(self.posting_date, self.posting_time)
def validate_inventory_dimension_negative_stock(self):
if self.is_cancelled:
if self.is_cancelled or self.actual_qty >= 0:
return
extra_cond = ""
kwargs = {}
dimensions = self._get_inventory_dimensions()
if not dimensions:
return
for dimension, values in dimensions.items():
kwargs[dimension] = values.get("value")
extra_cond += f" and {dimension} = %({dimension})s"
kwargs.update(
{
"item_code": self.item_code,
"warehouse": self.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"company": self.company,
"sle": self.name,
}
)
sle = get_previous_sle(kwargs, extra_cond=extra_cond)
qty_after_transaction = 0.0
flt_precision = cint(frappe.db.get_default("float_precision")) or 2
if sle:
qty_after_transaction = sle.qty_after_transaction
for dimension, values in dimensions.items():
dimension_value = values.get("value")
available_qty = self.get_available_qty_after_prev_transaction(dimension, dimension_value)
diff = qty_after_transaction + flt(self.actual_qty)
diff = flt(diff, flt_precision)
if diff < 0 and abs(diff) > 0.0001:
self.throw_validation_error(diff, dimensions)
diff = flt(available_qty + flt(self.actual_qty), flt_precision) # qty after current transaction
if diff < 0 and abs(diff) > 0.0001:
self.throw_validation_error(diff, dimension, dimension_value)
def throw_validation_error(self, diff, dimensions):
dimension_msg = _(", with the inventory {0}: {1}").format(
"dimensions" if len(dimensions) > 1 else "dimension",
", ".join(f"{bold(d.doctype)} ({d.value})" for k, d in dimensions.items()),
)
def get_available_qty_after_prev_transaction(self, dimension, dimension_value):
sle = frappe.qb.DocType("Stock Ledger Entry")
available_qty = (
frappe.qb.from_(sle)
.select(Sum(sle.actual_qty))
.where(
(sle.item_code == self.item_code)
& (sle.warehouse == self.warehouse)
& (sle.posting_datetime < self.posting_datetime)
& (sle.company == self.company)
& (sle.is_cancelled == 0)
& (sle[dimension] == dimension_value)
)
).run()
return available_qty[0][0] or 0
def throw_validation_error(self, diff, dimension, dimension_value):
msg = _(
"{0} units of {1} are required in {2}{3}, on {4} {5} for {6} to complete the transaction."
"{0} units of {1} are required in {2} with the inventory dimension: {3} ({4}) on {5} {6} for {7} to complete the transaction."
).format(
abs(diff),
frappe.get_desk_link("Item", self.item_code),
frappe.get_desk_link("Warehouse", self.warehouse),
dimension_msg,
frappe.bold(dimension),
frappe.bold(dimension_value),
self.posting_date,
self.posting_time,
frappe.get_desk_link(self.voucher_type, self.voucher_no),
)
frappe.throw(msg, title=_("Inventory Dimension Negative Stock"))
frappe.throw(
msg, title=_("Inventory Dimension Negative Stock"), exc=InventoryDimensionNegativeStockError
)
def _get_inventory_dimensions(self):
inv_dimensions = get_inventory_dimensions()

View File

@@ -324,6 +324,7 @@ class StockReconciliation(StockController):
row.item_code,
posting_date=self.posting_date,
posting_time=self.posting_time,
for_stock_levels=True,
)
total_current_qty += current_qty
@@ -1322,7 +1323,16 @@ def get_stock_balance_for(
qty, rate = data
if item_dict.get("has_batch_no"):
qty = get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0
qty = (
get_batch_qty(
batch_no,
warehouse,
posting_date=posting_date,
posting_time=posting_time,
for_stock_levels=True,
)
or 0
)
return {
"qty": qty,

View File

@@ -297,6 +297,7 @@ def get_item_map(item_code, include_uom):
if include_uom:
ucd = frappe.qb.DocType("UOM Conversion Detail")
query = query.select(ucd.conversion_factor)
query = query.left_join(ucd).on((ucd.parent == item.name) & (ucd.uom == include_uom))
items = query.run(as_dict=True)

View File

@@ -124,7 +124,7 @@ class SerialBatchBundle:
"Outward": self.sle.actual_qty < 0,
}.get(sn_doc.type_of_transaction)
if not condition:
if not condition and self.sle.actual_qty:
correct_type = "Inward"
if sn_doc.type_of_transaction == "Inward":
correct_type = "Outward"
@@ -133,7 +133,7 @@ class SerialBatchBundle:
frappe.throw(_(msg), title=_("Incorrect Type of Transaction"))
precision = sn_doc.precision("total_qty")
if flt(sn_doc.total_qty, precision) != flt(self.sle.actual_qty, precision):
if self.sle.actual_qty and flt(sn_doc.total_qty, precision) != flt(self.sle.actual_qty, precision):
msg = f"Total qty {flt(sn_doc.total_qty, precision)} of Serial and Batch Bundle {link} is not equal to Actual Qty {flt(self.sle.actual_qty, precision)} in the {self.sle.voucher_type} {self.sle.voucher_no}"
frappe.throw(_(msg))
@@ -288,7 +288,7 @@ class SerialBatchBundle:
"Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
)
if docstatus != 1:
if docstatus == 0:
self.submit_serial_and_batch_bundle()
if self.item_details.has_serial_no == 1:
@@ -311,7 +311,9 @@ class SerialBatchBundle:
if self.is_pos_transaction():
return
frappe.get_cached_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle).cancel()
doc = frappe.get_cached_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
if doc.docstatus == 1:
doc.cancel()
def is_pos_transaction(self):
if (

View File

@@ -1183,6 +1183,7 @@ class update_entries_after:
stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
stock_entry.db_update()
for d in stock_entry.items:
# Update only the row that matches the voucher_detail_no or the row containing the FG/Scrap Item.
if d.name == voucher_detail_no or (not d.s_warehouse and d.t_warehouse):
d.db_update()

View File

@@ -766,7 +766,11 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False
"postprocess": update_item,
"condition": lambda doc: doc.name in po_sr_item_dict,
},
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges",
"reset_value": True,
"condition": lambda doc: not doc.is_tax_withholding_account,
},
},
postprocess=post_process,
)