mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-06 15:00:27 +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,
|
# the repo. Unless a later match takes precedence,
|
||||||
|
|
||||||
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
|
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
|
||||||
erpnext/assets/ @anandbaburajan @deepeshgarg007
|
erpnext/assets/ @khushi8112 @deepeshgarg007
|
||||||
erpnext/regional @deepeshgarg007 @ruthra-kumar
|
erpnext/regional @deepeshgarg007 @ruthra-kumar
|
||||||
erpnext/selling @deepeshgarg007 @ruthra-kumar
|
erpnext/selling @deepeshgarg007 @ruthra-kumar
|
||||||
erpnext/support/ @deepeshgarg007
|
erpnext/support/ @deepeshgarg007
|
||||||
pos*
|
pos*
|
||||||
|
|
||||||
erpnext/buying/ @rohitwaghchaure @s-aga-r
|
erpnext/buying/ @rohitwaghchaure
|
||||||
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
|
erpnext/maintenance/ @rohitwaghchaure
|
||||||
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
|
erpnext/manufacturing/ @rohitwaghchaure
|
||||||
erpnext/quality_management/ @rohitwaghchaure @s-aga-r
|
erpnext/quality_management/ @rohitwaghchaure
|
||||||
erpnext/stock/ @rohitwaghchaure @s-aga-r
|
erpnext/stock/ @rohitwaghchaure
|
||||||
erpnext/subcontracting @rohitwaghchaure @s-aga-r
|
erpnext/subcontracting @rohitwaghchaure
|
||||||
|
|
||||||
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
|
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
|
||||||
erpnext/patches/ @deepeshgarg007
|
erpnext/patches/ @deepeshgarg007
|
||||||
|
|
||||||
.github/ @deepeshgarg007
|
.github/ @deepeshgarg007
|
||||||
pyproject.toml @phot0n
|
pyproject.toml @akhilnarang
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _, qb
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import flt, nowdate
|
from frappe.utils import flt, nowdate
|
||||||
@@ -564,11 +564,24 @@ def make_payment_request(**args):
|
|||||||
# fetches existing payment request `grand_total` amount
|
# fetches existing payment request `grand_total` amount
|
||||||
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
|
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
|
grand_total -= existing_payment_request_amount
|
||||||
|
|
||||||
if not grand_total:
|
if not grand_total:
|
||||||
frappe.throw(_("Payment Request is already created"))
|
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:
|
if draft_payment_request:
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
@@ -678,21 +691,65 @@ def get_amount(ref_doc, payment_account=None):
|
|||||||
frappe.throw(_("Payment Entry is already created"))
|
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.
|
Return the total amount of Payment Requests against a reference document.
|
||||||
"""
|
"""
|
||||||
PR = frappe.qb.DocType("Payment Request")
|
PR = frappe.qb.DocType("Payment Request")
|
||||||
|
|
||||||
response = (
|
query = (
|
||||||
frappe.qb.from_(PR)
|
frappe.qb.from_(PR)
|
||||||
.select(Sum(PR.grand_total))
|
.select(Sum(PR.grand_total))
|
||||||
.where(PR.reference_doctype == ref_dt)
|
.where(PR.reference_doctype == ref_dt)
|
||||||
.where(PR.reference_name == ref_dn)
|
.where(PR.reference_name == ref_dn)
|
||||||
.where(PR.docstatus == 1)
|
.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
|
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
|
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();
|
super.refresh();
|
||||||
|
|
||||||
if (doc.docstatus == 1 && !doc.is_return) {
|
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"));
|
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
inv.save()
|
inv.save()
|
||||||
|
|
||||||
self.assertEqual(inv.net_total, 4298.25)
|
self.assertEqual(inv.net_total, 4298.24)
|
||||||
self.assertEqual(inv.grand_total, 4900.00)
|
self.assertEqual(inv.grand_total, 4900.00)
|
||||||
|
|
||||||
def test_tax_calculation_with_multiple_items(self):
|
def test_tax_calculation_with_multiple_items(self):
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
|||||||
inv.load_from_db()
|
inv.load_from_db()
|
||||||
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
|
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
|
||||||
self.assertEqual(consolidated_invoice.status, "Return")
|
self.assertEqual(consolidated_invoice.status, "Return")
|
||||||
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001)
|
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.002)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|||||||
@@ -863,6 +863,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
|
|
||||||
self.make_tax_gl_entries(gl_entries)
|
self.make_tax_gl_entries(gl_entries)
|
||||||
self.make_internal_transfer_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)
|
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():
|
if grand_total and not self.is_internal_transfer():
|
||||||
against_voucher = self.name
|
self.add_supplier_gl_entry(gl_entries, base_grand_total, grand_total)
|
||||||
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
|
def add_supplier_gl_entry(
|
||||||
gl_entries.append(
|
self, gl_entries, base_grand_total, grand_total, against_account=None, remarks=None, skip_merge=False
|
||||||
self.get_gl_dict(
|
):
|
||||||
{
|
against_voucher = self.name
|
||||||
"account": self.credit_to,
|
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||||
"party_type": "Supplier",
|
against_voucher = self.return_against
|
||||||
"party": self.supplier,
|
|
||||||
"due_date": self.due_date,
|
# Did not use base_grand_total to book rounding loss gle
|
||||||
"against": self.against_expense_account,
|
gl = {
|
||||||
"credit": base_grand_total,
|
"account": self.credit_to,
|
||||||
"credit_in_account_currency": base_grand_total
|
"party_type": "Supplier",
|
||||||
if self.party_account_currency == self.company_currency
|
"party": self.supplier,
|
||||||
else grand_total,
|
"due_date": self.due_date,
|
||||||
"against_voucher": against_voucher,
|
"against": against_account or self.against_expense_account,
|
||||||
"against_voucher_type": self.doctype,
|
"credit": base_grand_total,
|
||||||
"project": self.project,
|
"credit_in_account_currency": base_grand_total
|
||||||
"cost_center": self.cost_center,
|
if self.party_account_currency == self.company_currency
|
||||||
},
|
else grand_total,
|
||||||
self.party_account_currency,
|
"against_voucher": against_voucher,
|
||||||
item=self,
|
"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):
|
def make_item_gl_entries(self, gl_entries):
|
||||||
# item 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):
|
def make_payment_gl_entries(self, gl_entries):
|
||||||
# Make Cash GL Entries
|
# Make Cash GL Entries
|
||||||
if cint(self.is_paid) and self.cash_bank_account and self.paid_amount:
|
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()
|
payment_entry.load_from_db()
|
||||||
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
|
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):
|
def test_provisional_accounting_entry(self):
|
||||||
setup_provisional_accounting()
|
setup_provisional_accounting()
|
||||||
|
|
||||||
|
|||||||
@@ -314,7 +314,8 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
si.insert()
|
si.insert()
|
||||||
|
|
||||||
# with inclusive tax
|
# 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.net_total, 3947.37)
|
||||||
self.assertEqual(si.grand_total, 5000)
|
self.assertEqual(si.grand_total, 5000)
|
||||||
|
|
||||||
@@ -658,7 +659,7 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
62.5,
|
62.5,
|
||||||
625.0,
|
625.0,
|
||||||
50,
|
50,
|
||||||
499.97600115194473,
|
499.98,
|
||||||
],
|
],
|
||||||
"_Test Item Home Desktop 200": [
|
"_Test Item Home Desktop 200": [
|
||||||
190.66,
|
190.66,
|
||||||
@@ -669,7 +670,7 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
190.66,
|
190.66,
|
||||||
953.3,
|
953.3,
|
||||||
150,
|
150,
|
||||||
749.9968530500239,
|
750,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,20 +683,21 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
self.assertEqual(d.get(k), expected_values[d.item_code][i])
|
self.assertEqual(d.get(k), expected_values[d.item_code][i])
|
||||||
|
|
||||||
# check net total
|
# 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)
|
self.assertEqual(si.total, 1578.3)
|
||||||
|
|
||||||
# check tax calculation
|
# check tax calculation
|
||||||
expected_values = {
|
expected_values = {
|
||||||
"keys": ["tax_amount", "total"],
|
"keys": ["tax_amount", "total"],
|
||||||
"_Test Account Excise Duty - _TC": [140, 1389.97],
|
"_Test Account Excise Duty - _TC": [140, 1389.98],
|
||||||
"_Test Account Education Cess - _TC": [2.8, 1392.77],
|
"_Test Account Education Cess - _TC": [2.8, 1392.78],
|
||||||
"_Test Account S&H Education Cess - _TC": [1.4, 1394.17],
|
"_Test Account S&H Education Cess - _TC": [1.4, 1394.18],
|
||||||
"_Test Account CST - _TC": [27.88, 1422.05],
|
"_Test Account CST - _TC": [27.88, 1422.06],
|
||||||
"_Test Account VAT - _TC": [156.25, 1578.30],
|
"_Test Account VAT - _TC": [156.25, 1578.31],
|
||||||
"_Test Account Customs Duty - _TC": [125, 1703.30],
|
"_Test Account Customs Duty - _TC": [125, 1703.31],
|
||||||
"_Test Account Shipping Charges - _TC": [100, 1803.30],
|
"_Test Account Shipping Charges - _TC": [100, 1803.31],
|
||||||
"_Test Account Discount - _TC": [-180.33, 1622.97],
|
"_Test Account Discount - _TC": [-180.33, 1622.98],
|
||||||
}
|
}
|
||||||
|
|
||||||
for d in si.get("taxes"):
|
for d in si.get("taxes"):
|
||||||
@@ -731,7 +733,7 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
"base_rate": 2500,
|
"base_rate": 2500,
|
||||||
"base_amount": 25000,
|
"base_amount": 25000,
|
||||||
"net_rate": 40,
|
"net_rate": 40,
|
||||||
"net_amount": 399.9808009215558,
|
"net_amount": 399.98,
|
||||||
"base_net_rate": 2000,
|
"base_net_rate": 2000,
|
||||||
"base_net_amount": 19999,
|
"base_net_amount": 19999,
|
||||||
},
|
},
|
||||||
@@ -745,7 +747,7 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
"base_rate": 7500,
|
"base_rate": 7500,
|
||||||
"base_amount": 37500,
|
"base_amount": 37500,
|
||||||
"net_rate": 118.01,
|
"net_rate": 118.01,
|
||||||
"net_amount": 590.0531205155963,
|
"net_amount": 590.05,
|
||||||
"base_net_rate": 5900.5,
|
"base_net_rate": 5900.5,
|
||||||
"base_net_amount": 29502.5,
|
"base_net_amount": 29502.5,
|
||||||
},
|
},
|
||||||
@@ -783,8 +785,13 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(si.base_grand_total, 60795)
|
self.assertEqual(si.base_grand_total, 60795)
|
||||||
self.assertEqual(si.grand_total, 1215.90)
|
self.assertEqual(si.grand_total, 1215.90)
|
||||||
self.assertEqual(si.rounding_adjustment, 0.01)
|
# no rounding adjustment as the Smallest Currency Fraction Value of USD is 0.01
|
||||||
self.assertEqual(si.base_rounding_adjustment, 0.50)
|
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):
|
def test_outstanding(self):
|
||||||
w = self.make()
|
w = self.make()
|
||||||
@@ -2172,7 +2179,7 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
|
|
||||||
def test_rounding_adjustment_2(self):
|
def test_rounding_adjustment_2(self):
|
||||||
si = create_sales_invoice(rate=400, do_not_save=True)
|
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(
|
si.append(
|
||||||
"items",
|
"items",
|
||||||
{
|
{
|
||||||
@@ -2198,18 +2205,19 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
si.save()
|
si.save()
|
||||||
si.submit()
|
si.submit()
|
||||||
self.assertEqual(si.net_total, 1271.19)
|
self.assertEqual(si.net_total, si.base_net_total)
|
||||||
self.assertEqual(si.grand_total, 1500)
|
self.assertEqual(si.net_total, 1272.20)
|
||||||
self.assertEqual(si.total_taxes_and_charges, 228.82)
|
self.assertEqual(si.grand_total, 1501.20)
|
||||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
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")
|
round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account")
|
||||||
expected_values = {
|
expected_values = {
|
||||||
"_Test Account Service Tax - _TC": [0.0, 114.41],
|
"_Test Account Service Tax - _TC": [0.0, 114.50],
|
||||||
"_Test Account VAT - _TC": [0.0, 114.41],
|
"_Test Account VAT - _TC": [0.0, 114.50],
|
||||||
si.debit_to: [1500, 0.0],
|
si.debit_to: [1501, 0.0],
|
||||||
round_off_account: [0.01, 0.01],
|
round_off_account: [0.20, 0.0],
|
||||||
"Sales - _TC": [0.0, 1271.18],
|
"Sales - _TC": [0.0, 1272.20],
|
||||||
}
|
}
|
||||||
|
|
||||||
gl_entries = frappe.db.sql(
|
gl_entries = frappe.db.sql(
|
||||||
@@ -2267,7 +2275,8 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
|
|
||||||
si.save()
|
si.save()
|
||||||
si.submit()
|
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.grand_total, 4488.02)
|
||||||
self.assertEqual(si.total_taxes_and_charges, 480.86)
|
self.assertEqual(si.total_taxes_and_charges, 480.86)
|
||||||
self.assertEqual(si.rounding_adjustment, -0.02)
|
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 Service Tax - _TC", 0.0, 240.43],
|
||||||
["_Test Account VAT - _TC", 0.0, 240.43],
|
["_Test Account VAT - _TC", 0.0, 240.43],
|
||||||
["Sales - _TC", 0.0, 4007.15],
|
["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)
|
self.assertEqual(pi.grand_total, 18000)
|
||||||
|
|
||||||
# check gl entry for the purchase invoice
|
# 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)
|
self.assertEqual(len(gl_entries), 3)
|
||||||
for d in gl_entries:
|
for d in gl_entries:
|
||||||
if d.account == pi.credit_to:
|
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"):
|
elif d.account == pi.items[0].get("expense_account"):
|
||||||
self.assertEqual(d.debit, 20000)
|
self.assertEqual(d.debit, 20000)
|
||||||
elif d.account == pi.taxes[0].get("account_head"):
|
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)
|
merge_properties = get_merge_properties(accounting_dimensions)
|
||||||
|
|
||||||
for entry in gl_map:
|
for entry in gl_map:
|
||||||
|
if entry._skip_merge:
|
||||||
|
merged_gl_map.append(entry)
|
||||||
|
continue
|
||||||
|
|
||||||
entry.merge_key = get_merge_key(entry, merge_properties)
|
entry.merge_key = get_merge_key(entry, merge_properties)
|
||||||
# if there is already an entry in this account then just add it
|
# if there is already an entry in this account then just add it
|
||||||
# to that entry
|
# 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)
|
.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)
|
journal_entries = query.run(as_dict=True)
|
||||||
return journal_entries
|
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)
|
query = query.where(parent_doc.posting_date <= filters.to_date)
|
||||||
|
|
||||||
if payments:
|
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)
|
query = query.where(parent_doc.cost_center == filters.cost_center)
|
||||||
else:
|
else:
|
||||||
if filters.get("cost_center"):
|
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,
|
stock_ledger_entry.batch_no,
|
||||||
Sum(stock_ledger_entry.actual_qty).as_("qty"),
|
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.is_cancelled == 0)
|
||||||
.where(
|
.where(
|
||||||
(stock_ledger_entry.item_code == filters.get("item_code"))
|
(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)
|
.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(
|
query = query.select(
|
||||||
Concat("MFG-", batch_table.manufacturing_date).as_("manufacturing_date"),
|
Concat("MFG-", batch_table.manufacturing_date).as_("manufacturing_date"),
|
||||||
Concat("EXP-", batch_table.expiry_date).as_("expiry_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,
|
bundle.batch_no,
|
||||||
Sum(bundle.qty).as_("qty"),
|
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.is_cancelled == 0)
|
||||||
.where(
|
.where(
|
||||||
(stock_ledger_entry.item_code == filters.get("item_code"))
|
(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)
|
.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(
|
bundle_query = bundle_query.select(
|
||||||
Concat("MFG-", batch_table.manufacturing_date),
|
Concat("MFG-", batch_table.manufacturing_date),
|
||||||
Concat("EXP-", batch_table.expiry_date),
|
Concat("EXP-", batch_table.expiry_date),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import frappe
|
|||||||
from frappe import _, scrub
|
from frappe import _, scrub
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
|
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
|
||||||
|
from frappe.utils.deprecations import deprecated
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate
|
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_net_total()
|
||||||
self.calculate_tax_withholding_net_total()
|
self.calculate_tax_withholding_net_total()
|
||||||
self.calculate_taxes()
|
self.calculate_taxes()
|
||||||
self.manipulate_grand_total_for_inclusive_tax()
|
self.adjust_grand_total_for_inclusive_tax()
|
||||||
self.calculate_totals()
|
self.calculate_totals()
|
||||||
self._cleanup()
|
self._cleanup()
|
||||||
self.calculate_total_net_weight()
|
self.calculate_total_net_weight()
|
||||||
@@ -286,7 +287,7 @@ class calculate_taxes_and_totals:
|
|||||||
):
|
):
|
||||||
amount = flt(item.amount) - total_inclusive_tax_amount_per_qty
|
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.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate"))
|
||||||
item.discount_percentage = flt(
|
item.discount_percentage = flt(
|
||||||
item.discount_percentage, item.precision("discount_percentage")
|
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 = round(tax.base_tax_amount, 0)
|
||||||
tax.base_tax_amount_after_discount_amount = round(tax.base_tax_amount_after_discount_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):
|
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 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")):
|
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]
|
last_tax = self.doc.get("taxes")[-1]
|
||||||
@@ -553,17 +559,21 @@ class calculate_taxes_and_totals:
|
|||||||
diff = flt(diff, self.doc.precision("rounding_adjustment"))
|
diff = flt(diff, self.doc.precision("rounding_adjustment"))
|
||||||
|
|
||||||
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
|
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):
|
def calculate_totals(self):
|
||||||
if self.doc.get("taxes"):
|
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:
|
else:
|
||||||
self.doc.grand_total = flt(self.doc.net_total)
|
self.doc.grand_total = flt(self.doc.net_total)
|
||||||
|
|
||||||
if self.doc.get("taxes"):
|
if self.doc.get("taxes"):
|
||||||
self.doc.total_taxes_and_charges = flt(
|
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"),
|
self.doc.precision("total_taxes_and_charges"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -626,8 +636,8 @@ class calculate_taxes_and_totals:
|
|||||||
self.doc.grand_total, self.doc.currency, self.doc.precision("rounded_total")
|
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
|
# rounding adjustment should always be the difference vetween grand and rounded total
|
||||||
self.doc.rounding_adjustment += flt(
|
self.doc.rounding_adjustment = flt(
|
||||||
self.doc.rounded_total - self.doc.grand_total, self.doc.precision("rounding_adjustment")
|
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
|
base_total_rm_cost = 0
|
||||||
|
|
||||||
for d in self.get("items"):
|
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
|
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(
|
d.rate = self.get_rm_rate(
|
||||||
{
|
{
|
||||||
"company": self.company,
|
"company": self.company,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ frappe.ui.form.on("Job Card", {
|
|||||||
frappe.flags.resume_job = 0;
|
frappe.flags.resume_job = 0;
|
||||||
let has_items = frm.doc.items && frm.doc.items.length;
|
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();
|
frm.disable_save();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,17 +87,17 @@ frappe.ui.form.on("Production Plan", {
|
|||||||
if (frm.doc.docstatus === 1) {
|
if (frm.doc.docstatus === 1) {
|
||||||
frm.trigger("show_progress");
|
frm.trigger("show_progress");
|
||||||
|
|
||||||
if (frm.doc.status !== "Completed") {
|
frm.add_custom_button(
|
||||||
frm.add_custom_button(
|
__("Production Plan Summary"),
|
||||||
__("Production Plan Summary"),
|
() => {
|
||||||
() => {
|
frappe.set_route("query-report", "Production Plan Summary", {
|
||||||
frappe.set_route("query-report", "Production Plan Summary", {
|
production_plan: frm.doc.name,
|
||||||
production_plan: frm.doc.name,
|
});
|
||||||
});
|
},
|
||||||
},
|
__("View")
|
||||||
__("View")
|
);
|
||||||
);
|
|
||||||
|
|
||||||
|
if (frm.doc.status !== "Completed") {
|
||||||
if (frm.doc.status === "Closed") {
|
if (frm.doc.status === "Closed") {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Re-open"),
|
__("Re-open"),
|
||||||
|
|||||||
@@ -27,32 +27,51 @@ def get_data(filters):
|
|||||||
|
|
||||||
|
|
||||||
def get_production_plan_item_details(filters, data, order_details):
|
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"))
|
production_plan_doc = frappe.get_cached_doc("Production Plan", filters.get("production_plan"))
|
||||||
for row in production_plan_doc.po_items:
|
for row in production_plan_doc.po_items:
|
||||||
work_order = frappe.get_value(
|
work_orders = frappe.get_all(
|
||||||
"Work Order",
|
"Work Order",
|
||||||
{"production_plan_item": row.name, "bom_no": row.bom_no, "production_item": row.item_code},
|
filters={
|
||||||
"name",
|
"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:
|
order_qty = row.planned_qty
|
||||||
itemwise_indent.setdefault(row.item_code, {})
|
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(
|
data.append(
|
||||||
{
|
{
|
||||||
"indent": 0,
|
|
||||||
"item_code": row.item_code,
|
"item_code": row.item_code,
|
||||||
"sales_order": row.get("sales_order"),
|
"indent": 0,
|
||||||
"item_name": frappe.get_cached_value("Item", row.item_code, "item_name"),
|
|
||||||
"qty": row.planned_qty,
|
"qty": row.planned_qty,
|
||||||
"document_type": "Work Order",
|
"produced_qty": total_produced_qty,
|
||||||
"document_name": work_order or "",
|
"pending_qty": pending_qty,
|
||||||
"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)),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -153,14 +153,14 @@ class Task(NestedSet):
|
|||||||
def validate_parent_template_task(self):
|
def validate_parent_template_task(self):
|
||||||
if self.parent_task:
|
if self.parent_task:
|
||||||
if not frappe.db.get_value("Task", self.parent_task, "is_template"):
|
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))
|
frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
|
||||||
|
|
||||||
def validate_depends_on_tasks(self):
|
def validate_depends_on_tasks(self):
|
||||||
if self.depends_on:
|
if self.depends_on:
|
||||||
for task in self.depends_on:
|
for task in self.depends_on:
|
||||||
if not frappe.db.get_value("Task", task.task, "is_template"):
|
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))
|
frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
|
||||||
|
|
||||||
def validate_completed_on(self):
|
def validate_completed_on(self):
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ erpnext.accounts.taxes = {
|
|||||||
let tax = frappe.get_doc(cdt, cdn);
|
let tax = frappe.get_doc(cdt, cdn);
|
||||||
try {
|
try {
|
||||||
me.validate_taxes_and_charges(cdt, cdn);
|
me.validate_taxes_and_charges(cdt, cdn);
|
||||||
me.validate_inclusive_tax(tax);
|
me.validate_inclusive_tax(tax, frm);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
tax.included_in_print_rate = 0;
|
tax.included_in_print_rate = 0;
|
||||||
refresh_field("included_in_print_rate", tax.name, tax.parentfield);
|
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() {
|
let actual_type_error = function() {
|
||||||
var msg = __("Actual type tax cannot be included in Item rate in row {0}", [tax.idx])
|
var msg = __("Actual type tax cannot be included in Item rate in row {0}", [tax.idx])
|
||||||
frappe.throw(msg);
|
frappe.throw(msg);
|
||||||
@@ -186,12 +187,12 @@ erpnext.accounts.taxes = {
|
|||||||
if(tax.charge_type == "Actual") {
|
if(tax.charge_type == "Actual") {
|
||||||
// inclusive tax cannot be of type Actual
|
// inclusive tax cannot be of type Actual
|
||||||
actual_type_error();
|
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)
|
!cint(this.frm.doc["taxes"][tax.row_id - 1].included_in_print_rate)
|
||||||
) {
|
) {
|
||||||
// referred row should also be an inclusive tax
|
// referred row should also be an inclusive tax
|
||||||
on_previous_row_error(tax.row_id);
|
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),
|
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; });
|
function(t) { return cint(t.included_in_print_rate) ? null : t; });
|
||||||
if(taxes_not_included.length > 0) {
|
if(taxes_not_included.length > 0) {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
this.determine_exclusive_rate();
|
this.determine_exclusive_rate();
|
||||||
this.calculate_net_total();
|
this.calculate_net_total();
|
||||||
this.calculate_taxes();
|
this.calculate_taxes();
|
||||||
this.manipulate_grand_total_for_inclusive_tax();
|
this.adjust_grand_total_for_inclusive_tax();
|
||||||
this.calculate_totals();
|
this.calculate_totals();
|
||||||
this._cleanup();
|
this._cleanup();
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
|
|
||||||
if (!this.discount_amount_applied) {
|
if (!this.discount_amount_applied) {
|
||||||
erpnext.accounts.taxes.validate_taxes_and_charges(tax.doctype, tax.name);
|
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);
|
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)) {
|
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;
|
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;
|
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"]);
|
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.net_total += item.net_amount;
|
||||||
me.frm.doc.base_net_total += item.base_net_amount;
|
me.frm.doc.base_net_total += item.base_net_amount;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
calculate_shipping_charges() {
|
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() {
|
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;
|
var me = this;
|
||||||
// if fully inclusive taxes and diff
|
// if fully inclusive taxes and diff
|
||||||
if (this.frm.doc["taxes"] && this.frm.doc["taxes"].length) {
|
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"));
|
diff = flt(diff, precision("rounding_adjustment"));
|
||||||
|
|
||||||
if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) {
|
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 me = this;
|
||||||
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0;
|
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0;
|
||||||
this.frm.doc.grand_total = flt(tax_count
|
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);
|
: this.frm.doc.net_total);
|
||||||
|
|
||||||
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
|
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)) {
|
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.rounded_total = round_based_on_smallest_currency_fraction(this.frm.doc.grand_total,
|
||||||
this.frm.doc.currency, precision("rounded_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"));
|
precision("rounding_adjustment"));
|
||||||
|
|
||||||
this.set_in_company_currency(this.frm.doc, ["rounding_adjustment", "rounded_total"]);
|
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) {
|
if (total_for_discount_amount) {
|
||||||
$.each(this.frm._items || [], function(i, item) {
|
$.each(this.frm._items || [], function(i, item) {
|
||||||
distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
|
distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
|
||||||
item.net_amount = flt(item.net_amount - distributed_amount,
|
item.net_amount = flt(item.net_amount - distributed_amount, precision("net_amount", item));
|
||||||
precision("base_amount", item));
|
|
||||||
net_total += item.net_amount;
|
net_total += item.net_amount;
|
||||||
|
|
||||||
// discount amount rounding loss adjustment if no taxes
|
// 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 => {
|
||||||
this.frm.doc.payments.find(payment => {
|
if (payment.default) {
|
||||||
if (payment.default) {
|
payment.amount = total_amount_to_pay;
|
||||||
payment.amount = total_amount_to_pay;
|
} else {
|
||||||
}
|
payment.amount = 0
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
this.frm.refresh_fields();
|
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") &&
|
if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
|
||||||
['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doctype)) {
|
['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doctype)) {
|
||||||
erpnext.utils.get_shipping_address(this.frm, function() {
|
let is_drop_ship = me.frm.doc.items.some(item => item.delivered_by_supplier);
|
||||||
set_party_account(set_pricing);
|
|
||||||
});
|
if (!is_drop_ship) {
|
||||||
|
console.log('get_shipping_address');
|
||||||
|
erpnext.utils.get_shipping_address(this.frm, function() {
|
||||||
|
set_party_account(set_pricing);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
set_party_account(set_pricing);
|
set_party_account(set_pricing);
|
||||||
@@ -2446,7 +2451,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
payment_terms_template() {
|
payment_terms_template() {
|
||||||
var me = this;
|
var me = this;
|
||||||
const doc = this.frm.doc;
|
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;
|
var posting_date = doc.posting_date || doc.transaction_date;
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.controllers.accounts_controller.get_payment_terms",
|
method: "erpnext.controllers.accounts_controller.get_payment_terms",
|
||||||
|
|||||||
@@ -437,6 +437,11 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
fieldname: "batch_no",
|
fieldname: "batch_no",
|
||||||
label: __("Batch No"),
|
label: __("Batch No"),
|
||||||
in_list_view: 1,
|
in_list_view: 1,
|
||||||
|
get_route_options_for_new_doc: () => {
|
||||||
|
return {
|
||||||
|
item: this.item.item_code,
|
||||||
|
};
|
||||||
|
},
|
||||||
change() {
|
change() {
|
||||||
let doc = this.doc;
|
let doc = this.doc;
|
||||||
if (!doc.qty && me.item.type_of_transaction === "Outward") {
|
if (!doc.qty && me.item.type_of_transaction === "Outward") {
|
||||||
@@ -457,6 +462,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
is_inward = true;
|
is_inward = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let include_expired_batches = me.include_expired_batches();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query: "erpnext.controllers.queries.get_batch_no",
|
query: "erpnext.controllers.queries.get_batch_no",
|
||||||
filters: {
|
filters: {
|
||||||
@@ -464,6 +471,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
warehouse:
|
warehouse:
|
||||||
this.item.s_warehouse || this.item.t_warehouse || this.item.warehouse,
|
this.item.s_warehouse || this.item.t_warehouse || this.item.warehouse,
|
||||||
is_inward: is_inward,
|
is_inward: is_inward,
|
||||||
|
include_expired_batches: include_expired_batches,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -492,6 +500,14 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
return fields;
|
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() {
|
get_auto_data() {
|
||||||
let { qty, based_on } = this.dialog.get_values();
|
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 = []
|
target.payment_schedule = []
|
||||||
|
|
||||||
if is_drop_ship_order(target):
|
if is_drop_ship_order(target):
|
||||||
target.customer = source.customer
|
if source.shipping_address_name:
|
||||||
target.customer_name = source.customer_name
|
target.shipping_address = 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:
|
else:
|
||||||
target.customer = target.customer_name = target.shipping_address = None
|
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,
|
"description": item_doc.description,
|
||||||
"is_stock_item": item_doc.is_stock_item,
|
"is_stock_item": item_doc.is_stock_item,
|
||||||
"item_code": item_doc.name,
|
"item_code": item_doc.name,
|
||||||
|
"item_group": item_doc.item_group,
|
||||||
"item_image": item_doc.image,
|
"item_image": item_doc.image,
|
||||||
"item_name": item_doc.item_name,
|
"item_name": item_doc.item_name,
|
||||||
"serial_no": serial_no,
|
"serial_no": serial_no,
|
||||||
@@ -92,6 +93,12 @@ def search_by_term(search_term, warehouse, price_list):
|
|||||||
return {"items": [item]}
|
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()
|
@frappe.whitelist()
|
||||||
def get_items(start, page_length, price_list, item_group, pos_profile, search_term=""):
|
def get_items(start, page_length, price_list, item_group, pos_profile, search_term=""):
|
||||||
warehouse, hide_unavailable_items = frappe.db.get_value(
|
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:
|
if search_term:
|
||||||
result = search_by_term(search_term, warehouse, price_list) or []
|
result = search_by_term(search_term, warehouse, price_list) or []
|
||||||
|
filter_result_items(result, pos_profile)
|
||||||
if result:
|
if result:
|
||||||
return 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:
|
if not items_data:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
current_date = frappe.utils.today()
|
||||||
|
|
||||||
for item in items_data:
|
for item in items_data:
|
||||||
uoms = frappe.get_doc("Item", item.item_code).get("uoms", [])
|
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 = frappe.get_all(
|
||||||
"Item Price",
|
"Item Price",
|
||||||
fields=["price_list_rate", "currency", "uom", "batch_no"],
|
fields=["price_list_rate", "currency", "uom", "batch_no", "valid_from", "valid_upto"],
|
||||||
filters={
|
filters={
|
||||||
"price_list": price_list,
|
"price_list": price_list,
|
||||||
"item_code": item.item_code,
|
"item_code": item.item_code,
|
||||||
"selling": True,
|
"selling": True,
|
||||||
|
"valid_from": ["<=", current_date],
|
||||||
|
"valid_upto": ["in", [None, "", current_date]],
|
||||||
},
|
},
|
||||||
|
order_by="valid_from desc",
|
||||||
|
limit=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not item_price:
|
if not item_price:
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ erpnext.PointOfSale.ItemSelector = class {
|
|||||||
return `<div class="item-qty-pill">
|
return `<div class="item-qty-pill">
|
||||||
<span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span>
|
<span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span>
|
||||||
</div>
|
</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
|
<img
|
||||||
onerror="cur_pos.item_selector.handle_broken_image(this)"
|
onerror="cur_pos.item_selector.handle_broken_image(this)"
|
||||||
class="h-full item-img" src="${item_image}"
|
class="h-full item-img" src="${item_image}"
|
||||||
@@ -138,7 +138,6 @@ erpnext.PointOfSale.ItemSelector = class {
|
|||||||
|
|
||||||
make_search_bar() {
|
make_search_bar() {
|
||||||
const me = this;
|
const me = this;
|
||||||
const doc = me.events.get_frm().doc;
|
|
||||||
this.$component.find(".search-field").html("");
|
this.$component.find(".search-field").html("");
|
||||||
this.$component.find(".item-group-field").html("");
|
this.$component.find(".item-group-field").html("");
|
||||||
|
|
||||||
@@ -163,6 +162,7 @@ erpnext.PointOfSale.ItemSelector = class {
|
|||||||
me.filter_items();
|
me.filter_items();
|
||||||
},
|
},
|
||||||
get_query: function () {
|
get_query: function () {
|
||||||
|
const doc = me.events.get_frm().doc;
|
||||||
return {
|
return {
|
||||||
query: "erpnext.selling.page.point_of_sale.point_of_sale.item_group_query",
|
query: "erpnext.selling.page.point_of_sale.point_of_sale.item_group_query",
|
||||||
filters: {
|
filters: {
|
||||||
|
|||||||
@@ -449,18 +449,355 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"France": {
|
"France": {
|
||||||
"France VAT 20%": {
|
"chart_of_accounts": {
|
||||||
"account_name": "VAT 20%",
|
"France - Plan Comptable General avec code": {
|
||||||
"tax_rate": 20,
|
"sales_tax_templates": [
|
||||||
"default": 1
|
{
|
||||||
},
|
"title": "TVA 20% Collectée",
|
||||||
"France VAT 10%": {
|
"tax_category": "Vente Domestique",
|
||||||
"account_name": "VAT 10%",
|
"taxes": [
|
||||||
"tax_rate": 10
|
{
|
||||||
},
|
"account_head": {
|
||||||
"France VAT 5.5%": {
|
"account_name": "TVA 20% Collectée",
|
||||||
"account_name": "VAT 5.5%",
|
"account_number": "445720",
|
||||||
"tax_rate": 5.5
|
"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):
|
def from_detailed_data(company_name, data):
|
||||||
"""Create Taxes and Charges Templates from detailed 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", {})
|
coa_data = data.get("chart_of_accounts", {})
|
||||||
tax_templates = coa_data.get(coa_name) or coa_data.get("*", {})
|
tax_templates = coa_data.get(coa_name) or coa_data.get("*", {})
|
||||||
tax_categories = data.get("tax_categories")
|
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.item.test_item import create_item
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
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_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
|
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
|
|
||||||
|
|
||||||
@@ -426,39 +427,49 @@ class TestInventoryDimension(FrappeTestCase):
|
|||||||
|
|
||||||
warehouse = create_warehouse("Negative Stock Warehouse")
|
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 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, do_not_submit=True)
|
||||||
doc.items[0].inv_site = "Site 1"
|
doc.items[0].inv_site = "Site 1"
|
||||||
self.assertRaises(frappe.ValidationError, doc.submit)
|
self.assertRaises(InventoryDimensionNegativeStockError, doc.submit)
|
||||||
|
|
||||||
|
# cancel the stock entry
|
||||||
doc.reload()
|
doc.reload()
|
||||||
if doc.docstatus == 1:
|
if doc.docstatus == 1:
|
||||||
doc.cancel()
|
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 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True)
|
||||||
|
|
||||||
doc.items[0].to_inv_site = "Site 1"
|
doc.items[0].to_inv_site = "Site 1"
|
||||||
doc.submit()
|
doc.submit()
|
||||||
|
|
||||||
|
# check inventory dimension value in stock ledger entry
|
||||||
site_name = frappe.get_all(
|
site_name = frappe.get_all(
|
||||||
"Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"]
|
"Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"]
|
||||||
)[0].inv_site
|
)[0].inv_site
|
||||||
|
|
||||||
self.assertEqual(site_name, "Site 1")
|
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 = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True)
|
||||||
|
|
||||||
doc.items[0].inv_site = "Site 1"
|
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.reload()
|
||||||
inv_dimension.db_set("validate_negative_stock", 0)
|
inv_dimension.db_set("validate_negative_stock", 0)
|
||||||
frappe.local.inventory_dimensions = {}
|
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 = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True)
|
||||||
|
|
||||||
doc.items[0].inv_site = "Site 1"
|
doc.items[0].inv_site = "Site 1"
|
||||||
doc.submit()
|
doc.submit()
|
||||||
self.assertEqual(doc.docstatus, 1)
|
self.assertEqual(doc.docstatus, 1)
|
||||||
|
|
||||||
|
# check inventory dimension value in stock ledger entry
|
||||||
site_name = frappe.get_all(
|
site_name = frappe.get_all(
|
||||||
"Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"]
|
"Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"]
|
||||||
)[0].inv_site
|
)[0].inv_site
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ class PickList(Document):
|
|||||||
"actual_qty",
|
"actual_qty",
|
||||||
)
|
)
|
||||||
|
|
||||||
if row.qty > bin_qty:
|
if row.qty > flt(bin_qty):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}."
|
"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,
|
"no_copy": 1,
|
||||||
"oldfieldname": "status",
|
"oldfieldname": "status",
|
||||||
"oldfieldtype": "Select",
|
"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_hide": 1,
|
||||||
"print_width": "150px",
|
"print_width": "150px",
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
@@ -1273,7 +1273,7 @@
|
|||||||
"idx": 261,
|
"idx": 261,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-07-04 14:50:10.538472",
|
"modified": "2024-11-13 16:55:14.129055",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Purchase Receipt",
|
"name": "Purchase Receipt",
|
||||||
|
|||||||
@@ -112,7 +112,9 @@ class PurchaseReceipt(BuyingController):
|
|||||||
shipping_address: DF.Link | None
|
shipping_address: DF.Link | None
|
||||||
shipping_address_display: DF.SmallText | None
|
shipping_address_display: DF.SmallText | None
|
||||||
shipping_rule: DF.Link | 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
|
subcontracting_receipt: DF.Link | None
|
||||||
supplied_items: DF.Table[PurchaseReceiptItemSupplied]
|
supplied_items: DF.Table[PurchaseReceiptItemSupplied]
|
||||||
supplier: DF.Link
|
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):
|
def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False):
|
||||||
# Update Billing % based on pending accepted qty
|
# Update Billing % based on pending accepted qty
|
||||||
|
buying_settings = frappe.get_single("Buying Settings")
|
||||||
|
|
||||||
total_amount, total_billed_amount = 0, 0
|
total_amount, total_billed_amount = 0, 0
|
||||||
item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
|
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_qty = flt(item_wise_returned_qty.get(item.name))
|
||||||
returned_amount = flt(returned_qty) * flt(item.rate)
|
returned_amount = flt(returned_qty) * flt(item.rate)
|
||||||
pending_amount = flt(item.amount) - returned_amount
|
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_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:
|
if pr_doc.get("is_return") and not total_amount and total_billed_amount:
|
||||||
total_amount = total_billed_amount
|
total_amount = total_billed_amount
|
||||||
|
|||||||
@@ -3900,6 +3900,54 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
for incoming_rate in bundle_data:
|
for incoming_rate in bundle_data:
|
||||||
self.assertEqual(incoming_rate, 0)
|
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():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||||
|
|||||||
@@ -689,6 +689,9 @@ class SerialandBatchBundle(Document):
|
|||||||
serial_batches = {}
|
serial_batches = {}
|
||||||
for row in self.entries:
|
for row in self.entries:
|
||||||
if not row.qty and row.batch_no and not row.serial_no:
|
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(
|
frappe.throw(
|
||||||
_("At row {0}: Qty is mandatory for the batch {1}").format(
|
_("At row {0}: Qty is mandatory for the batch {1}").format(
|
||||||
bold(row.idx), bold(row.batch_no)
|
bold(row.idx), bold(row.batch_no)
|
||||||
|
|||||||
@@ -117,6 +117,10 @@ frappe.ui.form.on("Stock Entry", {
|
|||||||
filters["is_inward"] = 1;
|
filters["is_inward"] = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (["Material Receipt", "Material Transfer", "Material Issue"].includes(doc.purpose)) {
|
||||||
|
filters["include_expired_batches"] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query: "erpnext.controllers.queries.get_batch_no",
|
query: "erpnext.controllers.queries.get_batch_no",
|
||||||
filters: filters,
|
filters: filters,
|
||||||
|
|||||||
@@ -795,9 +795,6 @@ class StockEntry(StockController):
|
|||||||
self.set_total_incoming_outgoing_value()
|
self.set_total_incoming_outgoing_value()
|
||||||
self.set_total_amount()
|
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):
|
def set_basic_rate(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
|
||||||
"""
|
"""
|
||||||
Set rate for outgoing, scrapped and finished items
|
Set rate for outgoing, scrapped and finished items
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import frappe
|
|||||||
from frappe import _, bold
|
from frappe import _, bold
|
||||||
from frappe.core.doctype.role.role import get_users
|
from frappe.core.doctype.role.role import get_users
|
||||||
from frappe.model.document import Document
|
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 frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate
|
||||||
|
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
@@ -25,6 +26,10 @@ class BackDatedStockTransaction(frappe.ValidationError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryDimensionNegativeStockError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
exclude_from_linked_with = True
|
exclude_from_linked_with = True
|
||||||
|
|
||||||
|
|
||||||
@@ -104,61 +109,56 @@ class StockLedgerEntry(Document):
|
|||||||
self.posting_datetime = get_combine_datetime(self.posting_date, self.posting_time)
|
self.posting_datetime = get_combine_datetime(self.posting_date, self.posting_time)
|
||||||
|
|
||||||
def validate_inventory_dimension_negative_stock(self):
|
def validate_inventory_dimension_negative_stock(self):
|
||||||
if self.is_cancelled:
|
if self.is_cancelled or self.actual_qty >= 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
extra_cond = ""
|
|
||||||
kwargs = {}
|
|
||||||
|
|
||||||
dimensions = self._get_inventory_dimensions()
|
dimensions = self._get_inventory_dimensions()
|
||||||
if not dimensions:
|
if not dimensions:
|
||||||
return
|
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
|
flt_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||||
if sle:
|
for dimension, values in dimensions.items():
|
||||||
qty_after_transaction = sle.qty_after_transaction
|
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(available_qty + flt(self.actual_qty), flt_precision) # qty after current transaction
|
||||||
diff = flt(diff, flt_precision)
|
if diff < 0 and abs(diff) > 0.0001:
|
||||||
if diff < 0 and abs(diff) > 0.0001:
|
self.throw_validation_error(diff, dimension, dimension_value)
|
||||||
self.throw_validation_error(diff, dimensions)
|
|
||||||
|
|
||||||
def throw_validation_error(self, diff, dimensions):
|
def get_available_qty_after_prev_transaction(self, dimension, dimension_value):
|
||||||
dimension_msg = _(", with the inventory {0}: {1}").format(
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
"dimensions" if len(dimensions) > 1 else "dimension",
|
available_qty = (
|
||||||
", ".join(f"{bold(d.doctype)} ({d.value})" for k, d in dimensions.items()),
|
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 = _(
|
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(
|
).format(
|
||||||
abs(diff),
|
abs(diff),
|
||||||
frappe.get_desk_link("Item", self.item_code),
|
frappe.get_desk_link("Item", self.item_code),
|
||||||
frappe.get_desk_link("Warehouse", self.warehouse),
|
frappe.get_desk_link("Warehouse", self.warehouse),
|
||||||
dimension_msg,
|
frappe.bold(dimension),
|
||||||
|
frappe.bold(dimension_value),
|
||||||
self.posting_date,
|
self.posting_date,
|
||||||
self.posting_time,
|
self.posting_time,
|
||||||
frappe.get_desk_link(self.voucher_type, self.voucher_no),
|
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):
|
def _get_inventory_dimensions(self):
|
||||||
inv_dimensions = get_inventory_dimensions()
|
inv_dimensions = get_inventory_dimensions()
|
||||||
|
|||||||
@@ -324,6 +324,7 @@ class StockReconciliation(StockController):
|
|||||||
row.item_code,
|
row.item_code,
|
||||||
posting_date=self.posting_date,
|
posting_date=self.posting_date,
|
||||||
posting_time=self.posting_time,
|
posting_time=self.posting_time,
|
||||||
|
for_stock_levels=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
total_current_qty += current_qty
|
total_current_qty += current_qty
|
||||||
@@ -1322,7 +1323,16 @@ def get_stock_balance_for(
|
|||||||
qty, rate = data
|
qty, rate = data
|
||||||
|
|
||||||
if item_dict.get("has_batch_no"):
|
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 {
|
return {
|
||||||
"qty": qty,
|
"qty": qty,
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ def get_item_map(item_code, include_uom):
|
|||||||
|
|
||||||
if include_uom:
|
if include_uom:
|
||||||
ucd = frappe.qb.DocType("UOM Conversion Detail")
|
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))
|
query = query.left_join(ucd).on((ucd.parent == item.name) & (ucd.uom == include_uom))
|
||||||
|
|
||||||
items = query.run(as_dict=True)
|
items = query.run(as_dict=True)
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ class SerialBatchBundle:
|
|||||||
"Outward": self.sle.actual_qty < 0,
|
"Outward": self.sle.actual_qty < 0,
|
||||||
}.get(sn_doc.type_of_transaction)
|
}.get(sn_doc.type_of_transaction)
|
||||||
|
|
||||||
if not condition:
|
if not condition and self.sle.actual_qty:
|
||||||
correct_type = "Inward"
|
correct_type = "Inward"
|
||||||
if sn_doc.type_of_transaction == "Inward":
|
if sn_doc.type_of_transaction == "Inward":
|
||||||
correct_type = "Outward"
|
correct_type = "Outward"
|
||||||
@@ -133,7 +133,7 @@ class SerialBatchBundle:
|
|||||||
frappe.throw(_(msg), title=_("Incorrect Type of Transaction"))
|
frappe.throw(_(msg), title=_("Incorrect Type of Transaction"))
|
||||||
|
|
||||||
precision = sn_doc.precision("total_qty")
|
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}"
|
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))
|
frappe.throw(_(msg))
|
||||||
|
|
||||||
@@ -288,7 +288,7 @@ class SerialBatchBundle:
|
|||||||
"Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
|
"Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
|
||||||
)
|
)
|
||||||
|
|
||||||
if docstatus != 1:
|
if docstatus == 0:
|
||||||
self.submit_serial_and_batch_bundle()
|
self.submit_serial_and_batch_bundle()
|
||||||
|
|
||||||
if self.item_details.has_serial_no == 1:
|
if self.item_details.has_serial_no == 1:
|
||||||
@@ -311,7 +311,9 @@ class SerialBatchBundle:
|
|||||||
if self.is_pos_transaction():
|
if self.is_pos_transaction():
|
||||||
return
|
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):
|
def is_pos_transaction(self):
|
||||||
if (
|
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.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
|
||||||
stock_entry.db_update()
|
stock_entry.db_update()
|
||||||
for d in stock_entry.items:
|
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):
|
if d.name == voucher_detail_no or (not d.s_warehouse and d.t_warehouse):
|
||||||
d.db_update()
|
d.db_update()
|
||||||
|
|
||||||
|
|||||||
@@ -766,7 +766,11 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False
|
|||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda doc: doc.name in po_sr_item_dict,
|
"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,
|
postprocess=post_process,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user