Merge pull request #38690 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
commit
0d8a52f63b
@ -1689,13 +1689,42 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
return data
|
||||
|
||||
|
||||
def split_invoices_based_on_payment_terms(outstanding_invoices, company):
|
||||
invoice_ref_based_on_payment_terms = {}
|
||||
def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list:
|
||||
"""Split a list of invoices based on their payment terms."""
|
||||
exc_rates = get_currency_data(outstanding_invoices, company)
|
||||
|
||||
outstanding_invoices_after_split = []
|
||||
for entry in outstanding_invoices:
|
||||
if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if payment_term_template := frappe.db.get_value(
|
||||
entry.voucher_type, entry.voucher_no, "payment_terms_template"
|
||||
):
|
||||
split_rows = get_split_invoice_rows(entry, payment_term_template, exc_rates)
|
||||
if not split_rows:
|
||||
continue
|
||||
|
||||
frappe.msgprint(
|
||||
_("Splitting {0} {1} into {2} rows as per Payment Terms").format(
|
||||
_(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows)
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
outstanding_invoices_after_split += split_rows
|
||||
continue
|
||||
|
||||
# If not an invoice or no payment terms template, add as it is
|
||||
outstanding_invoices_after_split.append(entry)
|
||||
|
||||
return outstanding_invoices_after_split
|
||||
|
||||
|
||||
def get_currency_data(outstanding_invoices: list, company: str = None) -> dict:
|
||||
"""Get currency and conversion data for a list of invoices."""
|
||||
exc_rates = frappe._dict()
|
||||
company_currency = (
|
||||
frappe.db.get_value("Company", company, "default_currency") if company else None
|
||||
)
|
||||
exc_rates = frappe._dict()
|
||||
|
||||
for doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
|
||||
for x in frappe.db.get_all(
|
||||
@ -1710,72 +1739,54 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company):
|
||||
company_currency=company_currency,
|
||||
)
|
||||
|
||||
for idx, d in enumerate(outstanding_invoices):
|
||||
if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
|
||||
payment_term_template = frappe.db.get_value(
|
||||
d.voucher_type, d.voucher_no, "payment_terms_template"
|
||||
return exc_rates
|
||||
|
||||
|
||||
def get_split_invoice_rows(invoice: dict, payment_term_template: str, exc_rates: dict) -> list:
|
||||
"""Split invoice based on its payment schedule table."""
|
||||
split_rows = []
|
||||
allocate_payment_based_on_payment_terms = frappe.db.get_value(
|
||||
"Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
|
||||
)
|
||||
|
||||
if not allocate_payment_based_on_payment_terms:
|
||||
return [invoice]
|
||||
|
||||
payment_schedule = frappe.get_all(
|
||||
"Payment Schedule", filters={"parent": invoice.voucher_no}, fields=["*"], order_by="due_date"
|
||||
)
|
||||
for payment_term in payment_schedule:
|
||||
if not payment_term.outstanding > 0.1:
|
||||
continue
|
||||
|
||||
doc_details = exc_rates.get(payment_term.parent, None)
|
||||
is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
|
||||
doc_details.party_account_currency != doc_details.company_currency
|
||||
)
|
||||
payment_term_outstanding = flt(payment_term.outstanding)
|
||||
if not is_multi_currency_acc:
|
||||
payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
|
||||
|
||||
split_rows.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"due_date": invoice.due_date,
|
||||
"currency": invoice.currency,
|
||||
"voucher_no": invoice.voucher_no,
|
||||
"voucher_type": invoice.voucher_type,
|
||||
"posting_date": invoice.posting_date,
|
||||
"invoice_amount": flt(invoice.invoice_amount),
|
||||
"outstanding_amount": payment_term_outstanding
|
||||
if payment_term_outstanding
|
||||
else invoice.outstanding_amount,
|
||||
"payment_term_outstanding": payment_term_outstanding,
|
||||
"payment_amount": payment_term.payment_amount,
|
||||
"payment_term": payment_term.payment_term,
|
||||
}
|
||||
)
|
||||
if payment_term_template:
|
||||
allocate_payment_based_on_payment_terms = frappe.get_cached_value(
|
||||
"Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
|
||||
)
|
||||
if allocate_payment_based_on_payment_terms:
|
||||
payment_schedule = frappe.get_all(
|
||||
"Payment Schedule", filters={"parent": d.voucher_no}, fields=["*"]
|
||||
)
|
||||
)
|
||||
|
||||
for payment_term in payment_schedule:
|
||||
if payment_term.outstanding > 0.1:
|
||||
doc_details = exc_rates.get(payment_term.parent, None)
|
||||
is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
|
||||
doc_details.party_account_currency != doc_details.company_currency
|
||||
)
|
||||
payment_term_outstanding = flt(payment_term.outstanding)
|
||||
if not is_multi_currency_acc:
|
||||
payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
|
||||
|
||||
invoice_ref_based_on_payment_terms.setdefault(idx, [])
|
||||
invoice_ref_based_on_payment_terms[idx].append(
|
||||
frappe._dict(
|
||||
{
|
||||
"due_date": d.due_date,
|
||||
"currency": d.currency,
|
||||
"voucher_no": d.voucher_no,
|
||||
"voucher_type": d.voucher_type,
|
||||
"posting_date": d.posting_date,
|
||||
"invoice_amount": flt(d.invoice_amount),
|
||||
"outstanding_amount": payment_term_outstanding
|
||||
if payment_term_outstanding
|
||||
else d.outstanding_amount,
|
||||
"payment_term_outstanding": payment_term_outstanding,
|
||||
"payment_amount": payment_term.payment_amount,
|
||||
"payment_term": payment_term.payment_term,
|
||||
"account": d.account,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
outstanding_invoices_after_split = []
|
||||
if invoice_ref_based_on_payment_terms:
|
||||
for idx, ref in invoice_ref_based_on_payment_terms.items():
|
||||
voucher_no = ref[0]["voucher_no"]
|
||||
voucher_type = ref[0]["voucher_type"]
|
||||
|
||||
frappe.msgprint(
|
||||
_("Spliting {} {} into {} row(s) as per Payment Terms").format(
|
||||
voucher_type, voucher_no, len(ref)
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
|
||||
outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx]
|
||||
|
||||
existing_row = list(filter(lambda x: x.get("voucher_no") == voucher_no, outstanding_invoices))
|
||||
index = outstanding_invoices.index(existing_row[0])
|
||||
outstanding_invoices.pop(index)
|
||||
|
||||
outstanding_invoices_after_split += outstanding_invoices
|
||||
return outstanding_invoices_after_split
|
||||
return split_rows
|
||||
|
||||
|
||||
def get_orders_to_be_billed(
|
||||
|
@ -6,11 +6,12 @@ import unittest
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils import add_days, flt, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
InvalidPaymentEntry,
|
||||
get_outstanding_reference_documents,
|
||||
get_payment_entry,
|
||||
get_reference_details,
|
||||
)
|
||||
@ -1471,6 +1472,45 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
for field in ["account", "debit", "credit"]:
|
||||
self.assertEqual(self.expected_gle[row][field], gl_entries[row][field])
|
||||
|
||||
def test_outstanding_invoices_api(self):
|
||||
"""
|
||||
Test if `get_outstanding_reference_documents` fetches invoices in the right order.
|
||||
"""
|
||||
customer = create_customer("Max Mustermann", "INR")
|
||||
create_payment_terms_template()
|
||||
|
||||
# SI has an earlier due date and SI2 has a later due date
|
||||
si = create_sales_invoice(
|
||||
qty=1, rate=100, customer=customer, posting_date=add_days(nowdate(), -4)
|
||||
)
|
||||
si2 = create_sales_invoice(do_not_save=1, qty=1, rate=100, customer=customer)
|
||||
si2.payment_terms_template = "Test Receivable Template"
|
||||
si2.submit()
|
||||
|
||||
args = {
|
||||
"posting_date": nowdate(),
|
||||
"company": "_Test Company",
|
||||
"party_type": "Customer",
|
||||
"payment_type": "Pay",
|
||||
"party": customer,
|
||||
"party_account": "Debtors - _TC",
|
||||
}
|
||||
args.update(
|
||||
{
|
||||
"get_outstanding_invoices": True,
|
||||
"from_posting_date": add_days(nowdate(), -4),
|
||||
"to_posting_date": add_days(nowdate(), 2),
|
||||
}
|
||||
)
|
||||
references = get_outstanding_reference_documents(args)
|
||||
|
||||
self.assertEqual(len(references), 3)
|
||||
self.assertEqual(references[0].voucher_no, si.name)
|
||||
self.assertEqual(references[1].voucher_no, si2.name)
|
||||
self.assertEqual(references[2].voucher_no, si2.name)
|
||||
self.assertEqual(references[1].payment_term, "Basic Amount Receivable")
|
||||
self.assertEqual(references[2].payment_term, "Tax Receivable")
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
@ -1531,6 +1571,9 @@ def create_payment_terms_template():
|
||||
def create_payment_terms_template_with_discount(
|
||||
name=None, discount_type=None, discount=None, template_name=None
|
||||
):
|
||||
"""
|
||||
Create a Payment Terms Template with % or amount discount.
|
||||
"""
|
||||
create_payment_term(name or "30 Credit Days with 10% Discount")
|
||||
template_name = template_name or "Test Discount Template"
|
||||
|
||||
|
@ -34,4 +34,6 @@ class PaymentReconciliationAllocation(Document):
|
||||
unreconciled_amount: DF.Currency
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
@ -26,4 +26,6 @@ class PaymentReconciliationInvoice(Document):
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
@ -30,4 +30,6 @@ class PaymentReconciliationPayment(Document):
|
||||
remark: DF.SmallText | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
@ -581,6 +581,8 @@ def apply_pricing_rule_on_transaction(doc):
|
||||
if d.price_or_product_discount == "Price":
|
||||
if d.apply_discount_on:
|
||||
doc.set("apply_discount_on", d.apply_discount_on)
|
||||
# Variable to track whether the condition has been met
|
||||
condition_met = False
|
||||
|
||||
for field in ["additional_discount_percentage", "discount_amount"]:
|
||||
pr_field = "discount_percentage" if field == "additional_discount_percentage" else field
|
||||
@ -603,6 +605,11 @@ def apply_pricing_rule_on_transaction(doc):
|
||||
if coupon_code_pricing_rule == d.name:
|
||||
# if selected coupon code is linked with pricing rule
|
||||
doc.set(field, d.get(pr_field))
|
||||
|
||||
# Set the condition_met variable to True and break out of the loop
|
||||
condition_met = True
|
||||
break
|
||||
|
||||
else:
|
||||
# reset discount if not linked
|
||||
doc.set(field, 0)
|
||||
@ -611,6 +618,10 @@ def apply_pricing_rule_on_transaction(doc):
|
||||
doc.set(field, 0)
|
||||
|
||||
doc.calculate_taxes_and_totals()
|
||||
|
||||
# Break out of the main loop if the condition is met
|
||||
if condition_met:
|
||||
break
|
||||
elif d.price_or_product_discount == "Product":
|
||||
item_details = frappe._dict({"parenttype": doc.doctype, "free_item_data": []})
|
||||
get_product_discount_rule(d, item_details, doc=doc)
|
||||
|
@ -126,7 +126,7 @@ class RepostAccountingLedger(Document):
|
||||
return rendered_page
|
||||
|
||||
def on_submit(self):
|
||||
if len(self.vouchers) > 1:
|
||||
if len(self.vouchers) > 5:
|
||||
job_name = "repost_accounting_ledger_" + self.name
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
|
||||
@ -170,8 +170,6 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.make_gl_entries(1)
|
||||
doc.make_gl_entries()
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def get_allowed_types_from_settings():
|
||||
return [
|
||||
|
@ -20,18 +20,11 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.update_repost_settings()
|
||||
update_repost_settings()
|
||||
|
||||
def teadDown(self):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def update_repost_settings(self):
|
||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
repost_settings.save()
|
||||
|
||||
def test_01_basic_functions(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
@ -90,9 +83,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
# Submit repost document
|
||||
ral.save().submit()
|
||||
|
||||
# background jobs don't run on test cases. Manually triggering repost function.
|
||||
start_repost(ral.name)
|
||||
|
||||
res = (
|
||||
qb.from_(gl)
|
||||
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
|
||||
@ -177,26 +167,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save().submit()
|
||||
|
||||
# without deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = False
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save()
|
||||
|
||||
# assert preview data is generated
|
||||
preview = ral.generate_preview()
|
||||
self.assertIsNotNone(preview)
|
||||
|
||||
ral.save().submit()
|
||||
|
||||
# background jobs don't run on test cases. Manually triggering repost function.
|
||||
start_repost(ral.name)
|
||||
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
# with deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
@ -205,6 +175,38 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save().submit()
|
||||
|
||||
start_repost(ral.name)
|
||||
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
def test_05_without_deletion_flag(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save().submit()
|
||||
|
||||
# without deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = False
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save().submit()
|
||||
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
|
||||
def update_repost_settings():
|
||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
repost_settings.save()
|
||||
|
@ -2799,6 +2799,12 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
|
||||
def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self):
|
||||
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import (
|
||||
update_repost_settings,
|
||||
)
|
||||
|
||||
update_repost_settings()
|
||||
|
||||
additional_discount_account = create_account(
|
||||
account_name="Discount Account",
|
||||
parent_account="Indirect Expenses - _TC",
|
||||
|
@ -8,7 +8,17 @@ import re
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
formatdate,
|
||||
get_first_day,
|
||||
getdate,
|
||||
today,
|
||||
)
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
@ -43,6 +53,8 @@ def get_period_list(
|
||||
year_start_date = getdate(period_start_date)
|
||||
year_end_date = getdate(period_end_date)
|
||||
|
||||
year_end_date = getdate(today()) if year_end_date > getdate(today()) else year_end_date
|
||||
|
||||
months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity]
|
||||
|
||||
period_list = []
|
||||
|
@ -340,6 +340,10 @@ class AssetDepreciationSchedule(Document):
|
||||
n == 0
|
||||
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
|
||||
and not self.opening_accumulated_depreciation
|
||||
and get_updated_rate_of_depreciation_for_wdv_and_dd(
|
||||
asset_doc, value_after_depreciation, row, False
|
||||
)
|
||||
== row.rate_of_depreciation
|
||||
):
|
||||
from_date = add_days(
|
||||
asset_doc.available_for_use_date, -1
|
||||
@ -605,7 +609,9 @@ def get_depreciation_amount(
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb_row):
|
||||
def get_updated_rate_of_depreciation_for_wdv_and_dd(
|
||||
asset, depreciable_value, fb_row, show_msg=True
|
||||
):
|
||||
return fb_row.rate_of_depreciation
|
||||
|
||||
|
||||
|
@ -292,6 +292,7 @@ class AccountsController(TransactionBase):
|
||||
def on_trash(self):
|
||||
self._remove_references_in_repost_doctypes()
|
||||
self._remove_references_in_unreconcile()
|
||||
self.remove_serial_and_batch_bundle()
|
||||
|
||||
# delete sl and gl entries on deletion of transaction
|
||||
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
|
||||
@ -307,6 +308,15 @@ class AccountsController(TransactionBase):
|
||||
(self.doctype, self.name),
|
||||
)
|
||||
|
||||
def remove_serial_and_batch_bundle(self):
|
||||
bundles = frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
filters={"voucher_type": self.doctype, "voucher_no": self.name, "docstatus": ("!=", 1)},
|
||||
)
|
||||
|
||||
for bundle in bundles:
|
||||
frappe.delete_doc("Serial and Batch Bundle", bundle.name)
|
||||
|
||||
def validate_deferred_income_expense_account(self):
|
||||
field_map = {
|
||||
"Sales Invoice": "deferred_revenue_account",
|
||||
|
@ -637,6 +637,7 @@ additional_timeline_content = {
|
||||
|
||||
extend_bootinfo = [
|
||||
"erpnext.support.doctype.service_level_agreement.service_level_agreement.add_sla_doctypes",
|
||||
"erpnext.startup.boot.bootinfo",
|
||||
]
|
||||
|
||||
|
||||
|
@ -36,14 +36,14 @@ erpnext.buying = {
|
||||
|
||||
// no idea where me is coming from
|
||||
if(this.frm.get_field('shipping_address')) {
|
||||
this.frm.set_query("shipping_address", function() {
|
||||
if(me.frm.doc.customer) {
|
||||
this.frm.set_query("shipping_address", () => {
|
||||
if(this.frm.doc.customer) {
|
||||
return {
|
||||
query: 'frappe.contacts.doctype.address.address.address_query',
|
||||
filters: { link_doctype: 'Customer', link_name: me.frm.doc.customer }
|
||||
filters: { link_doctype: 'Customer', link_name: this.frm.doc.customer }
|
||||
};
|
||||
} else
|
||||
return erpnext.queries.company_address_query(me.frm.doc)
|
||||
return erpnext.queries.company_address_query(this.frm.doc)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -471,7 +471,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
item.pricing_rules = ''
|
||||
return this.frm.call({
|
||||
method: "erpnext.stock.get_item_details.get_item_details",
|
||||
child: item,
|
||||
args: {
|
||||
doc: me.frm.doc,
|
||||
args: {
|
||||
@ -520,6 +519,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
frappe.run_serially([
|
||||
() => {
|
||||
var child = locals[cdt][cdn];
|
||||
var std_field_list = ["doctype"]
|
||||
.concat(frappe.model.std_fields_list)
|
||||
.concat(frappe.model.child_table_field_list);
|
||||
|
||||
for (var key in r.message) {
|
||||
if (std_field_list.indexOf(key) === -1) {
|
||||
if (key === "qty" && child[key]) continue;
|
||||
child[key] = r.message[key];
|
||||
}
|
||||
}
|
||||
},
|
||||
() => {
|
||||
var d = locals[cdt][cdn];
|
||||
me.add_taxes_from_item_tax_template(d.item_tax_rate);
|
||||
|
@ -2,10 +2,16 @@ frappe.provide("erpnext.financial_statements");
|
||||
|
||||
erpnext.financial_statements = {
|
||||
"filters": get_filters(),
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
"formatter": function(value, row, column, data, default_formatter, filter) {
|
||||
if (data && column.fieldname=="account") {
|
||||
value = data.account_name || value;
|
||||
|
||||
if (filter && filter?.text && filter?.type == "contains") {
|
||||
if (!value.toLowerCase().includes(filter.text)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.account) {
|
||||
column.link_onclick =
|
||||
"erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")";
|
||||
|
@ -8,7 +8,7 @@ $.extend(erpnext, {
|
||||
if(!company && cur_frm)
|
||||
company = cur_frm.doc.company;
|
||||
if(company)
|
||||
return frappe.get_doc(":Company", company).default_currency || frappe.boot.sysdefaults.currency;
|
||||
return frappe.get_doc(":Company", company)?.default_currency || frappe.boot.sysdefaults.currency;
|
||||
else
|
||||
return frappe.boot.sysdefaults.currency;
|
||||
},
|
||||
|
@ -31,7 +31,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
secondary_action: () => this.edit_full_form(),
|
||||
});
|
||||
|
||||
this.dialog.set_value("qty", this.item.qty).then(() => {
|
||||
let qty = this.item.stock_qty || this.item.transfer_qty || this.item.qty;
|
||||
this.dialog.set_value("qty", qty).then(() => {
|
||||
if (this.item.serial_no) {
|
||||
this.dialog.set_value("scan_serial_no", this.item.serial_no);
|
||||
frappe.model.set_value(this.item.doctype, this.item.name, 'serial_no', '');
|
||||
|
@ -2,8 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import os
|
||||
import json
|
||||
import os
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@ -114,10 +114,11 @@ def update_regional_tax_settings(country, company):
|
||||
frappe.scrub(country)
|
||||
)
|
||||
frappe.get_attr(module_name)(country, company)
|
||||
except Exception as e:
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
except Exception:
|
||||
# Log error and ignore if failed to setup regional tax settings
|
||||
frappe.log_error("Unable to setup regional tax settings")
|
||||
pass
|
||||
|
||||
|
||||
def make_taxes_and_charges_template(company_name, doctype, template):
|
||||
|
@ -75,3 +75,11 @@ def update_page_info(bootinfo):
|
||||
"Sales Person Tree": {"title": "Sales Person Tree", "route": "Tree/Sales Person"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def bootinfo(bootinfo):
|
||||
if bootinfo.get("user") and bootinfo["user"].get("name"):
|
||||
bootinfo["user"]["employee"] = ""
|
||||
employee = frappe.db.get_value("Employee", {"user_id": bootinfo["user"]["name"]}, "name")
|
||||
if employee:
|
||||
bootinfo["user"]["employee"] = employee
|
||||
|
@ -1,5 +1,5 @@
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
from frappe.utils.deprecations import deprecated
|
||||
|
||||
|
||||
def get_leaderboards():
|
||||
@ -54,12 +54,13 @@ def get_leaderboards():
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_customers(date_range, company, field, limit=None):
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if field == "outstanding_amount":
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
filters.append(["posting_date", ">=", "between", [date_range[0], date_range[1]]])
|
||||
return frappe.db.get_all(
|
||||
if from_date and to_date:
|
||||
filters.append(["posting_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.get_list(
|
||||
"Sales Invoice",
|
||||
fields=["customer as name", "sum(outstanding_amount) as value"],
|
||||
filters=filters,
|
||||
@ -69,26 +70,20 @@ def get_all_customers(date_range, company, field, limit=None):
|
||||
)
|
||||
else:
|
||||
if field == "total_sales_amount":
|
||||
select_field = "sum(so_item.base_net_amount)"
|
||||
select_field = "base_net_total"
|
||||
elif field == "total_qty_sold":
|
||||
select_field = "sum(so_item.stock_qty)"
|
||||
select_field = "total_qty"
|
||||
|
||||
date_condition = get_date_condition(date_range, "so.transaction_date")
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select so.customer as name, {0} as value
|
||||
FROM `tabSales Order` as so JOIN `tabSales Order Item` as so_item
|
||||
ON so.name = so_item.parent
|
||||
where so.docstatus = 1 {1} and so.company = %s
|
||||
group by so.customer
|
||||
order by value DESC
|
||||
limit %s
|
||||
""".format(
|
||||
select_field, date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=["customer as name", f"sum({select_field}) as value"],
|
||||
filters=filters,
|
||||
group_by="customer",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@ -96,55 +91,58 @@ def get_all_customers(date_range, company, field, limit=None):
|
||||
def get_all_items(date_range, company, field, limit=None):
|
||||
if field in ("available_stock_qty", "available_stock_value"):
|
||||
select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)"
|
||||
return frappe.db.get_all(
|
||||
results = frappe.db.get_all(
|
||||
"Bin",
|
||||
fields=["item_code as name", "{0} as value".format(select_field)],
|
||||
group_by="item_code",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
readable_active_items = set(frappe.get_list("Item", filters={"disabled": 0}, pluck="name"))
|
||||
return [item for item in results if item["name"] in readable_active_items]
|
||||
else:
|
||||
if field == "total_sales_amount":
|
||||
select_field = "sum(order_item.base_net_amount)"
|
||||
select_field = "base_net_amount"
|
||||
select_doctype = "Sales Order"
|
||||
elif field == "total_purchase_amount":
|
||||
select_field = "sum(order_item.base_net_amount)"
|
||||
select_field = "base_net_amount"
|
||||
select_doctype = "Purchase Order"
|
||||
elif field == "total_qty_sold":
|
||||
select_field = "sum(order_item.stock_qty)"
|
||||
select_field = "stock_qty"
|
||||
select_doctype = "Sales Order"
|
||||
elif field == "total_qty_purchased":
|
||||
select_field = "sum(order_item.stock_qty)"
|
||||
select_field = "stock_qty"
|
||||
select_doctype = "Purchase Order"
|
||||
|
||||
date_condition = get_date_condition(date_range, "sales_order.transaction_date")
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select order_item.item_code as name, {0} as value
|
||||
from `tab{1}` sales_order join `tab{1} Item` as order_item
|
||||
on sales_order.name = order_item.parent
|
||||
where sales_order.docstatus = 1
|
||||
and sales_order.company = %s {2}
|
||||
group by order_item.item_code
|
||||
order by value desc
|
||||
limit %s
|
||||
""".format(
|
||||
select_field, select_doctype, date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
) # nosec
|
||||
child_doctype = f"{select_doctype} Item"
|
||||
return frappe.get_list(
|
||||
select_doctype,
|
||||
fields=[
|
||||
f"`tab{child_doctype}`.item_code as name",
|
||||
f"sum(`tab{child_doctype}`.{select_field}) as value",
|
||||
],
|
||||
filters=filters,
|
||||
order_by="value desc",
|
||||
group_by=f"`tab{child_doctype}`.item_code",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_suppliers(date_range, company, field, limit=None):
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
|
||||
if field == "outstanding_amount":
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
filters.append(["posting_date", "between", [date_range[0], date_range[1]]])
|
||||
return frappe.db.get_all(
|
||||
if from_date and to_date:
|
||||
filters.append(["posting_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.get_list(
|
||||
"Purchase Invoice",
|
||||
fields=["supplier as name", "sum(outstanding_amount) as value"],
|
||||
filters=filters,
|
||||
@ -154,48 +152,40 @@ def get_all_suppliers(date_range, company, field, limit=None):
|
||||
)
|
||||
else:
|
||||
if field == "total_purchase_amount":
|
||||
select_field = "sum(purchase_order_item.base_net_amount)"
|
||||
select_field = "base_net_total"
|
||||
elif field == "total_qty_purchased":
|
||||
select_field = "sum(purchase_order_item.stock_qty)"
|
||||
select_field = "total_qty"
|
||||
|
||||
date_condition = get_date_condition(date_range, "purchase_order.modified")
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select purchase_order.supplier as name, {0} as value
|
||||
FROM `tabPurchase Order` as purchase_order LEFT JOIN `tabPurchase Order Item`
|
||||
as purchase_order_item ON purchase_order.name = purchase_order_item.parent
|
||||
where
|
||||
purchase_order.docstatus = 1
|
||||
{1}
|
||||
and purchase_order.company = %s
|
||||
group by purchase_order.supplier
|
||||
order by value DESC
|
||||
limit %s""".format(
|
||||
select_field, date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
) # nosec
|
||||
return frappe.get_list(
|
||||
"Purchase Order",
|
||||
fields=["supplier as name", f"sum({select_field}) as value"],
|
||||
filters=filters,
|
||||
group_by="supplier",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_sales_partner(date_range, company, field, limit=None):
|
||||
if field == "total_sales_amount":
|
||||
select_field = "sum(`base_net_total`)"
|
||||
select_field = "base_net_total"
|
||||
elif field == "total_commission":
|
||||
select_field = "sum(`total_commission`)"
|
||||
select_field = "total_commission"
|
||||
|
||||
filters = {"sales_partner": ["!=", ""], "docstatus": 1, "company": company}
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
filters["transaction_date"] = ["between", [date_range[0], date_range[1]]]
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company], ["sales_partner", "is", "set"]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=[
|
||||
"`sales_partner` as name",
|
||||
"{} as value".format(select_field),
|
||||
"sales_partner as name",
|
||||
f"sum({select_field}) as value",
|
||||
],
|
||||
filters=filters,
|
||||
group_by="sales_partner",
|
||||
@ -206,27 +196,29 @@ def get_all_sales_partner(date_range, company, field, limit=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_sales_person(date_range, company, field=None, limit=0):
|
||||
date_condition = get_date_condition(date_range, "sales_order.transaction_date")
|
||||
filters = [
|
||||
["docstatus", "=", "1"],
|
||||
["company", "=", company],
|
||||
["Sales Team", "sales_person", "is", "set"],
|
||||
]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select sales_team.sales_person as name, sum(sales_order.base_net_total) as value
|
||||
from `tabSales Order` as sales_order join `tabSales Team` as sales_team
|
||||
on sales_order.name = sales_team.parent and sales_team.parenttype = 'Sales Order'
|
||||
where sales_order.docstatus = 1
|
||||
and sales_order.company = %s
|
||||
{date_condition}
|
||||
group by sales_team.sales_person
|
||||
order by value DESC
|
||||
limit %s
|
||||
""".format(
|
||||
date_condition=date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=[
|
||||
"`tabSales Team`.sales_person as name",
|
||||
"sum(`tabSales Team`.allocated_amount) as value",
|
||||
],
|
||||
filters=filters,
|
||||
group_by="`tabSales Team`.sales_person",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@deprecated
|
||||
def get_date_condition(date_range, field):
|
||||
date_condition = ""
|
||||
if date_range:
|
||||
@ -236,3 +228,11 @@ def get_date_condition(date_range, field):
|
||||
field, frappe.db.escape(from_date), frappe.db.escape(to_date)
|
||||
)
|
||||
return date_condition
|
||||
|
||||
|
||||
def parse_date_range(date_range):
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
return date_range[0], date_range[1]
|
||||
|
||||
return None, None
|
||||
|
@ -121,7 +121,7 @@ frappe.ui.form.on('Serial and Batch Bundle', {
|
||||
frappe.throw(__("Please attach CSV file"));
|
||||
}
|
||||
|
||||
if (frm.doc.has_serial_no && !prompt_data.using_csv_file && !prompt_data.serial_nos) {
|
||||
if (frm.doc.has_serial_no && !prompt_data.csv_file && !prompt_data.serial_nos) {
|
||||
frappe.throw(__("Please enter serial nos"));
|
||||
}
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2022-09-29 14:56:38.338267",
|
||||
"creation": "2023-08-11 17:22:12.907518",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
@ -250,7 +250,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-28 12:56:03.072224",
|
||||
"modified": "2023-12-07 17:56:55.528563",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial and Batch Bundle",
|
||||
@ -270,6 +270,118 @@
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Purchase User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Purchase Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Stock User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Stock Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Delivery User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Delivery Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Manufacturing User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Manufacturing Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
@ -506,6 +506,22 @@ class SerialandBatchBundle(Document):
|
||||
serial_batches = {}
|
||||
|
||||
for row in self.entries:
|
||||
if self.has_serial_no and not row.serial_no:
|
||||
frappe.throw(
|
||||
_("At row {0}: Serial No is mandatory for Item {1}").format(
|
||||
bold(row.idx), bold(self.item_code)
|
||||
),
|
||||
title=_("Serial No is mandatory"),
|
||||
)
|
||||
|
||||
if self.has_batch_no and not row.batch_no:
|
||||
frappe.throw(
|
||||
_("At row {0}: Batch No is mandatory for Item {1}").format(
|
||||
bold(row.idx), bold(self.item_code)
|
||||
),
|
||||
title=_("Batch No is mandatory"),
|
||||
)
|
||||
|
||||
if row.serial_no:
|
||||
serial_nos.append(row.serial_no)
|
||||
|
||||
@ -688,6 +704,7 @@ class SerialandBatchBundle(Document):
|
||||
"item_code": self.item_code,
|
||||
"warehouse": self.warehouse,
|
||||
"batch_no": batches,
|
||||
"consider_negative_batches": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -698,6 +715,9 @@ class SerialandBatchBundle(Document):
|
||||
available_batches = get_available_batches_qty(available_batches)
|
||||
for batch_no in batches:
|
||||
if batch_no not in available_batches or available_batches[batch_no] < 0:
|
||||
if flt(available_batches.get(batch_no)) < 0:
|
||||
self.validate_negative_batch(batch_no, available_batches[batch_no])
|
||||
|
||||
self.throw_error_message(
|
||||
f"Batch {bold(batch_no)} is not available in the selected warehouse {self.warehouse}"
|
||||
)
|
||||
@ -789,6 +809,9 @@ def parse_csv_file_to_get_serial_batch(reader):
|
||||
if index == 0:
|
||||
has_serial_no = row[0] == "Serial No"
|
||||
has_batch_no = row[0] == "Batch No"
|
||||
if not has_batch_no:
|
||||
has_batch_no = row[1] == "Batch No"
|
||||
|
||||
continue
|
||||
|
||||
if not row[0]:
|
||||
@ -805,6 +828,13 @@ def parse_csv_file_to_get_serial_batch(reader):
|
||||
}
|
||||
)
|
||||
|
||||
batch_nos.append(
|
||||
{
|
||||
"batch_no": row[1],
|
||||
"qty": row[2],
|
||||
}
|
||||
)
|
||||
|
||||
serial_nos.append(_dict)
|
||||
elif has_batch_no:
|
||||
batch_nos.append(
|
||||
@ -840,6 +870,9 @@ def make_serial_nos(item_code, serial_nos):
|
||||
serial_nos_details = []
|
||||
user = frappe.session.user
|
||||
for serial_no in serial_nos:
|
||||
if frappe.db.exists("Serial No", serial_no):
|
||||
continue
|
||||
|
||||
serial_nos_details.append(
|
||||
(
|
||||
serial_no,
|
||||
@ -870,7 +903,7 @@ def make_serial_nos(item_code, serial_nos):
|
||||
|
||||
frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
|
||||
|
||||
frappe.msgprint(_("Serial Nos are created successfully"))
|
||||
frappe.msgprint(_("Serial Nos are created successfully"), alert=True)
|
||||
|
||||
|
||||
def make_batch_nos(item_code, batch_nos):
|
||||
@ -881,6 +914,9 @@ def make_batch_nos(item_code, batch_nos):
|
||||
batch_nos_details = []
|
||||
user = frappe.session.user
|
||||
for batch_no in batch_nos:
|
||||
if frappe.db.exists("Batch", batch_no):
|
||||
continue
|
||||
|
||||
batch_nos_details.append(
|
||||
(batch_no, batch_no, now(), now(), user, user, item.item_code, item.item_name, item.description)
|
||||
)
|
||||
@ -899,7 +935,7 @@ def make_batch_nos(item_code, batch_nos):
|
||||
|
||||
frappe.db.bulk_insert("Batch", fields=fields, values=set(batch_nos_details))
|
||||
|
||||
frappe.msgprint(_("Batch Nos are created successfully"))
|
||||
frappe.msgprint(_("Batch Nos are created successfully"), alert=True)
|
||||
|
||||
|
||||
def parse_serial_nos(data):
|
||||
@ -1454,7 +1490,8 @@ def get_auto_batch_nos(kwargs):
|
||||
available_batches, stock_ledgers_batches, pos_invoice_batches, sre_reserved_batches
|
||||
)
|
||||
|
||||
available_batches = list(filter(lambda x: x.qty > 0, available_batches))
|
||||
if not kwargs.consider_negative_batches:
|
||||
available_batches = list(filter(lambda x: x.qty > 0, available_batches))
|
||||
|
||||
if not qty:
|
||||
return available_batches
|
||||
|
@ -368,6 +368,58 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
# Batch does not belong to serial no
|
||||
self.assertRaises(frappe.exceptions.ValidationError, doc.save)
|
||||
|
||||
def test_auto_delete_draft_serial_and_batch_bundle(self):
|
||||
serial_and_batch_code = "New Serial No Auto Delete 1"
|
||||
make_item(
|
||||
serial_and_batch_code,
|
||||
{
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "TEST-SER-VALL-.#####",
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
ste = make_stock_entry(
|
||||
item_code=serial_and_batch_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=1,
|
||||
rate=500,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
serial_no = "SN-TEST-AUTO-DEL"
|
||||
if not frappe.db.exists("Serial No", serial_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial No",
|
||||
"serial_no": serial_no,
|
||||
"item_code": serial_and_batch_code,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
bundle_doc = make_serial_batch_bundle(
|
||||
{
|
||||
"item_code": serial_and_batch_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"voucher_type": "Stock Entry",
|
||||
"posting_date": ste.posting_date,
|
||||
"posting_time": ste.posting_time,
|
||||
"qty": 1,
|
||||
"serial_nos": [serial_no],
|
||||
"type_of_transaction": "Inward",
|
||||
"do_not_submit": True,
|
||||
}
|
||||
)
|
||||
|
||||
bundle_doc.reload()
|
||||
ste.items[0].serial_and_batch_bundle = bundle_doc.name
|
||||
ste.save()
|
||||
ste.reload()
|
||||
|
||||
ste.delete()
|
||||
self.assertFalse(frappe.db.exists("Serial and Batch Bundle", bundle_doc.name))
|
||||
|
||||
|
||||
def get_batch_from_bundle(bundle):
|
||||
from erpnext.stock.serial_batch_bundle import get_batch_nos
|
||||
|
@ -27,7 +27,6 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Serial No",
|
||||
"mandatory_depends_on": "eval:parent.has_serial_no == 1",
|
||||
"options": "Serial No",
|
||||
"search_index": 1
|
||||
},
|
||||
@ -38,7 +37,6 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Batch No",
|
||||
"mandatory_depends_on": "eval:parent.has_batch_no == 1",
|
||||
"options": "Batch",
|
||||
"search_index": 1
|
||||
},
|
||||
@ -122,7 +120,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-03 15:29:50.199075",
|
||||
"modified": "2023-12-10 19:47:48.227772",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial and Batch Entry",
|
||||
|
@ -1737,6 +1737,45 @@ class TestStockEntry(FrappeTestCase):
|
||||
self.assertFalse(doc.is_enqueue_action())
|
||||
frappe.flags.in_test = True
|
||||
|
||||
def test_negative_batch(self):
|
||||
item_code = "Test Negative Batch Item - 001"
|
||||
make_item(
|
||||
item_code,
|
||||
{"has_batch_no": 1, "create_new_batch": 1, "batch_naming_series": "Test-BCH-NNS.#####"},
|
||||
)
|
||||
|
||||
se1 = make_stock_entry(
|
||||
item_code=item_code,
|
||||
purpose="Material Receipt",
|
||||
qty=100,
|
||||
target="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
se1.reload()
|
||||
|
||||
batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
|
||||
|
||||
se2 = make_stock_entry(
|
||||
item_code=item_code,
|
||||
purpose="Material Issue",
|
||||
batch_no=batch_no,
|
||||
qty=10,
|
||||
source="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
se2.reload()
|
||||
|
||||
se3 = make_stock_entry(
|
||||
item_code=item_code,
|
||||
purpose="Material Receipt",
|
||||
qty=100,
|
||||
target="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
se3.reload()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, se1.cancel)
|
||||
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
|
@ -209,7 +209,7 @@ frappe.ui.form.on("Stock Reconciliation", {
|
||||
|
||||
set_amount_quantity: function(doc, cdt, cdn) {
|
||||
var d = frappe.model.get_doc(cdt, cdn);
|
||||
if (d.qty & d.valuation_rate) {
|
||||
if (d.qty && d.valuation_rate) {
|
||||
frappe.model.set_value(cdt, cdn, "amount", flt(d.qty) * flt(d.valuation_rate));
|
||||
frappe.model.set_value(cdt, cdn, "quantity_difference", flt(d.qty) - flt(d.current_qty));
|
||||
frappe.model.set_value(cdt, cdn, "amount_difference", flt(d.amount) - flt(d.current_amount));
|
||||
|
@ -1,9 +1,12 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import copy
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_serial_nos_from_sle
|
||||
from erpnext.stock.stock_ledger import get_stock_ledger_entries
|
||||
|
||||
|
||||
@ -15,8 +18,8 @@ def execute(filters=None):
|
||||
|
||||
def get_columns(filters):
|
||||
columns = [
|
||||
{"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date"},
|
||||
{"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time"},
|
||||
{"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date", "width": 120},
|
||||
{"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time", "width": 90},
|
||||
{
|
||||
"label": _("Voucher Type"),
|
||||
"fieldtype": "Link",
|
||||
@ -29,7 +32,7 @@ def get_columns(filters):
|
||||
"fieldtype": "Dynamic Link",
|
||||
"fieldname": "voucher_no",
|
||||
"options": "voucher_type",
|
||||
"width": 180,
|
||||
"width": 230,
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
@ -49,7 +52,7 @@ def get_columns(filters):
|
||||
"label": _("Status"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "status",
|
||||
"width": 120,
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Serial No"),
|
||||
@ -62,7 +65,7 @@ def get_columns(filters):
|
||||
"label": _("Valuation Rate"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "valuation_rate",
|
||||
"width": 150,
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
"label": _("Qty"),
|
||||
@ -102,15 +105,29 @@ def get_data(filters):
|
||||
}
|
||||
)
|
||||
|
||||
serial_nos = [{"serial_no": row.serial_no, "valuation_rate": row.valuation_rate}]
|
||||
serial_nos = []
|
||||
if row.serial_no:
|
||||
parsed_serial_nos = get_serial_nos_from_sle(row.serial_no)
|
||||
for serial_no in parsed_serial_nos:
|
||||
if filters.get("serial_no") and filters.get("serial_no") != serial_no:
|
||||
continue
|
||||
|
||||
serial_nos.append(
|
||||
{
|
||||
"serial_no": serial_no,
|
||||
"valuation_rate": abs(row.stock_value_difference / row.actual_qty),
|
||||
}
|
||||
)
|
||||
|
||||
if row.serial_and_batch_bundle:
|
||||
serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle, [])
|
||||
serial_nos.extend(bundle_wise_serial_nos.get(row.serial_and_batch_bundle, []))
|
||||
|
||||
for index, bundle_data in enumerate(serial_nos):
|
||||
if index == 0:
|
||||
args.serial_no = bundle_data.get("serial_no")
|
||||
args.valuation_rate = bundle_data.get("valuation_rate")
|
||||
data.append(args)
|
||||
new_args = copy.deepcopy(args)
|
||||
new_args.serial_no = bundle_data.get("serial_no")
|
||||
new_args.valuation_rate = bundle_data.get("valuation_rate")
|
||||
data.append(new_args)
|
||||
else:
|
||||
data.append(
|
||||
{
|
||||
|
@ -7,6 +7,7 @@ from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import update_status as update_po_status
|
||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||
from erpnext.stock.stock_balance import update_bin_qty
|
||||
from erpnext.stock.utils import get_bin
|
||||
@ -308,6 +309,9 @@ class SubcontractingOrder(SubcontractingController):
|
||||
"Subcontracting Order", self.name, "status", status, update_modified=update_modified
|
||||
)
|
||||
|
||||
if status == "Closed":
|
||||
update_po_status("Closed", self.purchase_order)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontracting_receipt(source_name, target_doc=None):
|
||||
|
Loading…
x
Reference in New Issue
Block a user