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 return data
def split_invoices_based_on_payment_terms(outstanding_invoices, company): def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list:
invoice_ref_based_on_payment_terms = {} """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 = ( company_currency = (
frappe.db.get_value("Company", company, "default_currency") if company else None frappe.db.get_value("Company", company, "default_currency") if company else None
) )
exc_rates = frappe._dict()
for doctype in ["Sales Invoice", "Purchase Invoice"]: for doctype in ["Sales Invoice", "Purchase Invoice"]:
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype] invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
for x in frappe.db.get_all( 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, company_currency=company_currency,
) )
for idx, d in enumerate(outstanding_invoices): return exc_rates
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" 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: return split_rows
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
def get_orders_to_be_billed( def get_orders_to_be_billed(

View File

@ -6,11 +6,12 @@ import unittest
import frappe import frappe
from frappe import qb from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings 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.account.test_account import create_account
from erpnext.accounts.doctype.payment_entry.payment_entry import ( from erpnext.accounts.doctype.payment_entry.payment_entry import (
InvalidPaymentEntry, InvalidPaymentEntry,
get_outstanding_reference_documents,
get_payment_entry, get_payment_entry,
get_reference_details, get_reference_details,
) )
@ -1471,6 +1472,45 @@ class TestPaymentEntry(FrappeTestCase):
for field in ["account", "debit", "credit"]: for field in ["account", "debit", "credit"]:
self.assertEqual(self.expected_gle[row][field], gl_entries[row][field]) 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): def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry") payment_entry = frappe.new_doc("Payment Entry")
@ -1531,6 +1571,9 @@ def create_payment_terms_template():
def create_payment_terms_template_with_discount( def create_payment_terms_template_with_discount(
name=None, discount_type=None, discount=None, template_name=None 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") create_payment_term(name or "30 Credit Days with 10% Discount")
template_name = template_name or "Test Discount Template" template_name = template_name or "Test Discount Template"

View File

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

View File

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

View File

@ -30,4 +30,6 @@ class PaymentReconciliationPayment(Document):
remark: DF.SmallText | None remark: DF.SmallText | None
# end: auto-generated types # 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.price_or_product_discount == "Price":
if d.apply_discount_on: if d.apply_discount_on:
doc.set("apply_discount_on", 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"]: for field in ["additional_discount_percentage", "discount_amount"]:
pr_field = "discount_percentage" if field == "additional_discount_percentage" else field 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 coupon_code_pricing_rule == d.name:
# if selected coupon code is linked with pricing rule # if selected coupon code is linked with pricing rule
doc.set(field, d.get(pr_field)) 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: else:
# reset discount if not linked # reset discount if not linked
doc.set(field, 0) doc.set(field, 0)
@ -611,6 +618,10 @@ def apply_pricing_rule_on_transaction(doc):
doc.set(field, 0) doc.set(field, 0)
doc.calculate_taxes_and_totals() 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": elif d.price_or_product_discount == "Product":
item_details = frappe._dict({"parenttype": doc.doctype, "free_item_data": []}) item_details = frappe._dict({"parenttype": doc.doctype, "free_item_data": []})
get_product_discount_rule(d, item_details, doc=doc) get_product_discount_rule(d, item_details, doc=doc)

View File

@ -126,7 +126,7 @@ class RepostAccountingLedger(Document):
return rendered_page return rendered_page
def on_submit(self): def on_submit(self):
if len(self.vouchers) > 1: if len(self.vouchers) > 5:
job_name = "repost_accounting_ledger_" + self.name job_name = "repost_accounting_ledger_" + self.name
frappe.enqueue( frappe.enqueue(
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost", 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(1)
doc.make_gl_entries() doc.make_gl_entries()
frappe.db.commit()
def get_allowed_types_from_settings(): def get_allowed_types_from_settings():
return [ return [

View File

@ -20,18 +20,11 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
self.create_company() self.create_company()
self.create_customer() self.create_customer()
self.create_item() self.create_item()
self.update_repost_settings() update_repost_settings()
def teadDown(self): def tearDown(self):
frappe.db.rollback() 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): def test_01_basic_functions(self):
si = create_sales_invoice( si = create_sales_invoice(
item=self.item, item=self.item,
@ -90,9 +83,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
# Submit repost document # Submit repost document
ral.save().submit() ral.save().submit()
# background jobs don't run on test cases. Manually triggering repost function.
start_repost(ral.name)
res = ( res = (
qb.from_(gl) qb.from_(gl)
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit")) .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 = get_payment_entry(si.doctype, si.name)
pe.save().submit() 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 # with deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger") ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company ral.company = self.company
@ -205,6 +175,38 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name}) ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
ral.save().submit() 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": si.name, "is_cancelled": 1}))
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.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}) @change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self): 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( additional_discount_account = create_account(
account_name="Discount Account", account_name="Discount Account",
parent_account="Indirect Expenses - _TC", parent_account="Indirect Expenses - _TC",

View File

@ -8,7 +8,17 @@ import re
import frappe import frappe
from frappe import _ 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 ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
@ -43,6 +53,8 @@ def get_period_list(
year_start_date = getdate(period_start_date) year_start_date = getdate(period_start_date)
year_end_date = getdate(period_end_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] months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity]
period_list = [] period_list = []

View File

@ -340,6 +340,10 @@ class AssetDepreciationSchedule(Document):
n == 0 n == 0
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata) and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
and not self.opening_accumulated_depreciation 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( from_date = add_days(
asset_doc.available_for_use_date, -1 asset_doc.available_for_use_date, -1
@ -605,7 +609,9 @@ def get_depreciation_amount(
@erpnext.allow_regional @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 return fb_row.rate_of_depreciation

View File

@ -292,6 +292,7 @@ class AccountsController(TransactionBase):
def on_trash(self): def on_trash(self):
self._remove_references_in_repost_doctypes() self._remove_references_in_repost_doctypes()
self._remove_references_in_unreconcile() self._remove_references_in_unreconcile()
self.remove_serial_and_batch_bundle()
# delete sl and gl entries on deletion of transaction # delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
@ -307,6 +308,15 @@ class AccountsController(TransactionBase):
(self.doctype, self.name), (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): def validate_deferred_income_expense_account(self):
field_map = { field_map = {
"Sales Invoice": "deferred_revenue_account", "Sales Invoice": "deferred_revenue_account",

View File

@ -637,6 +637,7 @@ additional_timeline_content = {
extend_bootinfo = [ extend_bootinfo = [
"erpnext.support.doctype.service_level_agreement.service_level_agreement.add_sla_doctypes", "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 // no idea where me is coming from
if(this.frm.get_field('shipping_address')) { if(this.frm.get_field('shipping_address')) {
this.frm.set_query("shipping_address", function() { this.frm.set_query("shipping_address", () => {
if(me.frm.doc.customer) { if(this.frm.doc.customer) {
return { return {
query: 'frappe.contacts.doctype.address.address.address_query', 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 } 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 = '' item.pricing_rules = ''
return this.frm.call({ return this.frm.call({
method: "erpnext.stock.get_item_details.get_item_details", method: "erpnext.stock.get_item_details.get_item_details",
child: item,
args: { args: {
doc: me.frm.doc, doc: me.frm.doc,
args: { args: {
@ -520,6 +519,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
callback: function(r) { callback: function(r) {
if(!r.exc) { if(!r.exc) {
frappe.run_serially([ 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]; var d = locals[cdt][cdn];
me.add_taxes_from_item_tax_template(d.item_tax_rate); 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 = { erpnext.financial_statements = {
"filters": get_filters(), "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") { if (data && column.fieldname=="account") {
value = data.account_name || value; value = data.account_name || value;
if (filter && filter?.text && filter?.type == "contains") {
if (!value.toLowerCase().includes(filter.text)) {
return value;
}
}
if (data.account) { if (data.account) {
column.link_onclick = column.link_onclick =
"erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")"; "erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")";

View File

@ -8,7 +8,7 @@ $.extend(erpnext, {
if(!company && cur_frm) if(!company && cur_frm)
company = cur_frm.doc.company; company = cur_frm.doc.company;
if(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 else
return frappe.boot.sysdefaults.currency; return frappe.boot.sysdefaults.currency;
}, },

View File

@ -31,7 +31,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
secondary_action: () => this.edit_full_form(), 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) { if (this.item.serial_no) {
this.dialog.set_value("scan_serial_no", 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', ''); 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 # License: GNU General Public License v3. See license.txt
import os
import json import json
import os
import frappe import frappe
from frappe import _ from frappe import _
@ -114,10 +114,11 @@ def update_regional_tax_settings(country, company):
frappe.scrub(country) frappe.scrub(country)
) )
frappe.get_attr(module_name)(country, company) 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 # Log error and ignore if failed to setup regional tax settings
frappe.log_error("Unable 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): 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"}, "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 import frappe
from frappe.utils import cint from frappe.utils.deprecations import deprecated
def get_leaderboards(): def get_leaderboards():
@ -54,12 +54,13 @@ def get_leaderboards():
@frappe.whitelist() @frappe.whitelist()
def get_all_customers(date_range, company, field, limit=None): 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": if field == "outstanding_amount":
filters = [["docstatus", "=", "1"], ["company", "=", company]] if from_date and to_date:
if date_range: filters.append(["posting_date", "between", [from_date, to_date]])
date_range = frappe.parse_json(date_range)
filters.append(["posting_date", ">=", "between", [date_range[0], date_range[1]]]) return frappe.get_list(
return frappe.db.get_all(
"Sales Invoice", "Sales Invoice",
fields=["customer as name", "sum(outstanding_amount) as value"], fields=["customer as name", "sum(outstanding_amount) as value"],
filters=filters, filters=filters,
@ -69,26 +70,20 @@ def get_all_customers(date_range, company, field, limit=None):
) )
else: else:
if field == "total_sales_amount": if field == "total_sales_amount":
select_field = "sum(so_item.base_net_amount)" select_field = "base_net_total"
elif field == "total_qty_sold": 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( return frappe.get_list(
""" "Sales Order",
select so.customer as name, {0} as value fields=["customer as name", f"sum({select_field}) as value"],
FROM `tabSales Order` as so JOIN `tabSales Order Item` as so_item filters=filters,
ON so.name = so_item.parent group_by="customer",
where so.docstatus = 1 {1} and so.company = %s order_by="value desc",
group by so.customer limit=limit,
order by value DESC
limit %s
""".format(
select_field, date_condition
),
(company, cint(limit)),
as_dict=1,
) )
@ -96,55 +91,58 @@ def get_all_customers(date_range, company, field, limit=None):
def get_all_items(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"): if field in ("available_stock_qty", "available_stock_value"):
select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(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", "Bin",
fields=["item_code as name", "{0} as value".format(select_field)], fields=["item_code as name", "{0} as value".format(select_field)],
group_by="item_code", group_by="item_code",
order_by="value desc", order_by="value desc",
limit=limit, 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: else:
if field == "total_sales_amount": if field == "total_sales_amount":
select_field = "sum(order_item.base_net_amount)" select_field = "base_net_amount"
select_doctype = "Sales Order" select_doctype = "Sales Order"
elif field == "total_purchase_amount": elif field == "total_purchase_amount":
select_field = "sum(order_item.base_net_amount)" select_field = "base_net_amount"
select_doctype = "Purchase Order" select_doctype = "Purchase Order"
elif field == "total_qty_sold": elif field == "total_qty_sold":
select_field = "sum(order_item.stock_qty)" select_field = "stock_qty"
select_doctype = "Sales Order" select_doctype = "Sales Order"
elif field == "total_qty_purchased": elif field == "total_qty_purchased":
select_field = "sum(order_item.stock_qty)" select_field = "stock_qty"
select_doctype = "Purchase Order" 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( child_doctype = f"{select_doctype} Item"
""" return frappe.get_list(
select order_item.item_code as name, {0} as value select_doctype,
from `tab{1}` sales_order join `tab{1} Item` as order_item fields=[
on sales_order.name = order_item.parent f"`tab{child_doctype}`.item_code as name",
where sales_order.docstatus = 1 f"sum(`tab{child_doctype}`.{select_field}) as value",
and sales_order.company = %s {2} ],
group by order_item.item_code filters=filters,
order by value desc order_by="value desc",
limit %s group_by=f"`tab{child_doctype}`.item_code",
""".format( limit=limit,
select_field, select_doctype, date_condition )
),
(company, cint(limit)),
as_dict=1,
) # nosec
@frappe.whitelist() @frappe.whitelist()
def get_all_suppliers(date_range, company, field, limit=None): 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": if field == "outstanding_amount":
filters = [["docstatus", "=", "1"], ["company", "=", company]] if from_date and to_date:
if date_range: filters.append(["posting_date", "between", [from_date, to_date]])
date_range = frappe.parse_json(date_range)
filters.append(["posting_date", "between", [date_range[0], date_range[1]]]) return frappe.get_list(
return frappe.db.get_all(
"Purchase Invoice", "Purchase Invoice",
fields=["supplier as name", "sum(outstanding_amount) as value"], fields=["supplier as name", "sum(outstanding_amount) as value"],
filters=filters, filters=filters,
@ -154,48 +152,40 @@ def get_all_suppliers(date_range, company, field, limit=None):
) )
else: else:
if field == "total_purchase_amount": if field == "total_purchase_amount":
select_field = "sum(purchase_order_item.base_net_amount)" select_field = "base_net_total"
elif field == "total_qty_purchased": 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( return frappe.get_list(
""" "Purchase Order",
select purchase_order.supplier as name, {0} as value fields=["supplier as name", f"sum({select_field}) as value"],
FROM `tabPurchase Order` as purchase_order LEFT JOIN `tabPurchase Order Item` filters=filters,
as purchase_order_item ON purchase_order.name = purchase_order_item.parent group_by="supplier",
where order_by="value desc",
purchase_order.docstatus = 1 limit=limit,
{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
@frappe.whitelist() @frappe.whitelist()
def get_all_sales_partner(date_range, company, field, limit=None): def get_all_sales_partner(date_range, company, field, limit=None):
if field == "total_sales_amount": if field == "total_sales_amount":
select_field = "sum(`base_net_total`)" select_field = "base_net_total"
elif field == "total_commission": elif field == "total_commission":
select_field = "sum(`total_commission`)" select_field = "total_commission"
filters = {"sales_partner": ["!=", ""], "docstatus": 1, "company": company} filters = [["docstatus", "=", "1"], ["company", "=", company], ["sales_partner", "is", "set"]]
if date_range: from_date, to_date = parse_date_range(date_range)
date_range = frappe.parse_json(date_range) if from_date and to_date:
filters["transaction_date"] = ["between", [date_range[0], date_range[1]]] filters.append(["transaction_date", "between", [from_date, to_date]])
return frappe.get_list( return frappe.get_list(
"Sales Order", "Sales Order",
fields=[ fields=[
"`sales_partner` as name", "sales_partner as name",
"{} as value".format(select_field), f"sum({select_field}) as value",
], ],
filters=filters, filters=filters,
group_by="sales_partner", group_by="sales_partner",
@ -206,27 +196,29 @@ def get_all_sales_partner(date_range, company, field, limit=None):
@frappe.whitelist() @frappe.whitelist()
def get_all_sales_person(date_range, company, field=None, limit=0): 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( return frappe.get_list(
""" "Sales Order",
select sales_team.sales_person as name, sum(sales_order.base_net_total) as value fields=[
from `tabSales Order` as sales_order join `tabSales Team` as sales_team "`tabSales Team`.sales_person as name",
on sales_order.name = sales_team.parent and sales_team.parenttype = 'Sales Order' "sum(`tabSales Team`.allocated_amount) as value",
where sales_order.docstatus = 1 ],
and sales_order.company = %s filters=filters,
{date_condition} group_by="`tabSales Team`.sales_person",
group by sales_team.sales_person order_by="value desc",
order by value DESC limit=limit,
limit %s
""".format(
date_condition=date_condition
),
(company, cint(limit)),
as_dict=1,
) )
@deprecated
def get_date_condition(date_range, field): def get_date_condition(date_range, field):
date_condition = "" date_condition = ""
if date_range: 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) field, frappe.db.escape(from_date), frappe.db.escape(to_date)
) )
return date_condition 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")); 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")); frappe.throw(__("Please enter serial nos"));
} }
}, },

View File

@ -1,7 +1,7 @@
{ {
"actions": [], "actions": [],
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2022-09-29 14:56:38.338267", "creation": "2023-08-11 17:22:12.907518",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
@ -250,7 +250,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-07-28 12:56:03.072224", "modified": "2023-12-07 17:56:55.528563",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Serial and Batch Bundle", "name": "Serial and Batch Bundle",
@ -270,6 +270,118 @@
"share": 1, "share": 1,
"submit": 1, "submit": 1,
"write": 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", "sort_field": "modified",

View File

@ -506,6 +506,22 @@ class SerialandBatchBundle(Document):
serial_batches = {} serial_batches = {}
for row in self.entries: 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: if row.serial_no:
serial_nos.append(row.serial_no) serial_nos.append(row.serial_no)
@ -688,6 +704,7 @@ class SerialandBatchBundle(Document):
"item_code": self.item_code, "item_code": self.item_code,
"warehouse": self.warehouse, "warehouse": self.warehouse,
"batch_no": batches, "batch_no": batches,
"consider_negative_batches": True,
} }
) )
) )
@ -698,6 +715,9 @@ class SerialandBatchBundle(Document):
available_batches = get_available_batches_qty(available_batches) available_batches = get_available_batches_qty(available_batches)
for batch_no in batches: for batch_no in batches:
if batch_no not in available_batches or available_batches[batch_no] < 0: 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( self.throw_error_message(
f"Batch {bold(batch_no)} is not available in the selected warehouse {self.warehouse}" 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: if index == 0:
has_serial_no = row[0] == "Serial No" has_serial_no = row[0] == "Serial No"
has_batch_no = row[0] == "Batch No" has_batch_no = row[0] == "Batch No"
if not has_batch_no:
has_batch_no = row[1] == "Batch No"
continue continue
if not row[0]: 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) serial_nos.append(_dict)
elif has_batch_no: elif has_batch_no:
batch_nos.append( batch_nos.append(
@ -840,6 +870,9 @@ def make_serial_nos(item_code, serial_nos):
serial_nos_details = [] serial_nos_details = []
user = frappe.session.user user = frappe.session.user
for serial_no in serial_nos: for serial_no in serial_nos:
if frappe.db.exists("Serial No", serial_no):
continue
serial_nos_details.append( serial_nos_details.append(
( (
serial_no, 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.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): def make_batch_nos(item_code, batch_nos):
@ -881,6 +914,9 @@ def make_batch_nos(item_code, batch_nos):
batch_nos_details = [] batch_nos_details = []
user = frappe.session.user user = frappe.session.user
for batch_no in batch_nos: for batch_no in batch_nos:
if frappe.db.exists("Batch", batch_no):
continue
batch_nos_details.append( batch_nos_details.append(
(batch_no, batch_no, now(), now(), user, user, item.item_code, item.item_name, item.description) (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.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): 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, 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: if not qty:
return available_batches return available_batches

View File

@ -368,6 +368,58 @@ class TestSerialandBatchBundle(FrappeTestCase):
# Batch does not belong to serial no # Batch does not belong to serial no
self.assertRaises(frappe.exceptions.ValidationError, doc.save) 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): def get_batch_from_bundle(bundle):
from erpnext.stock.serial_batch_bundle import get_batch_nos from erpnext.stock.serial_batch_bundle import get_batch_nos

View File

@ -27,7 +27,6 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Serial No", "label": "Serial No",
"mandatory_depends_on": "eval:parent.has_serial_no == 1",
"options": "Serial No", "options": "Serial No",
"search_index": 1 "search_index": 1
}, },
@ -38,7 +37,6 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Batch No", "label": "Batch No",
"mandatory_depends_on": "eval:parent.has_batch_no == 1",
"options": "Batch", "options": "Batch",
"search_index": 1 "search_index": 1
}, },
@ -122,7 +120,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-07-03 15:29:50.199075", "modified": "2023-12-10 19:47:48.227772",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Serial and Batch Entry", "name": "Serial and Batch Entry",

View File

@ -1737,6 +1737,45 @@ class TestStockEntry(FrappeTestCase):
self.assertFalse(doc.is_enqueue_action()) self.assertFalse(doc.is_enqueue_action())
frappe.flags.in_test = True 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): def make_serialized_item(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -209,7 +209,7 @@ frappe.ui.form.on("Stock Reconciliation", {
set_amount_quantity: function(doc, cdt, cdn) { set_amount_quantity: function(doc, cdt, cdn) {
var d = frappe.model.get_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, "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, "quantity_difference", flt(d.qty) - flt(d.current_qty));
frappe.model.set_value(cdt, cdn, "amount_difference", flt(d.amount) - flt(d.current_amount)); 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 # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
import copy
import frappe import frappe
from frappe import _ 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 from erpnext.stock.stock_ledger import get_stock_ledger_entries
@ -15,8 +18,8 @@ def execute(filters=None):
def get_columns(filters): def get_columns(filters):
columns = [ columns = [
{"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date"}, {"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date", "width": 120},
{"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time"}, {"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time", "width": 90},
{ {
"label": _("Voucher Type"), "label": _("Voucher Type"),
"fieldtype": "Link", "fieldtype": "Link",
@ -29,7 +32,7 @@ def get_columns(filters):
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"fieldname": "voucher_no", "fieldname": "voucher_no",
"options": "voucher_type", "options": "voucher_type",
"width": 180, "width": 230,
}, },
{ {
"label": _("Company"), "label": _("Company"),
@ -49,7 +52,7 @@ def get_columns(filters):
"label": _("Status"), "label": _("Status"),
"fieldtype": "Data", "fieldtype": "Data",
"fieldname": "status", "fieldname": "status",
"width": 120, "width": 90,
}, },
{ {
"label": _("Serial No"), "label": _("Serial No"),
@ -62,7 +65,7 @@ def get_columns(filters):
"label": _("Valuation Rate"), "label": _("Valuation Rate"),
"fieldtype": "Float", "fieldtype": "Float",
"fieldname": "valuation_rate", "fieldname": "valuation_rate",
"width": 150, "width": 130,
}, },
{ {
"label": _("Qty"), "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: 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): for index, bundle_data in enumerate(serial_nos):
if index == 0: if index == 0:
args.serial_no = bundle_data.get("serial_no") new_args = copy.deepcopy(args)
args.valuation_rate = bundle_data.get("valuation_rate") new_args.serial_no = bundle_data.get("serial_no")
data.append(args) new_args.valuation_rate = bundle_data.get("valuation_rate")
data.append(new_args)
else: else:
data.append( data.append(
{ {

View File

@ -7,6 +7,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.utils import flt 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 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.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.stock_balance import update_bin_qty from erpnext.stock.stock_balance import update_bin_qty
from erpnext.stock.utils import get_bin from erpnext.stock.utils import get_bin
@ -308,6 +309,9 @@ class SubcontractingOrder(SubcontractingController):
"Subcontracting Order", self.name, "status", status, update_modified=update_modified "Subcontracting Order", self.name, "status", status, update_modified=update_modified
) )
if status == "Closed":
update_po_status("Closed", self.purchase_order)
@frappe.whitelist() @frappe.whitelist()
def make_subcontracting_receipt(source_name, target_doc=None): def make_subcontracting_receipt(source_name, target_doc=None):