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

chore: release v15
This commit is contained in:
Deepesh Garg 2023-12-12 21:40:42 +05:30 committed by GitHub
commit 0d8a52f63b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 628 additions and 235 deletions

View File

@ -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(

View File

@ -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"

View File

@ -34,4 +34,6 @@ class PaymentReconciliationAllocation(Document):
unreconciled_amount: DF.Currency
# end: auto-generated types
pass
@staticmethod
def get_list(args):
pass

View File

@ -26,4 +26,6 @@ class PaymentReconciliationInvoice(Document):
parenttype: DF.Data
# end: auto-generated types
pass
@staticmethod
def get_list(args):
pass

View File

@ -30,4 +30,6 @@ class PaymentReconciliationPayment(Document):
remark: DF.SmallText | None
# end: auto-generated types
pass
@staticmethod
def get_list(args):
pass

View File

@ -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)

View File

@ -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 [

View File

@ -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()

View File

@ -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",

View File

@ -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 = []

View File

@ -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

View File

@ -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",

View File

@ -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",
]

View File

@ -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)
});
}
}

View File

@ -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);

View File

@ -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) + ")";

View File

@ -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;
},

View File

@ -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', '');

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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"));
}
},

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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)

View File

@ -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));

View File

@ -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(
{

View File

@ -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):