mirror of
https://github.com/frappe/erpnext.git
synced 2026-03-17 14:02:10 +00:00
Merge pull request #44103 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
16
CODEOWNERS
16
CODEOWNERS
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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
|
||||
|
||||
179
erpnext/accounts/report/sales_register/test_sales_register.py
Normal file
179
erpnext/accounts/report/sales_register/test_sales_register.py
Normal 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)
|
||||
@@ -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"):
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}."
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user