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

chore: release v15
This commit is contained in:
ruthra kumar
2024-11-27 21:30:10 +05:30
committed by GitHub
63 changed files with 708 additions and 315 deletions

View File

@@ -515,6 +515,55 @@ class TestJournalEntry(unittest.TestCase):
self.assertEqual(row.debit_in_account_currency, 100)
self.assertEqual(row.credit_in_account_currency, 100)
def test_toggle_debit_credit_if_negative(self):
from erpnext.accounts.general_ledger import process_gl_map
# Create JV with defaut cost center - _Test Cost Center
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
jv = frappe.new_doc("Journal Entry")
jv.posting_date = nowdate()
jv.company = "_Test Company"
jv.user_remark = "test"
jv.extend(
"accounts",
[
{
"account": "_Test Cash - _TC",
"debit": 100 * -1,
"debit_in_account_currency": 100 * -1,
"exchange_rate": 1,
},
{
"account": "_Test Bank - _TC",
"credit": 100 * -1,
"credit_in_account_currency": 100 * -1,
"exchange_rate": 1,
},
],
)
jv.flags.ignore_validate = True
jv.save()
self.assertEqual(len(jv.accounts), 2)
gl_map = jv.build_gl_map()
for row in gl_map:
if row.account == "_Test Cash - _TC":
self.assertEqual(row.debit, 100 * -1)
self.assertEqual(row.debit_in_account_currency, 100 * -1)
self.assertEqual(row.debit_in_transaction_currency, 100 * -1)
gl_map = process_gl_map(gl_map, False)
for row in gl_map:
if row.account == "_Test Cash - _TC":
self.assertEqual(row.credit, 100)
self.assertEqual(row.credit_in_account_currency, 100)
self.assertEqual(row.credit_in_transaction_currency, 100)
def test_transaction_exchange_rate_on_journals(self):
jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable USD - _TC", 100, save=False)
jv.accounts[0].update({"debit_in_account_currency": 8500, "exchange_rate": 1})

View File

@@ -1219,11 +1219,19 @@ class PaymentEntry(AccountsController):
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
gle.update(
{
dr_or_cr: allocated_amount_in_company_currency,
dr_or_cr + "_in_account_currency": d.allocated_amount,
"cost_center": cost_center,
}
self.get_gl_dict(
{
"account": self.party_account,
"party_type": self.party_type,
"party": self.party,
"against": against_account,
"account_currency": self.party_account_currency,
"cost_center": cost_center,
dr_or_cr + "_in_account_currency": d.allocated_amount,
dr_or_cr: allocated_amount_in_company_currency,
},
item=self,
)
)
if self.book_advance_payments_in_separate_party_account:
@@ -1746,7 +1754,7 @@ class PaymentEntry(AccountsController):
if paid_amount > total_negative_outstanding:
if total_negative_outstanding == 0:
frappe.msgprint(
_("Cannot {0} from {2} without any negative outstanding invoice").format(
_("Cannot {0} from {1} without any negative outstanding invoice").format(
self.payment_type,
self.party_type,
)

View File

@@ -3,7 +3,7 @@ import json
import frappe
from frappe import _, qb
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.query_builder.functions import Abs, Sum
from frappe.utils import flt, nowdate
from frappe.utils.background_jobs import enqueue
@@ -564,6 +564,8 @@ def make_payment_request(**args):
# fetches existing payment request `grand_total` amount
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
existing_paid_amount = get_existing_paid_amount(ref_doc.doctype, ref_doc.name)
def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount):
grand_total -= existing_payment_request_amount
if not grand_total:
@@ -583,6 +585,15 @@ def make_payment_request(**args):
else:
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
if existing_paid_amount:
if ref_doc.party_account_currency == ref_doc.currency:
if ref_doc.conversion_rate:
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
else:
grand_total -= flt(existing_paid_amount)
else:
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
if draft_payment_request:
frappe.db.set_value(
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
@@ -753,6 +764,27 @@ def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None =
return response[0][0] if response[0] else 0
def get_existing_paid_amount(doctype, name):
PL = frappe.qb.DocType("Payment Ledger Entry")
PER = frappe.qb.DocType("Payment Entry Reference")
query = (
frappe.qb.from_(PL)
.left_join(PER)
.on(
(PER.reference_doctype == PL.against_voucher_type) & (PER.reference_name == PL.against_voucher_no)
)
.select(Abs(Sum(PL.amount)).as_("total_paid_amount"))
.where(PL.against_voucher_type.eq(doctype))
.where(PL.against_voucher_no.eq(name))
.where(PL.amount < 0)
.where(PER.payment_request.isnull())
)
response = query.run()
return response[0][0] if response[0] else 0
def get_gateway_details(args): # nosemgrep
"""
Return gateway and payment account of default payment gateway

View File

@@ -7,6 +7,7 @@ import unittest
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
@@ -524,3 +525,21 @@ class TestPaymentRequest(FrappeTestCase):
self.assertEqual(pr.grand_total, 1000)
so.load_from_db()
def test_partial_paid_invoice_with_payment_request(self):
si = create_sales_invoice(currency="INR", qty=1, rate=5000)
si.save()
si.submit()
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
pe.reference_no = "PAYEE0002"
pe.reference_date = frappe.utils.nowdate()
pe.paid_amount = 2500
pe.references[0].allocated_amount = 2500
pe.save()
pe.submit()
si.load_from_db()
pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
self.assertEqual(pr.grand_total, si.outstanding_amount)

View File

@@ -171,9 +171,7 @@ class PeriodClosingVoucher(AccountsController):
pl_account_balances = self.get_account_balances_based_on_dimensions(report_type="Profit and Loss")
for dimensions, account_balances in pl_account_balances.items():
for acc, balances in account_balances.items():
balance_in_company_currency = flt(balances.debit_in_account_currency) - flt(
balances.credit_in_account_currency
)
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
if balance_in_company_currency and acc != "balances":
self.pl_accounts_reverse_gle.append(
self.get_gle_for_pl_account(acc, balances, dimensions)

View File

@@ -147,7 +147,7 @@ frappe.ui.form.on("POS Closing Entry", {
frm.doc.grand_total += flt(doc.grand_total);
frm.doc.net_total += flt(doc.net_total);
frm.doc.total_quantity += flt(doc.total_qty);
refresh_payments(doc, frm);
refresh_payments(doc, frm, false);
refresh_taxes(doc, frm);
refresh_fields(frm);
set_html_data(frm);
@@ -172,7 +172,7 @@ function set_form_data(data, frm) {
frm.doc.grand_total += flt(d.grand_total);
frm.doc.net_total += flt(d.net_total);
frm.doc.total_quantity += flt(d.total_qty);
refresh_payments(d, frm);
refresh_payments(d, frm, true);
refresh_taxes(d, frm);
});
}
@@ -186,7 +186,7 @@ function add_to_pos_transaction(d, frm) {
});
}
function refresh_payments(d, frm) {
function refresh_payments(d, frm, is_new) {
d.payments.forEach((p) => {
const payment = frm.doc.payment_reconciliation.find(
(pay) => pay.mode_of_payment === p.mode_of_payment
@@ -196,9 +196,7 @@ function refresh_payments(d, frm) {
}
if (payment) {
payment.expected_amount += flt(p.amount);
if (payment.closing_amount === 0) {
payment.closing_amount = payment.expected_amount;
}
if (is_new) payment.closing_amount = payment.expected_amount;
payment.difference = payment.closing_amount - payment.expected_amount;
} else {
frm.add_child("payment_reconciliation", {

View File

@@ -1137,6 +1137,45 @@ class TestPricingRule(FrappeTestCase):
so.save()
self.assertEqual(len(so.items), 1)
def test_pricing_rule_for_product_free_item_round_free_qty(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"currency": "USD",
"items": [
{
"item_code": "_Test Item",
}
],
"selling": 1,
"rate": 0,
"min_qty": 100,
"max_qty": 0,
"price_or_product_discount": "Product",
"same_item": 1,
"free_qty": 10,
"round_free_qty": 1,
"is_recursive": 1,
"recurse_for": 100,
"company": "_Test Company",
}
frappe.get_doc(test_record.copy()).insert()
# With pricing rule
so = make_sales_order(item_code="_Test Item", qty=100)
so.load_from_db()
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item")
self.assertEqual(so.items[1].qty, 10)
so = make_sales_order(item_code="_Test Item", qty=150)
so.load_from_db()
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item")
self.assertEqual(so.items[1].qty, 10)
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")

View File

@@ -655,7 +655,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
if transaction_qty:
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
if pricing_rule.round_free_qty:
qty = math.floor(qty)
qty = (flt(transaction_qty) // pricing_rule.recurse_for) * (pricing_rule.free_qty or 1)
if not qty:
return

View File

@@ -474,6 +474,7 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
reference_doctype="Process Statement Of Accounts",
reference_name=document_name,
attachments=attachments,
expose_recipients="header",
)
if doc.enable_auto_email and from_scheduler:

View File

@@ -45,7 +45,7 @@ class RepostAccountingLedger(Document):
latest_pcv = (
frappe.db.get_all(
"Period Closing Voucher",
filters={"company": self.company},
filters={"company": self.company, "docstatus": 1},
order_by="period_end_date desc",
pluck="period_end_date",
limit=1,

View File

@@ -741,20 +741,6 @@ frappe.ui.form.on("Sales Invoice", {
};
};
frm.set_query("company_address", function (doc) {
if (!doc.company) {
frappe.throw(__("Please set Company"));
}
return {
query: "frappe.contacts.doctype.address.address.address_query",
filters: {
link_doctype: "Company",
link_name: doc.company,
},
};
});
frm.set_query("pos_profile", function (doc) {
if (!doc.company) {
frappe.throw(__("Please set Company"));

View File

@@ -89,11 +89,14 @@
"incoming_rate",
"item_tax_rate",
"actual_batch_qty",
"actual_qty",
"section_break_eoec",
"serial_no",
"column_break_ytgd",
"batch_no",
"available_quantity_section",
"actual_qty",
"column_break_ogff",
"company_total_stock",
"edit_references",
"sales_order",
"so_detail",
@@ -675,7 +678,8 @@
"allow_on_submit": 1,
"fieldname": "actual_qty",
"fieldtype": "Float",
"label": "Available Qty at Warehouse",
"label": "Qty (Warehouse)",
"no_copy": 1,
"oldfieldname": "actual_qty",
"oldfieldtype": "Currency",
"print_hide": 1,
@@ -923,12 +927,30 @@
{
"fieldname": "column_break_ytgd",
"fieldtype": "Column Break"
},
{
"fieldname": "available_quantity_section",
"fieldtype": "Section Break",
"label": "Available Quantity"
},
{
"fieldname": "column_break_ogff",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "company_total_stock",
"fieldtype": "Float",
"label": "Qty (Company)",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-10-28 15:06:40.980995",
"modified": "2024-11-25 16:27:33.287341",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@@ -28,6 +28,7 @@ class SalesInvoiceItem(Document):
base_rate_with_margin: DF.Currency
batch_no: DF.Link | None
brand: DF.Data | None
company_total_stock: DF.Float
conversion_factor: DF.Float
cost_center: DF.Link
customer_item_code: DF.Data | None

View File

@@ -315,66 +315,48 @@ def check_if_in_list(gle, gl_map):
def toggle_debit_credit_if_negative(gl_map):
debit_credit_field_map = {
"debit": "credit",
"debit_in_account_currency": "credit_in_account_currency",
"debit_in_transaction_currency": "credit_in_transaction_currency",
}
for entry in gl_map:
# toggle debit, credit if negative entry
if flt(entry.debit) < 0 and flt(entry.credit) < 0 and flt(entry.debit) == flt(entry.credit):
entry.credit *= -1
entry.debit *= -1
for debit_field, credit_field in debit_credit_field_map.items():
debit = flt(entry.get(debit_field))
credit = flt(entry.get(credit_field))
if (
flt(entry.debit_in_account_currency) < 0
and flt(entry.credit_in_account_currency) < 0
and flt(entry.debit_in_account_currency) == flt(entry.credit_in_account_currency)
):
entry.credit_in_account_currency *= -1
entry.debit_in_account_currency *= -1
if debit < 0 and credit < 0 and debit == credit:
debit *= -1
credit *= -1
if flt(entry.debit) < 0:
entry.credit = flt(entry.credit) - flt(entry.debit)
entry.debit = 0.0
if debit < 0:
credit = credit - debit
debit = 0.0
if flt(entry.debit_in_account_currency) < 0:
entry.credit_in_account_currency = flt(entry.credit_in_account_currency) - flt(
entry.debit_in_account_currency
)
entry.debit_in_account_currency = 0.0
if credit < 0:
debit = debit - credit
credit = 0.0
if flt(entry.credit) < 0:
entry.debit = flt(entry.debit) - flt(entry.credit)
entry.credit = 0.0
# update net values
# In some scenarios net value needs to be shown in the ledger
# This method updates net values as debit or credit
if entry.post_net_value and debit and credit:
if debit > credit:
debit = debit - credit
credit = 0.0
if flt(entry.credit_in_account_currency) < 0:
entry.debit_in_account_currency = flt(entry.debit_in_account_currency) - flt(
entry.credit_in_account_currency
)
entry.credit_in_account_currency = 0.0
else:
credit = credit - debit
debit = 0.0
update_net_values(entry)
entry[debit_field] = debit
entry[credit_field] = credit
return gl_map
def update_net_values(entry):
# In some scenarios net value needs to be shown in the ledger
# This method updates net values as debit or credit
if entry.post_net_value and entry.debit and entry.credit:
if entry.debit > entry.credit:
entry.debit = entry.debit - entry.credit
entry.debit_in_account_currency = (
entry.debit_in_account_currency - entry.credit_in_account_currency
)
entry.credit = 0
entry.credit_in_account_currency = 0
else:
entry.credit = entry.credit - entry.debit
entry.credit_in_account_currency = (
entry.credit_in_account_currency - entry.debit_in_account_currency
)
entry.debit = 0
entry.debit_in_account_currency = 0
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
if not from_repost:
validate_cwip_accounts(gl_map)

View File

@@ -4,13 +4,14 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Purchase Invoice",
"dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\" frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
"function": "Sum",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"label": "Total Incoming Bills",
"modified": "2020-07-22 13:06:46.045344",
"modified": "2024-11-20 19:08:37.043777",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Total Incoming Bills",

View File

@@ -4,6 +4,7 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Payment Entry",
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\",false]]",
"function": "Sum",
"idx": 0,

View File

@@ -4,6 +4,7 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Sales Invoice",
"dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
"function": "Sum",
"idx": 0,

View File

@@ -4,6 +4,7 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Payment Entry",
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\",false]]",
"function": "Sum",
"idx": 0,

View File

@@ -1013,15 +1013,15 @@ class ReceivablePayableReport:
def get_columns(self):
self.columns = []
self.add_column("Posting Date", fieldtype="Date")
self.add_column(_("Posting Date"), fieldtype="Date")
self.add_column(
label="Party Type",
label=_("Party Type"),
fieldname="party_type",
fieldtype="Data",
width=100,
)
self.add_column(
label="Party",
label=_("Party"),
fieldname="party",
fieldtype="Dynamic Link",
options="party_type",
@@ -1037,10 +1037,10 @@ class ReceivablePayableReport:
if self.party_naming_by == "Naming Series":
if self.account_type == "Payable":
label = "Supplier Name"
label = _("Supplier Name")
fieldname = "supplier_name"
else:
label = "Customer Name"
label = _("Customer Name")
fieldname = "customer_name"
self.add_column(
label=label,
@@ -1066,7 +1066,7 @@ class ReceivablePayableReport:
width=180,
)
self.add_column(label="Due Date", fieldtype="Date")
self.add_column(label=_("Due Date"), fieldtype="Date")
if self.account_type == "Payable":
self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")

View File

@@ -440,6 +440,7 @@ class GrossProfitGenerator:
if grouped_by_invoice:
buying_amount = 0
base_amount = 0
for row in reversed(self.si_list):
if self.filters.get("group_by") == "Monthly":
@@ -480,12 +481,11 @@ class GrossProfitGenerator:
else:
row.buying_amount = flt(self.get_buying_amount(row, row.item_code), self.currency_precision)
if grouped_by_invoice:
if row.indent == 1.0:
buying_amount += row.buying_amount
elif row.indent == 0.0:
row.buying_amount = buying_amount
buying_amount = 0
if grouped_by_invoice and row.indent == 0.0:
row.buying_amount = buying_amount
row.base_amount = base_amount
buying_amount = 0
base_amount = 0
# get buying rate
if flt(row.qty):
@@ -495,11 +495,19 @@ class GrossProfitGenerator:
if self.is_not_invoice_row(row):
row.buying_rate, row.base_rate = 0.0, 0.0
if self.is_not_invoice_row(row):
self.update_return_invoices(row)
if grouped_by_invoice and row.indent == 1.0:
buying_amount += row.buying_amount
base_amount += row.base_amount
# calculate gross profit
row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision)
if row.base_amount:
row.gross_profit_percent = flt(
(row.gross_profit / row.base_amount) * 100.0, self.currency_precision
(row.gross_profit / row.base_amount) * 100.0,
self.currency_precision,
)
else:
row.gross_profit_percent = 0.0
@@ -510,33 +518,29 @@ class GrossProfitGenerator:
if self.grouped:
self.get_average_rate_based_on_group_by()
def update_return_invoices(self, row):
if row.parent in self.returned_invoices and row.item_code in self.returned_invoices[row.parent]:
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
for returned_item_row in returned_item_rows:
# returned_items 'qty' should be stateful
if returned_item_row.qty != 0:
if row.qty >= abs(returned_item_row.qty):
row.qty += returned_item_row.qty
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
returned_item_row.qty = 0
returned_item_row.base_amount = 0
else:
row.qty = 0
row.base_amount = 0
returned_item_row.qty += row.qty
returned_item_row.base_amount += row.base_amount
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
def get_average_rate_based_on_group_by(self):
for key in list(self.grouped):
if self.filters.get("group_by") == "Invoice":
for row in self.grouped[key]:
if row.indent == 1.0:
if (
row.parent in self.returned_invoices
and row.item_code in self.returned_invoices[row.parent]
):
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
for returned_item_row in returned_item_rows:
# returned_items 'qty' should be stateful
if returned_item_row.qty != 0:
if row.qty >= abs(returned_item_row.qty):
row.qty += returned_item_row.qty
returned_item_row.qty = 0
else:
row.qty = 0
returned_item_row.qty += row.qty
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
row.buying_amount = flt(
flt(row.qty) * flt(row.buying_rate), self.currency_precision
)
if flt(row.qty) or row.base_amount:
row = self.set_average_rate(row)
self.grouped_data.append(row)
elif self.filters.get("group_by") == "Payment Term":
if self.filters.get("group_by") == "Payment Term":
for i, row in enumerate(self.grouped[key]):
invoice_portion = 0
@@ -556,7 +560,7 @@ class GrossProfitGenerator:
new_row = self.set_average_rate(new_row)
self.grouped_data.append(new_row)
else:
elif self.filters.get("group_by") != "Invoice":
for i, row in enumerate(self.grouped[key]):
if i == 0:
new_row = row

View File

@@ -418,12 +418,12 @@ class TestGrossProfit(FrappeTestCase):
"item_name": self.item,
"warehouse": "Stores - _GP",
"qty": 0.0,
"avg._selling_rate": 0.0,
"avg._selling_rate": 100,
"valuation_rate": 0.0,
"selling_amount": -100.0,
"selling_amount": 0.0,
"buying_amount": 0.0,
"gross_profit": -100.0,
"gross_profit_%": 100.0,
"gross_profit": 0.0,
"gross_profit_%": 0.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
# Both items of Invoice should have '0' qty

View File

@@ -4,6 +4,7 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Asset",
"dynamic_filters_json": "[[\"Asset\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[]",
"function": "Sum",
"idx": 0,

View File

@@ -3,6 +3,7 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Asset",
"dynamic_filters_json": "[[\"Asset\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Asset\",\"creation\",\"Timespan\",\"this year\",false]]",
"function": "Count",
"idx": 0,

View File

@@ -3,6 +3,7 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Asset",
"dynamic_filters_json": "[[\"Asset\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[]",
"function": "Count",
"idx": 0,

View File

@@ -93,7 +93,7 @@ frappe.ui.form.on("Purchase Order", {
get_materials_from_supplier: function (frm) {
let po_details = [];
if (frm.doc.supplied_items && (flt(frm.doc.per_received, 2) == 100 || frm.doc.status === "Closed")) {
if (frm.doc.supplied_items && (flt(frm.doc.per_received) == 100 || frm.doc.status === "Closed")) {
frm.doc.supplied_items.forEach((d) => {
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
po_details.push(d.name);
@@ -329,8 +329,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
if (!["Closed", "Delivered"].includes(doc.status)) {
if (
this.frm.doc.status !== "Closed" &&
flt(this.frm.doc.per_received, 2) < 100 &&
flt(this.frm.doc.per_billed, 2) < 100
flt(this.frm.doc.per_received) < 100 &&
flt(this.frm.doc.per_billed) < 100
) {
if (!this.frm.doc.__onload || this.frm.doc.__onload.can_update_items) {
this.frm.add_custom_button(__("Update Items"), () => {
@@ -344,7 +344,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
}
if (this.frm.has_perm("submit")) {
if (flt(doc.per_billed, 2) < 100 || flt(doc.per_received, 2) < 100) {
if (flt(doc.per_billed) < 100 || flt(doc.per_received) < 100) {
if (doc.status != "On Hold") {
this.frm.add_custom_button(
__("Hold"),
@@ -383,7 +383,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
if (doc.status != "Closed") {
if (doc.status != "On Hold") {
if (flt(doc.per_received) < 100 && allow_receipt) {
cur_frm.add_custom_button(
this.frm.add_custom_button(
__("Purchase Receipt"),
this.make_purchase_receipt,
__("Create")
@@ -408,14 +408,15 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
}
}
// Please do not add precision in the below flt function
if (flt(doc.per_billed) < 100)
cur_frm.add_custom_button(
this.frm.add_custom_button(
__("Purchase Invoice"),
this.make_purchase_invoice,
__("Create")
);
if (flt(doc.per_billed, 2) < 100 && doc.status != "Delivered") {
if (flt(doc.per_billed) < 100 && doc.status != "Delivered") {
this.frm.add_custom_button(
__("Payment"),
() => this.make_payment_entry(),
@@ -423,7 +424,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
);
}
if (flt(doc.per_billed, 2) < 100) {
if (flt(doc.per_billed) < 100) {
this.frm.add_custom_button(
__("Payment Request"),
function () {

View File

@@ -18,6 +18,7 @@ def execute(filters=None):
columns = get_columns(filters)
data = get_data(filters)
update_received_amount(data)
if not data:
return [], [], None, []
@@ -40,7 +41,6 @@ def get_data(filters):
po = frappe.qb.DocType("Purchase Order")
po_item = frappe.qb.DocType("Purchase Order Item")
pi_item = frappe.qb.DocType("Purchase Invoice Item")
pr_item = frappe.qb.DocType("Purchase Receipt Item")
query = (
frappe.qb.from_(po)
@@ -48,8 +48,6 @@ def get_data(filters):
.on(po_item.parent == po.name)
.left_join(pi_item)
.on((pi_item.po_detail == po_item.name) & (pi_item.docstatus == 1))
.left_join(pr_item)
.on((pr_item.purchase_order_item == po_item.name) & (pr_item.docstatus == 1))
.select(
po.transaction_date.as_("date"),
po_item.schedule_date.as_("required_date"),
@@ -63,7 +61,6 @@ def get_data(filters):
(po_item.qty - po_item.received_qty).as_("pending_qty"),
Sum(IfNull(pi_item.qty, 0)).as_("billed_qty"),
po_item.base_amount.as_("amount"),
(pr_item.base_amount).as_("received_qty_amount"),
(po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"),
(po_item.base_amount - (po_item.billed_amt * IfNull(po.conversion_rate, 1))).as_(
"pending_amount"
@@ -95,6 +92,39 @@ def get_data(filters):
return data
def update_received_amount(data):
pr_data = get_received_amount_data(data)
for row in data:
row.received_qty_amount = flt(pr_data.get(row.name))
def get_received_amount_data(data):
pr = frappe.qb.DocType("Purchase Receipt")
pr_item = frappe.qb.DocType("Purchase Receipt Item")
query = (
frappe.qb.from_(pr)
.inner_join(pr_item)
.on(pr_item.parent == pr.name)
.select(
pr_item.purchase_order_item,
Sum(pr_item.base_amount).as_("received_qty_amount"),
)
.where((pr_item.parent == pr.name) & (pr.docstatus == 1))
.groupby(pr_item.purchase_order_item)
)
query = query.where(pr_item.purchase_order_item.isin([row.name for row in data]))
data = query.run()
if not data:
return frappe._dict()
return frappe._dict(data)
def prepare_data(data, filters):
completed, pending = 0, 0
pending_field = "pending_amount"

View File

@@ -1036,7 +1036,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
available_serial_nos.append(serial_no)
if available_serial_nos:
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
if parent_doc.doctype in ["Purchase Invoice", "Purchase Receipt"]:
available_serial_nos = get_available_serial_nos(available_serial_nos, warehouse)
if len(available_serial_nos) > qty:
@@ -1052,7 +1052,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
if batch_qty <= 0:
continue
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
if parent_doc.doctype in ["Purchase Invoice", "Purchase Receipt"]:
batch_qty = get_available_batch_qty(
parent_doc,
batch_no,

View File

@@ -21,9 +21,15 @@ class SellingController(StockController):
def onload(self):
super().onload()
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice", "Quotation"):
for item in self.get("items") + (self.get("packed_items") or []):
item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True))
company = self.company
item.update(
get_bin_details(
item.item_code, item.warehouse, company=company, include_child_warehouses=True
)
)
def validate(self):
super().validate()

View File

@@ -1008,11 +1008,13 @@ class StockController(AccountsController):
def validate_qi_presence(self, row):
"""Check if QI is present on row level. Warn on save and stop on submit if missing."""
if not row.quality_inspection:
msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}"
msg = _("Row #{0}: Quality Inspection is required for Item {1}").format(
row.idx, frappe.bold(row.item_code)
)
if self.docstatus == 1:
frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError)
frappe.throw(msg, title=_("Inspection Required"), exc=QualityInspectionRequiredError)
else:
frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue")
frappe.msgprint(msg, title=_("Inspection Required"), indicator="blue")
def validate_qi_submission(self, row):
"""Check if QI is submitted on row level, during submission"""
@@ -1021,11 +1023,13 @@ class StockController(AccountsController):
if not qa_docstatus == 1:
link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection)
msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}"
msg = _("Row #{0}: Quality Inspection {1} is not submitted for the item: {2}").format(
row.idx, link, row.item_code
)
if action == "Stop":
frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError)
frappe.throw(msg, title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError)
else:
frappe.msgprint(_(msg), alert=True, indicator="orange")
frappe.msgprint(msg, alert=True, indicator="orange")
def validate_qi_rejection(self, row):
"""Check if QI is rejected on row level, during submission"""
@@ -1034,11 +1038,13 @@ class StockController(AccountsController):
if qa_status == "Rejected":
link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection)
msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}"
msg = _("Row #{0}: Quality Inspection {1} was rejected for item {2}").format(
row.idx, link, row.item_code
)
if action == "Stop":
frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError)
frappe.throw(msg, title=_("Inspection Rejected"), exc=QualityInspectionRejectedError)
else:
frappe.msgprint(_(msg), alert=True, indicator="orange")
frappe.msgprint(msg, alert=True, indicator="orange")
def update_blanket_order(self):
blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order]))

View File

@@ -3,7 +3,7 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Opportunity",
"dynamic_filters_json": "[[\"Opportunity\",\"status\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"dynamic_filters_json": "[[\"Opportunity\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Opportunity\",\"company\",\"=\",null,false]]",
"function": "Count",
"idx": 0,

View File

@@ -15,6 +15,7 @@
"include_item_in_manufacturing",
"qty_section",
"required_qty",
"stock_uom",
"rate",
"amount",
"column_break_11",
@@ -138,11 +139,19 @@
"in_list_view": 1,
"label": "Returned Qty ",
"read_only": 1
},
{
"fetch_from": "item_code.stock_uom",
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2024-02-11 15:45:32.318374",
"modified": "2024-11-19 15:48:16.823384",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Item",
@@ -153,4 +162,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -25,6 +25,7 @@ class WorkOrderItem(Document):
item_code: DF.Link | None
item_name: DF.Data | None
operation: DF.Link | None
operation_row_id: DF.Int
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
@@ -32,6 +33,7 @@ class WorkOrderItem(Document):
required_qty: DF.Float
returned_qty: DF.Float
source_warehouse: DF.Link | None
stock_uom: DF.Link | None
transferred_qty: DF.Float
# end: auto-generated types

View File

@@ -3,6 +3,7 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Work Order",
"dynamic_filters_json": "[[\"Work Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Work Order\",\"status\",\"=\",\"Completed\"],[\"Work Order\",\"docstatus\",\"=\",1],[\"Work Order\",\"creation\",\"between\",[\"2020-06-08\",\"2020-07-08\"]]]",
"function": "Count",
"idx": 0,

View File

@@ -3,6 +3,7 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Work Order",
"dynamic_filters_json": "[[\"Work Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Work Order\",\"docstatus\",\"=\",1],[\"Work Order\",\"creation\",\"between\",[\"2020-06-08\",\"2020-07-08\"]]]",
"function": "Count",
"idx": 0,

View File

@@ -3,6 +3,7 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Job Card",
"dynamic_filters_json": "[[\"Job Card\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Job Card\",\"status\",\"!=\",\"Completed\"],[\"Job Card\",\"docstatus\",\"=\",1]]",
"function": "Count",
"idx": 0,

View File

@@ -382,3 +382,4 @@ erpnext.patches.v15_0.link_purchase_item_to_asset_doc
erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter
erpnext.patches.v15_0.update_task_assignee_email_field_in_asset_maintenance_log
erpnext.patches.v15_0.update_sub_voucher_type_in_gl_entries
erpnext.patches.v14_0.update_stock_uom_in_work_order_item

View File

@@ -0,0 +1,15 @@
import frappe
def execute():
frappe.db.sql(
"""
UPDATE
`tabWork Order Item`, `tabItem`
SET
`tabWork Order Item`.stock_uom = `tabItem`.stock_uom
WHERE
`tabWork Order Item`.item_code = `tabItem`.name
AND `tabWork Order Item`.docstatus = 1
"""
)

View File

@@ -1904,8 +1904,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
callback: function(r) {
if (!r.exc) {
frappe.run_serially([
() => me.frm.set_value("price_list_currency", r.message.parent.price_list_currency),
() => me.frm.set_value("plc_conversion_rate", r.message.parent.plc_conversion_rate),
() => {
if (r.message.parent.price_list_currency)
me.frm.set_value("price_list_currency", r.message.parent.price_list_currency);
},
() => {
if (r.message.parent.plc_conversion_rate)
me.frm.set_value("plc_conversion_rate", r.message.parent.plc_conversion_rate);
},
() => {
if(args.items.length) {
me._set_values_for_item_list(r.message.children);

View File

@@ -77,9 +77,13 @@ $.extend(erpnext.queries, {
},
company_address_query: function (doc) {
if (!doc.company) {
frappe.throw(__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "company", doc.name))]));
}
return {
query: "frappe.contacts.doctype.address.address.address_query",
filters: { is_your_company_address: 1, link_doctype: "Company", link_name: doc.company || "" },
filters: { link_doctype: "Company", link_name: doc.company },
};
},

View File

@@ -52,6 +52,7 @@ erpnext.sales_common = {
me.frm.set_query("customer_address", erpnext.queries.address_query);
me.frm.set_query("shipping_address_name", erpnext.queries.address_query);
me.frm.set_query("dispatch_address_name", erpnext.queries.dispatch_address_query);
me.frm.set_query("company_address", erpnext.queries.company_address_query);
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);

View File

@@ -5,6 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.utils import get_link_to_form
@@ -93,15 +94,24 @@ class ProductBundle(Document):
def get_new_item_code(doctype, txt, searchfield, start, page_len, filters):
product_bundles = frappe.db.get_list("Product Bundle", {"disabled": 0}, pluck="name")
if not searchfield or searchfield == "name":
searchfield = frappe.get_meta("Item").get("search_fields")
searchfield = searchfield.split(",")
searchfield.append("name")
item = frappe.qb.DocType("Item")
query = (
frappe.qb.from_(item)
.select(item.item_code, item.item_name)
.where((item.is_stock_item == 0) & (item.is_fixed_asset == 0) & (item[searchfield].like(f"%{txt}%")))
.select(item.name, item.item_name)
.where((item.is_stock_item == 0) & (item.is_fixed_asset == 0))
.limit(page_len)
.offset(start)
)
if searchfield:
query = query.where(Criterion.any([item[fieldname].like(f"%{txt}%") for fieldname in searchfield]))
if product_bundles:
query = query.where(item.name.notin(product_bundles))

View File

@@ -24,20 +24,6 @@ frappe.ui.form.on("Quotation", {
frm.set_df_property("packed_items", "cannot_add_rows", true);
frm.set_df_property("packed_items", "cannot_delete_rows", true);
frm.set_query("company_address", function (doc) {
if (!doc.company) {
frappe.throw(__("Please set Company"));
}
return {
query: "frappe.contacts.doctype.address.address.address_query",
filters: {
link_doctype: "Company",
link_name: doc.company,
},
};
});
frm.set_query("serial_and_batch_bundle", "packed_items", (doc, cdt, cdn) => {
let row = locals[cdt][cdn];
return {

View File

@@ -24,6 +24,10 @@
"uom",
"conversion_factor",
"stock_qty",
"available_quantity_section",
"actual_qty",
"column_break_ylrv",
"company_total_stock",
"section_break_16",
"price_list_rate",
"base_price_list_rate",
@@ -70,7 +74,6 @@
"prevdoc_docname",
"item_balance",
"projected_qty",
"actual_qty",
"col_break4",
"stock_balance",
"item_tax_rate",
@@ -460,9 +463,10 @@
"report_hide": 1
},
{
"allow_on_submit": 1,
"fieldname": "actual_qty",
"fieldtype": "Float",
"label": "Actual Qty",
"label": "Qty (Warehouse)",
"no_copy": 1,
"print_hide": 1,
"read_only": 1,
@@ -662,12 +666,31 @@
"label": "Has Alternative Item",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "available_quantity_section",
"fieldtype": "Section Break",
"label": "Available Quantity"
},
{
"fieldname": "column_break_ylrv",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "company_total_stock",
"fieldtype": "Float",
"label": "Qty (Company)",
"no_copy": 1,
"print_hide": 1,
"read_only": 1,
"report_hide": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-11-14 18:24:24.619832",
"modified": "2024-11-24 15:18:43.952844",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",
@@ -677,4 +700,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -27,6 +27,7 @@ class QuotationItem(Document):
blanket_order: DF.Link | None
blanket_order_rate: DF.Currency
brand: DF.Link | None
company_total_stock: DF.Float
conversion_factor: DF.Float
customer_item_code: DF.Data | None
description: DF.TextEditor | None

View File

@@ -26,20 +26,6 @@ frappe.ui.form.on("Sales Order", {
return doc.stock_qty <= doc.delivered_qty ? "green" : "orange";
});
frm.set_query("company_address", function (doc) {
if (!doc.company) {
frappe.throw(__("Please set Company"));
}
return {
query: "frappe.contacts.doctype.address.address.address_query",
filters: {
link_doctype: "Company",
link_name: doc.company,
},
};
});
frm.set_query("bom_no", "items", function (doc, cdt, cdn) {
var row = locals[cdt][cdn];
return {
@@ -57,8 +43,8 @@ frappe.ui.form.on("Sales Order", {
if (frm.doc.docstatus === 1) {
if (
frm.doc.status !== "Closed" &&
flt(frm.doc.per_delivered, 2) < 100 &&
flt(frm.doc.per_billed, 2) < 100 &&
flt(frm.doc.per_delivered) < 100 &&
flt(frm.doc.per_billed) < 100 &&
frm.has_perm("write")
) {
frm.add_custom_button(__("Update Items"), () => {
@@ -604,7 +590,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
__("Status")
);
if (flt(doc.per_delivered, 2) < 100 || flt(doc.per_billed, 2) < 100) {
if (flt(doc.per_delivered) < 100 || flt(doc.per_billed) < 100) {
// close
this.frm.add_custom_button(__("Close"), () => this.close_sales_order(), __("Status"));
}
@@ -627,7 +613,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
) && !this.frm.doc.skip_delivery_note;
if (this.frm.has_perm("submit")) {
if (flt(doc.per_delivered, 2) < 100 || flt(doc.per_billed, 2) < 100) {
if (flt(doc.per_delivered) < 100 || flt(doc.per_billed) < 100) {
// hold
this.frm.add_custom_button(
__("Hold"),
@@ -645,8 +631,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
if (
(!doc.__onload || !doc.__onload.has_reserved_stock) &&
flt(doc.per_picked, 2) < 100 &&
flt(doc.per_delivered, 2) < 100 &&
flt(doc.per_picked) < 100 &&
flt(doc.per_delivered) < 100 &&
frappe.model.can_create("Pick List")
) {
this.frm.add_custom_button(
@@ -664,7 +650,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
// delivery note
if (
flt(doc.per_delivered, 2) < 100 &&
flt(doc.per_delivered) < 100 &&
(order_is_a_sale || order_is_a_custom_sale) &&
allow_delivery
) {
@@ -686,7 +672,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
// sales invoice
if (flt(doc.per_billed, 2) < 100 && frappe.model.can_create("Sales Invoice")) {
if (flt(doc.per_billed) < 100 && frappe.model.can_create("Sales Invoice")) {
this.frm.add_custom_button(
__("Sales Invoice"),
() => me.make_sales_invoice(),
@@ -697,8 +683,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
// material request
if (
(!doc.order_type ||
((order_is_a_sale || order_is_a_custom_sale) &&
flt(doc.per_delivered, 2) < 100)) &&
((order_is_a_sale || order_is_a_custom_sale) && flt(doc.per_delivered) < 100)) &&
frappe.model.can_create("Material Request")
) {
this.frm.add_custom_button(
@@ -723,7 +708,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
// maintenance
if (flt(doc.per_delivered, 2) < 100 && (order_is_maintenance || order_is_a_custom_sale)) {
if (flt(doc.per_delivered) < 100 && (order_is_maintenance || order_is_a_custom_sale)) {
if (frappe.model.can_create("Maintenance Visit")) {
this.frm.add_custom_button(
__("Maintenance Visit"),
@@ -741,7 +726,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
// project
if (flt(doc.per_delivered, 2) < 100 && frappe.model.can_create("Project")) {
if (flt(doc.per_delivered) < 100 && frappe.model.can_create("Project")) {
this.frm.add_custom_button(__("Project"), () => this.make_project(), __("Create"));
}
@@ -769,10 +754,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
}
// payment request
if (
flt(doc.per_billed, precision("per_billed", doc)) <
100 + frappe.boot.sysdefaults.over_billing_allowance
) {
if (flt(doc.per_billed) < 100 + frappe.boot.sysdefaults.over_billing_allowance) {
this.frm.add_custom_button(
__("Payment Request"),
() => this.make_payment_request(),

View File

@@ -20,14 +20,14 @@ frappe.listview_settings["Sales Order"] = {
return [__("On Hold"), "orange", "status,=,On Hold"];
} else if (doc.status === "Completed") {
return [__("Completed"), "green", "status,=,Completed"];
} else if (!doc.skip_delivery_note && flt(doc.per_delivered, 2) < 100) {
} else if (!doc.skip_delivery_note && flt(doc.per_delivered) < 100) {
if (frappe.datetime.get_diff(doc.delivery_date) < 0) {
// not delivered & overdue
return [__("Overdue"), "red", "per_delivered,<,100|delivery_date,<,Today|status,!=,Closed"];
} else if (flt(doc.grand_total) === 0) {
// not delivered (zeroount order)
return [__("To Deliver"), "orange", "per_delivered,<,100|grand_total,=,0|status,!=,Closed"];
} else if (flt(doc.per_billed, 2) < 100) {
} else if (flt(doc.per_billed) < 100) {
// not delivered & not billed
return [
__("To Deliver and Bill"),
@@ -39,13 +39,13 @@ frappe.listview_settings["Sales Order"] = {
return [__("To Deliver"), "orange", "per_delivered,<,100|per_billed,=,100|status,!=,Closed"];
}
} else if (
flt(doc.per_delivered, 2) === 100 &&
flt(doc.per_delivered) === 100 &&
flt(doc.grand_total) !== 0 &&
flt(doc.per_billed, 2) < 100
flt(doc.per_billed) < 100
) {
// to bill
return [__("To Bill"), "orange", "per_delivered,=,100|per_billed,<,100|status,!=,Closed"];
} else if (doc.skip_delivery_note && flt(doc.per_billed, 2) < 100) {
} else if (doc.skip_delivery_note && flt(doc.per_billed) < 100) {
return [__("To Bill"), "orange", "per_billed,<,100|status,!=,Closed"];
}
},

View File

@@ -78,11 +78,14 @@
"against_blanket_order",
"blanket_order",
"blanket_order_rate",
"available_quantity_section",
"actual_qty",
"column_break_jpky",
"company_total_stock",
"manufacturing_section_section",
"bom_no",
"planning_section",
"projected_qty",
"actual_qty",
"ordered_qty",
"planned_qty",
"production_plan_qty",
@@ -636,7 +639,7 @@
"allow_on_submit": 1,
"fieldname": "actual_qty",
"fieldtype": "Float",
"label": "Actual Qty",
"label": "Qty (Warehouse)",
"no_copy": 1,
"print_hide": 1,
"print_width": "70px",
@@ -905,12 +908,30 @@
"label": "Is Stock Item",
"print_hide": 1,
"report_hide": 1
},
{
"allow_on_submit": 1,
"fieldname": "company_total_stock",
"fieldtype": "Float",
"label": "Qty (Company)",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_jpky",
"fieldtype": "Column Break"
},
{
"fieldname": "available_quantity_section",
"fieldtype": "Section Break",
"label": "Available Quantity"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-03-21 18:15:56.625005",
"modified": "2024-11-21 14:21:29.743474",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
@@ -921,4 +942,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -30,6 +30,7 @@ class SalesOrderItem(Document):
blanket_order_rate: DF.Currency
bom_no: DF.Link | None
brand: DF.Link | None
company_total_stock: DF.Float
conversion_factor: DF.Float
customer_item_code: DF.Data | None
delivered_by_supplier: DF.Check

View File

@@ -3,7 +3,6 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Customer",
"dynamic_filters_json": "",
"filters_json": "[[\"Customer\",\"disabled\",\"=\",\"0\"]]",
"function": "Count",
"idx": 0,

View File

@@ -96,6 +96,8 @@ def search_by_term(search_term, warehouse, price_list):
def filter_result_items(result, pos_profile):
if result and result.get("items"):
pos_item_groups = frappe.db.get_all("POS Item Group", {"parent": pos_profile}, pluck="item_group")
if not pos_item_groups:
return
result["items"] = [item for item in result.get("items") if item.get("item_group") in pos_item_groups]

View File

@@ -556,7 +556,7 @@ erpnext.PointOfSale.Controller = class {
const item_row_exists = !$.isEmptyObject(item_row);
const from_selector = field === "qty" && value === "+1";
if (from_selector) value = flt(item_row.stock_qty) + flt(value);
if (from_selector) value = flt(item_row.qty) + flt(value);
if (item_row_exists) {
if (field === "qty") value = flt(value);
@@ -687,7 +687,7 @@ erpnext.PointOfSale.Controller = class {
const is_stock_item = resp[1];
frappe.dom.unfreeze();
const bold_uom = item_row.uom.bold();
const bold_uom = item_row.stock_uom.bold();
const bold_item_code = item_row.item_code.bold();
const bold_warehouse = warehouse.bold();
const bold_available_qty = available_qty.toString().bold();

View File

@@ -73,6 +73,11 @@ frappe.query_reports["Sales Analytics"] = {
default: "Monthly",
reqd: 1,
},
{
fieldname: "show_aggregate_value_from_subsidiary_companies",
label: __("Show Aggregate Value from Subsidiary Companies"),
fieldtype: "Check",
},
],
get_datatable_options(options) {
return Object.assign(options, {

View File

@@ -4,6 +4,8 @@
import frappe
from frappe import _, scrub
from frappe.query_builder import DocType
from frappe.query_builder.functions import IfNull
from frappe.utils import add_days, add_to_date, flt, getdate
from erpnext.accounts.utils import get_fiscal_year
@@ -37,7 +39,26 @@ class Analytics:
]
self.get_period_date_ranges()
def update_company_list_for_parent_company(self):
company_list = [self.filters.get("company")]
selected_company = self.filters.get("company")
if (
selected_company
and self.filters.get("show_aggregate_value_from_subsidiary_companies")
and frappe.db.get_value("Company", selected_company, "is_group")
):
lft, rgt = frappe.db.get_value("Company", selected_company, ["lft", "rgt"])
child_companies = frappe.db.get_list(
"Company", filters={"lft": [">", lft], "rgt": ["<", rgt]}, pluck="name"
)
company_list.extend(child_companies)
self.filters["company"] = company_list
def run(self):
self.update_company_list_for_parent_company()
self.get_columns()
self.get_data()
self.get_chart_data()
@@ -123,14 +144,23 @@ class Analytics:
else:
value_field = "total_qty"
self.entries = frappe.db.sql(
""" select s.order_type as entity, s.{value_field} as value_field, s.{date_field}
from `tab{doctype}` s where s.docstatus = 1 and s.company = %s and s.{date_field} between %s and %s
and ifnull(s.order_type, '') != '' order by s.order_type
""".format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type),
(self.filters.company, self.filters.from_date, self.filters.to_date),
as_dict=1,
)
doctype = DocType(self.filters.doc_type)
self.entries = (
frappe.qb.from_(doctype)
.select(
doctype.order_type.as_("entity"),
doctype[self.date_field],
doctype[value_field].as_("value_field"),
)
.where(
(doctype.docstatus == 1)
& (doctype.company.isin(self.filters.company))
& (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date))
& (IfNull(doctype.order_type, "") != "")
)
.orderby(doctype.order_type)
).run(as_dict=True)
self.get_teams()
@@ -152,7 +182,7 @@ class Analytics:
fields=[entity, entity_name, value_field, self.date_field],
filters={
"docstatus": 1,
"company": self.filters.company,
"company": ["in", self.filters.company],
self.date_field: ("between", [self.filters.from_date, self.filters.to_date]),
},
)
@@ -167,16 +197,26 @@ class Analytics:
else:
value_field = "stock_qty"
self.entries = frappe.db.sql(
"""
select i.item_code as entity, i.item_name as entity_name, i.stock_uom, i.{value_field} as value_field, s.{date_field}
from `tab{doctype} Item` i , `tab{doctype}` s
where s.name = i.parent and i.docstatus = 1 and s.company = %s
and s.{date_field} between %s and %s
""".format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type),
(self.filters.company, self.filters.from_date, self.filters.to_date),
as_dict=1,
)
doctype = DocType(self.filters.doc_type)
doctype_item = DocType(f"{self.filters.doc_type} Item")
self.entries = (
frappe.qb.from_(doctype_item)
.join(doctype)
.on(doctype.name == doctype_item.parent)
.select(
doctype_item.item_code.as_("entity"),
doctype_item.item_name.as_("entity_name"),
doctype_item.stock_uom,
doctype_item[value_field].as_("value_field"),
doctype[self.date_field],
)
.where(
(doctype_item.docstatus == 1)
& (doctype.company.isin(self.filters.company))
& (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date))
)
).run(as_dict=True)
self.entity_names = {}
for d in self.entries:
@@ -201,7 +241,7 @@ class Analytics:
fields=[entity_field, value_field, self.date_field],
filters={
"docstatus": 1,
"company": self.filters.company,
"company": ["in", self.filters.company],
self.date_field: ("between", [self.filters.from_date, self.filters.to_date]),
},
)
@@ -213,16 +253,24 @@ class Analytics:
else:
value_field = "qty"
self.entries = frappe.db.sql(
f"""
select i.item_group as entity, i.{value_field} as value_field, s.{self.date_field}
from `tab{self.filters.doc_type} Item` i , `tab{self.filters.doc_type}` s
where s.name = i.parent and i.docstatus = 1 and s.company = %s
and s.{self.date_field} between %s and %s
""",
(self.filters.company, self.filters.from_date, self.filters.to_date),
as_dict=1,
)
doctype = DocType(self.filters.doc_type)
doctype_item = DocType(f"{self.filters.doc_type} Item")
self.entries = (
frappe.qb.from_(doctype_item)
.join(doctype)
.on(doctype.name == doctype_item.parent)
.select(
doctype_item.item_group.as_("entity"),
doctype_item[value_field].as_("value_field"),
doctype[self.date_field],
)
.where(
(doctype_item.docstatus == 1)
& (doctype.company.isin(self.filters.company))
& (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date))
)
).run(as_dict=True)
self.get_groups()
@@ -239,7 +287,7 @@ class Analytics:
fields=[entity, value_field, self.date_field],
filters={
"docstatus": 1,
"company": self.filters.company,
"company": ["in", self.filters.company],
"project": ["!=", ""],
self.date_field: ("between", [self.filters.from_date, self.filters.to_date]),
},
@@ -312,7 +360,7 @@ class Analytics:
str(((posting_date.month - 1) // 3) + 1), str(posting_date.year)
)
else:
year = get_fiscal_year(posting_date, company=self.filters.company)
year = get_fiscal_year(posting_date, company=self.filters.company[0])
period = str(year[0])
return period

View File

@@ -87,16 +87,19 @@
"column_break_rxvc",
"batch_no",
"available_qty_section",
"actual_batch_qty",
"actual_qty",
"installed_qty",
"item_tax_rate",
"actual_batch_qty",
"column_break_atna",
"company_total_stock",
"section_break_kejd",
"installed_qty",
"packed_qty",
"column_break_fguf",
"received_qty",
"accounting_details_section",
"expense_account",
"column_break_71",
"item_tax_rate",
"internal_transfer_section",
"material_request",
"purchase_order",
@@ -519,7 +522,7 @@
"allow_on_submit": 1,
"fieldname": "actual_qty",
"fieldtype": "Float",
"label": "Available Qty at From Warehouse",
"label": "Qty (Warehouse)",
"no_copy": 1,
"oldfieldname": "actual_qty",
"oldfieldtype": "Currency",
@@ -907,13 +910,30 @@
{
"fieldname": "column_break_rxvc",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "company_total_stock",
"fieldtype": "Float",
"label": "Qty (Company)",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "section_break_kejd",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_fguf",
"fieldtype": "Column Break"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-21 18:15:07.603672",
"modified": "2024-11-21 17:37:37.441498",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
@@ -923,4 +943,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -30,6 +30,7 @@ class DeliveryNoteItem(Document):
batch_no: DF.Link | None
billed_amt: DF.Currency
brand: DF.Link | None
company_total_stock: DF.Float
conversion_factor: DF.Float
cost_center: DF.Link | None
customer_item_code: DF.Data | None

View File

@@ -663,39 +663,41 @@ $.extend(erpnext.item, {
}
frm.doc.attributes.forEach(function (d) {
let p = new Promise((resolve) => {
if (!d.numeric_values) {
frappe
.call({
method: "frappe.client.get_list",
args: {
doctype: "Item Attribute Value",
filters: [["parent", "=", d.attribute]],
fields: ["attribute_value"],
limit_page_length: 0,
parent: "Item Attribute",
order_by: "idx",
},
})
.then((r) => {
if (r.message) {
attr_val_fields[d.attribute] = r.message.map(function (d) {
return d.attribute_value;
});
resolve();
}
});
} else {
let values = [];
for (var i = d.from_range; i <= d.to_range; i = flt(i + d.increment, 6)) {
values.push(i);
if (!d.disabled) {
let p = new Promise((resolve) => {
if (!d.numeric_values) {
frappe
.call({
method: "frappe.client.get_list",
args: {
doctype: "Item Attribute Value",
filters: [["parent", "=", d.attribute]],
fields: ["attribute_value"],
limit_page_length: 0,
parent: "Item Attribute",
order_by: "idx",
},
})
.then((r) => {
if (r.message) {
attr_val_fields[d.attribute] = r.message.map(function (d) {
return d.attribute_value;
});
resolve();
}
});
} else {
let values = [];
for (var i = d.from_range; i <= d.to_range; i = flt(i + d.increment, 6)) {
values.push(i);
}
attr_val_fields[d.attribute] = values;
resolve();
}
attr_val_fields[d.attribute] = values;
resolve();
}
});
});
promises.push(p);
promises.push(p);
}
}, this);
Promise.all(promises).then(() => {
@@ -710,26 +712,29 @@ $.extend(erpnext.item, {
for (var i = 0; i < frm.doc.attributes.length; i++) {
var fieldtype, desc;
var row = frm.doc.attributes[i];
if (row.numeric_values) {
fieldtype = "Float";
desc =
"Min Value: " +
row.from_range +
" , Max Value: " +
row.to_range +
", in Increments of: " +
row.increment;
} else {
fieldtype = "Data";
desc = "";
if (!row.disabled) {
if (row.numeric_values) {
fieldtype = "Float";
desc =
"Min Value: " +
row.from_range +
" , Max Value: " +
row.to_range +
", in Increments of: " +
row.increment;
} else {
fieldtype = "Data";
desc = "";
}
fields = fields.concat({
label: row.attribute,
fieldname: row.attribute,
fieldtype: fieldtype,
reqd: 0,
description: desc,
});
}
fields = fields.concat({
label: row.attribute,
fieldname: row.attribute,
fieldtype: fieldtype,
reqd: 0,
description: desc,
});
}
if (frm.doc.image) {

View File

@@ -10,6 +10,8 @@
"field_order": [
"attribute_name",
"numeric_values",
"column_break_vbik",
"disabled",
"section_break_4",
"from_range",
"increment",
@@ -70,15 +72,26 @@
"fieldtype": "Table",
"label": "Item Attribute Values",
"options": "Item Attribute Value"
},
{
"fieldname": "column_break_vbik",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
}
],
"icon": "fa fa-edit",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-10-02 12:03:02.359202",
"modified": "2024-11-26 20:05:29.421714",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Attribute",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -94,4 +107,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -30,6 +30,7 @@ class ItemAttribute(Document):
from erpnext.stock.doctype.item_attribute_value.item_attribute_value import ItemAttributeValue
attribute_name: DF.Data
disabled: DF.Check
from_range: DF.Float
increment: DF.Float
item_attribute_values: DF.Table[ItemAttributeValue]
@@ -47,6 +48,19 @@ class ItemAttribute(Document):
def on_update(self):
self.validate_exising_items()
self.set_enabled_disabled_in_items()
def set_enabled_disabled_in_items(self):
db_value = self.get_doc_before_save()
if not db_value or db_value.disabled != self.disabled:
item_variant_table = frappe.qb.DocType("Item Variant Attribute")
query = (
frappe.qb.update(item_variant_table)
.set(item_variant_table.disabled, self.disabled)
.where(item_variant_table.attribute == self.name)
)
query.run()
def validate_exising_items(self):
"""Validate that if there are existing items with attributes, they are valid"""

View File

@@ -11,6 +11,7 @@
"column_break_2",
"attribute_value",
"numeric_values",
"disabled",
"section_break_4",
"from_range",
"increment",
@@ -74,11 +75,18 @@
"fieldname": "to_range",
"fieldtype": "Float",
"label": "To Range"
},
{
"default": "0",
"fetch_from": "attribute.disabled",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
}
],
"istable": 1,
"links": [],
"modified": "2023-07-14 17:15:19.112119",
"modified": "2024-11-26 20:10:49.873339",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Variant Attribute",
@@ -87,4 +95,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -16,6 +16,7 @@ class ItemVariantAttribute(Document):
attribute: DF.Link
attribute_value: DF.Data | None
disabled: DF.Check
from_range: DF.Float
increment: DF.Float
numeric_values: DF.Check

View File

@@ -6,7 +6,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, cstr, flt, get_number_format_info
from frappe.utils import cint, cstr, flt, get_link_to_form, get_number_format_info
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template import (
get_template_details,
@@ -73,6 +73,27 @@ class QualityInspection(Document):
if self.readings:
self.inspect_and_set_status()
self.validate_inspection_required()
def validate_inspection_required(self):
if self.reference_type in ["Purchase Receipt", "Purchase Invoice"] and not frappe.get_cached_value(
"Item", self.item_code, "inspection_required_before_purchase"
):
frappe.throw(
_(
"'Inspection Required before Purchase' has disabled for the item {0}, no need to create the QI"
).format(get_link_to_form("Item", self.item_code))
)
if self.reference_type in ["Delivery Note", "Sales Invoice"] and not frappe.get_cached_value(
"Item", self.item_code, "inspection_required_before_delivery"
):
frappe.throw(
_(
"'Inspection Required before Delivery' has disabled for the item {0}, no need to create the QI"
).format(get_link_to_form("Item", self.item_code))
)
def before_submit(self):
self.validate_readings_status_mandatory()

View File

@@ -940,11 +940,12 @@ def get_batch_based_item_price(params, item_code) -> float:
params = parse_json(params)
item_price = get_item_price(params, item_code, force_batch_no=True)
if not item_price:
item_price = get_item_price(params, item_code, ignore_party=True, force_batch_no=True)
if item_price and item_price[0].uom == params.get("uom"):
return item_price[0].price_list_rate
if item_price and item_price[0][2] == params.get("uom"):
return item_price[0][1]
return 0.0

View File

@@ -3,6 +3,7 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Warehouse",
"dynamic_filters_json": "[[\"Warehouse\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Warehouse\",\"disabled\",\"=\",0]]",
"function": "Count",
"idx": 0,