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

chore: release v15
This commit is contained in:
ruthra kumar
2025-01-01 14:05:41 +05:30
committed by GitHub
54 changed files with 691 additions and 179 deletions

View File

@@ -128,7 +128,7 @@
"description": "Rate at which this tax is applied",
"fieldname": "tax_rate",
"fieldtype": "Float",
"label": "Rate",
"label": "Tax Rate",
"oldfieldname": "tax_rate",
"oldfieldtype": "Currency"
},

View File

@@ -101,7 +101,7 @@
"fieldname": "rate",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Rate",
"label": "Tax Rate",
"oldfieldname": "rate",
"oldfieldtype": "Currency"
},

View File

@@ -12,6 +12,7 @@ from frappe.utils import cint, flt
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
from erpnext.accounts.party import get_party_account
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
get_amounts_not_reflected_in_system,
get_entries,
@@ -304,54 +305,56 @@ def create_payment_entry_bts(
bank_transaction = frappe.db.get_values(
"Bank Transaction",
bank_transaction_name,
fieldname=["name", "unallocated_amount", "deposit", "bank_account"],
fieldname=["name", "unallocated_amount", "deposit", "bank_account", "currency"],
as_dict=True,
)[0]
paid_amount = bank_transaction.unallocated_amount
payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
company = frappe.get_value("Account", company_account, "company")
payment_entry_dict = {
"company": company,
"payment_type": payment_type,
"reference_no": reference_number,
"reference_date": reference_date,
"party_type": party_type,
"party": party,
"posting_date": posting_date,
"paid_amount": paid_amount,
"received_amount": paid_amount,
}
payment_entry = frappe.new_doc("Payment Entry")
bank_account = frappe.get_cached_value("Bank Account", bank_transaction.bank_account, "account")
company = frappe.get_cached_value("Account", bank_account, "company")
party_account = get_party_account(party_type, party, company)
payment_entry.update(payment_entry_dict)
bank_currency = bank_transaction.currency
party_currency = frappe.get_cached_value("Account", party_account, "account_currency")
if mode_of_payment:
payment_entry.mode_of_payment = mode_of_payment
if project:
payment_entry.project = project
if cost_center:
payment_entry.cost_center = cost_center
if payment_type == "Receive":
payment_entry.paid_to = company_account
else:
payment_entry.paid_from = company_account
exc_rate = get_exchange_rate(bank_currency, party_currency, posting_date)
payment_entry.validate()
amt_in_bank_acc_currency = bank_transaction.unallocated_amount
amount_in_party_currency = bank_transaction.unallocated_amount * exc_rate
pe = frappe.new_doc("Payment Entry")
pe.payment_type = payment_type
pe.company = company
pe.reference_no = reference_number
pe.reference_date = reference_date
pe.party_type = party_type
pe.party = party
pe.posting_date = posting_date
pe.paid_from = party_account if payment_type == "Receive" else bank_account
pe.paid_to = party_account if payment_type == "Pay" else bank_account
pe.paid_from_account_currency = party_currency if payment_type == "Receive" else bank_currency
pe.paid_to_account_currency = party_currency if payment_type == "Pay" else bank_currency
pe.paid_amount = amount_in_party_currency if payment_type == "Receive" else amt_in_bank_acc_currency
pe.received_amount = amount_in_party_currency if payment_type == "Pay" else amt_in_bank_acc_currency
pe.mode_of_payment = mode_of_payment
pe.project = project
pe.cost_center = cost_center
pe.validate()
if allow_edit:
return payment_entry
return pe
payment_entry.insert()
pe.insert()
pe.submit()
payment_entry.submit()
vouchers = json.dumps(
[
{
"payment_doctype": "Payment Entry",
"payment_name": payment_entry.name,
"amount": paid_amount,
"payment_name": pe.name,
"amount": amt_in_bank_acc_currency,
}
]
)
@@ -480,8 +483,12 @@ def get_linked_payments(
def subtract_allocations(gl_account, vouchers):
"Look up & subtract any existing Bank Transaction allocations"
copied = []
voucher_docs = [(voucher.get("doctype"), voucher.get("name")) for voucher in vouchers]
voucher_allocated_amounts = get_total_allocated_amount(voucher_docs)
for voucher in vouchers:
rows = get_total_allocated_amount(voucher.get("doctype"), voucher.get("name"))
rows = voucher_allocated_amounts.get((voucher.get("doctype"), voucher.get("name"))) or []
filtered_row = list(filter(lambda row: row.get("gl_account") == gl_account, rows))
if amount := None if not filtered_row else filtered_row[0]["total"]:
@@ -719,7 +726,7 @@ def get_pe_matching_query(
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
ConstantColumn("Payment Entry").as_("doctype"),
pe.name,
pe.paid_amount_after_tax.as_("paid_amount"),
pe.base_paid_amount_after_tax.as_("paid_amount"),
pe.reference_no,
pe.reference_date,
pe.party,

View File

@@ -154,10 +154,16 @@ class BankTransaction(Document):
"""
remaining_amount = self.unallocated_amount
to_remove = []
payment_entry_docs = [(pe.payment_document, pe.payment_entry) for pe in self.payment_entries]
pe_bt_allocations = get_total_allocated_amount(payment_entry_docs)
for payment_entry in self.payment_entries:
if payment_entry.allocated_amount == 0.0:
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
self, payment_entry
self,
payment_entry,
pe_bt_allocations.get((payment_entry.payment_document, payment_entry.payment_entry))
or [],
)
if 0.0 == unallocated_amount:
@@ -232,7 +238,7 @@ def get_doctypes_for_bank_reconciliation():
return frappe.get_hooks("bank_reconciliation_doctypes")
def get_clearance_details(transaction, payment_entry):
def get_clearance_details(transaction, payment_entry, bt_allocations):
"""
There should only be one bank gle for a voucher.
Could be none for a Bank Transaction.
@@ -241,7 +247,6 @@ def get_clearance_details(transaction, payment_entry):
"""
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
bt_allocations = get_total_allocated_amount(payment_entry.payment_document, payment_entry.payment_entry)
unallocated_amount = min(
transaction.unallocated_amount,
@@ -294,44 +299,52 @@ def get_related_bank_gl_entries(doctype, docname):
)
def get_total_allocated_amount(doctype, docname):
def get_total_allocated_amount(docs):
"""
Gets the sum of allocations for a voucher on each bank GL account
along with the latest bank transaction name & date
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
"""
if not docs:
return {}
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
result = frappe.db.sql(
"""
SELECT total, latest_name, latest_date, gl_account FROM (
SELECT total, latest_name, latest_date, gl_account, payment_document, payment_entry FROM (
SELECT
ROW_NUMBER() OVER w AS rownum,
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total,
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account, btp.payment_document, btp.payment_entry) AS total,
FIRST_VALUE(bt.name) OVER w AS latest_name,
FIRST_VALUE(bt.date) OVER w AS latest_date,
ba.account AS gl_account
ba.account AS gl_account,
btp.payment_document,
btp.payment_entry
FROM
`tabBank Transaction Payments` btp
LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
WHERE
btp.payment_document = %(doctype)s
AND btp.payment_entry = %(docname)s
(btp.payment_document, btp.payment_entry) IN %(docs)s
AND bt.docstatus = 1
WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc)
WINDOW w AS (PARTITION BY ba.account, btp.payment_document, btp.payment_entry ORDER BY bt.date DESC)
) temp
WHERE
rownum = 1
""",
dict(doctype=doctype, docname=docname),
dict(docs=docs),
as_dict=True,
)
payment_allocation_details = {}
for row in result:
# Why is this *sometimes* a byte string?
if isinstance(row["latest_name"], bytes):
row["latest_name"] = row["latest_name"].decode()
row["latest_date"] = frappe.utils.getdate(row["latest_date"])
return result
payment_allocation_details.setdefault((row["payment_document"], row["payment_entry"]), []).append(row)
return payment_allocation_details
def get_paid_amount(payment_entry, currency, gl_bank_account):

View File

@@ -13,7 +13,11 @@ import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency
from erpnext.accounts.party import (
validate_account_party_type,
validate_party_frozen_disabled,
validate_party_gle_currency,
)
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency
@@ -268,6 +272,7 @@ class GLEntry(Document):
def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party)
validate_account_party_type(self)
def validate_currency(self):
company_currency = erpnext.get_company_currency(self.company)

View File

@@ -79,3 +79,48 @@ class TestGLEntry(unittest.TestCase):
"SELECT current from tabSeries where name = %s", naming_series
)[0][0]
self.assertEqual(old_naming_series_current_value + 2, new_naming_series_current_value)
def test_validate_account_party_type(self):
jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
100,
"_Test Cost Center - _TC",
save=False,
submit=False,
)
for row in jv.accounts:
row.party_type = "Supplier"
break
jv.save()
try:
jv.submit()
except Exception as e:
self.assertEqual(
str(e),
"Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC",
)
jv1 = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
100,
"_Test Cost Center - _TC",
save=False,
submit=False,
)
for row in jv.accounts:
row.party_type = "Customer"
break
jv1.save()
try:
jv1.submit()
except Exception as e:
self.assertEqual(
str(e),
"Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC",
)

View File

@@ -27,6 +27,18 @@ frappe.ui.form.on("Payment Entry", {
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
// project excluded in setup_dimension_filters
frm.set_query("project", function (doc) {
let filters = {
company: doc.company,
};
if (doc.party_type == "Customer") filters.customer = doc.party;
return {
query: "erpnext.controllers.queries.get_project_name",
filters,
};
});
if (frm.is_new()) {
set_default_party_type(frm);
}

View File

@@ -1479,7 +1479,7 @@ class TestPaymentEntry(FrappeTestCase):
parent_account="Current Liabilities - _TC",
account_name="Advances Paid",
company=company,
account_type="Liability",
account_type="Payable",
)
frappe.db.set_value(

View File

@@ -12,15 +12,15 @@
</thead>
<tbody>
<tr>
<td class="text-left font-bold">{{ _('Grand Total') }}</td>
<td class="text-left font-bold">{{ _("Grand Total") }}</td>
<td class='text-right'> {{ frappe.utils.fmt_money(data.grand_total or '', currency=currency) }}</td>
</tr>
<tr>
<td class="text-left font-bold">{{ _('Net Total') }}</td>
<td class="text-left font-bold">{{ _("Net Total") }}</td>
<td class='text-right'> {{ frappe.utils.fmt_money(data.net_total or '', currency=currency) }}</td>
</tr>
<tr>
<td class="text-left font-bold">{{ _('Total Quantity') }}</td>
<td class="text-left font-bold">{{ _("Total Quantity") }}</td>
<td class='text-right'>{{ data.total_quantity or '' }}</td>
</tr>
@@ -44,7 +44,7 @@
<tbody>
{% for d in data.payment_reconciliation %}
<tr>
<td class="text-left">{{ d.mode_of_payment }}</td>
<td class="text-left">{{ _(d.mode_of_payment) }}</td>
<td class='text-right'> {{ frappe.utils.fmt_money(d.expected_amount - d.opening_amount, currency=currency) }}</td>
</tr>
{% endfor %}
@@ -63,7 +63,7 @@
<thead>
<tr>
<th class="text-left">{{ _("Account") }}</th>
<th class="text-left">{{ _("Rate") }}</th>
<th class="text-left">{{ _("Tax Rate") }}</th>
<th class="text-right">{{ _("Amount") }}</th>
</tr>
</thead>

View File

@@ -14,7 +14,7 @@
"fieldname": "rate",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "Rate",
"label": "Tax Rate",
"read_only": 1
},
{

View File

@@ -399,6 +399,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
hide_fields(this.frm.doc);
if (cint(this.frm.doc.is_paid)) {
this.frm.set_value("allocate_advances_automatically", 0);
this.frm.set_value("payment_terms_template", "");
this.frm.set_value("payment_schedule", []);
if (!this.frm.doc.company) {
this.frm.set_value("is_paid", 0);
frappe.msgprint(__("Please specify Company to proceed"));

View File

@@ -507,7 +507,7 @@ class SalesInvoice(SellingController):
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
def validate_pos_paid_amount(self):
if len(self.payments) == 0 and self.is_pos:
if len(self.payments) == 0 and self.is_pos and flt(self.grand_total) > 0:
frappe.throw(_("At least one mode of payment is required for POS invoice."))
def check_if_consolidated_invoice(self):

View File

@@ -43,6 +43,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import (
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
from erpnext.stock.get_item_details import get_item_tax_map
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
@@ -2873,13 +2874,26 @@ class TestSalesInvoice(FrappeTestCase):
item.save()
sales_invoice = create_sales_invoice(item="T Shirt", rate=700, do_not_submit=True)
item_tax_map = get_item_tax_map(
company=sales_invoice.company,
item_tax_template=sales_invoice.items[0].item_tax_template,
)
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
# Apply discount
sales_invoice.apply_discount_on = "Net Total"
sales_invoice.discount_amount = 300
sales_invoice.save()
item_tax_map = get_item_tax_map(
company=sales_invoice.company,
item_tax_template=sales_invoice.items[0].item_tax_template,
)
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_sales_invoice_with_discount_accounting_enabled(self):

View File

@@ -759,6 +759,17 @@ def validate_party_frozen_disabled(party_type, party_name):
frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True)
def validate_account_party_type(self):
if self.party_type and self.party:
account_type = frappe.get_cached_value("Account", self.account, "account_type")
if account_type and (account_type not in ["Receivable", "Payable"]):
frappe.throw(
_(
"Party Type and Party can only be set for Receivable / Payable account<br><br>" "{0}"
).format(self.account)
)
def get_dashboard_info(party_type, party, loyalty_program=None):
current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True)

View File

@@ -318,7 +318,7 @@ def get_columns(additional_table_columns, filters):
"width": 100,
},
{
"label": _("Rate"),
"label": _("Tax Rate"),
"fieldname": "rate",
"fieldtype": "Float",
"options": "currency",

View File

@@ -130,7 +130,9 @@ def get_fiscal_years(
else:
return ((fy.name, fy.year_start_date, fy.year_end_date),)
error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date))
error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(
_(label), formatdate(transaction_date)
)
if company:
error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))

View File

@@ -19,6 +19,7 @@ from frappe.utils import (
)
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.accounts.general_ledger import make_reverse_gl_entries
from erpnext.assets.doctype.asset.depreciation import (
get_comma_separated_links,
@@ -887,6 +888,7 @@ def get_asset_naming_series():
@frappe.whitelist()
def make_sales_invoice(asset, item_code, company, serial_no=None):
asset_doc = frappe.get_doc("Asset", asset)
si = frappe.new_doc("Sales Invoice")
si.company = company
si.currency = frappe.get_cached_value("Company", company, "default_currency")
@@ -903,6 +905,16 @@ def make_sales_invoice(asset, item_code, company, serial_no=None):
"qty": 1,
},
)
accounting_dimensions = get_dimensions(with_cost_center_and_project=True)
for dimension in accounting_dimensions[0]:
si.update(
{
dimension["fieldname"]: asset_doc.get(dimension["fieldname"])
or dimension.get("default_dimension")
}
)
si.set_missing_values()
return si

View File

@@ -18,11 +18,12 @@ def execute(filters=None):
columns = get_columns(filters)
data = get_data(filters)
update_received_amount(data)
if not data:
return [], [], None, []
update_received_amount(data)
data, chart_data = prepare_data(data, filters)
return columns, data, None, chart_data
@@ -103,6 +104,11 @@ def get_received_amount_data(data):
pr = frappe.qb.DocType("Purchase Receipt")
pr_item = frappe.qb.DocType("Purchase Receipt Item")
po_items = [row.name for row in data]
if not po_items:
return frappe._dict()
query = (
frappe.qb.from_(pr)
.inner_join(pr_item)
@@ -111,12 +117,10 @@ def get_received_amount_data(data):
pr_item.purchase_order_item,
Sum(pr_item.base_amount).as_("received_qty_amount"),
)
.where((pr_item.parent == pr.name) & (pr.docstatus == 1))
.where((pr.docstatus == 1) & (pr_item.purchase_order_item.isin(po_items)))
.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:

View File

@@ -464,9 +464,16 @@ class AccountsController(TransactionBase):
)
def validate_invoice_documents_schedule(self):
if self.is_return:
if (
self.is_return
or (self.doctype == "Purchase Invoice" and self.is_paid)
or (self.doctype == "Sales Invoice" and self.is_pos)
or self.get("is_opening") == "Yes"
):
self.payment_terms_template = ""
self.payment_schedule = []
if self.is_return:
return
self.validate_payment_schedule_dates()
@@ -1219,7 +1226,7 @@ class AccountsController(TransactionBase):
party_account = []
default_advance_account = None
if self.doctype == "Sales Invoice":
if self.doctype in ["Sales Invoice", "POS Invoice"]:
party_type = "Customer"
party = self.customer
amount_field = "credit_in_account_currency"

View File

@@ -9,6 +9,7 @@ from frappe.utils import cint, flt, getdate
from frappe.utils.data import nowtime
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.party import get_party_details
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
@@ -744,6 +745,7 @@ class BuyingController(SubcontractingController):
def auto_make_assets(self, asset_items):
items_data = get_asset_item_details(asset_items)
messages = []
accounting_dimensions = get_dimensions(with_cost_center_and_project=True)
for d in self.items:
if d.is_fixed_asset:
@@ -755,11 +757,11 @@ class BuyingController(SubcontractingController):
if item_data.get("asset_naming_series"):
created_assets = []
if item_data.get("is_grouped_asset"):
asset = self.make_asset(d, is_grouped_asset=True)
asset = self.make_asset(d, accounting_dimensions, is_grouped_asset=True)
created_assets.append(asset)
else:
for _qty in range(cint(d.qty)):
asset = self.make_asset(d)
asset = self.make_asset(d, accounting_dimensions)
created_assets.append(asset)
if len(created_assets) > 5:
@@ -797,7 +799,7 @@ class BuyingController(SubcontractingController):
for message in messages:
frappe.msgprint(message, title="Success", indicator="green")
def make_asset(self, row, is_grouped_asset=False):
def make_asset(self, row, accounting_dimensions, is_grouped_asset=False):
if not row.asset_location:
frappe.throw(_("Row {0}: Enter location for the asset item {1}").format(row.idx, row.item_code))
@@ -828,6 +830,13 @@ class BuyingController(SubcontractingController):
"purchase_invoice_item": row.name if self.doctype == "Purchase Invoice" else None,
}
)
for dimension in accounting_dimensions[0]:
asset.update(
{
dimension["fieldname"]: self.get(dimension["fieldname"])
or dimension.get("default_dimension")
}
)
asset.flags.ignore_validate = True
asset.flags.ignore_mandatory = True

View File

@@ -271,10 +271,14 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
qb_filter_or_conditions = []
ifelse = CustomFunction("IF", ["condition", "then", "else"])
if filters and filters.get("customer"):
qb_filter_and_conditions.append(
(proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == ""
)
if filters:
if filters.get("customer"):
qb_filter_and_conditions.append(
(proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == ""
)
if filters.get("company"):
qb_filter_and_conditions.append(proj.company == filters.get("company"))
qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled"]))

View File

@@ -126,9 +126,13 @@ status_map = {
"Partially Received",
"eval:self.status != 'Stopped' and self.per_received > 0 and self.per_received < 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'",
],
[
"Partially Received",
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type == 'Material Transfer'",
],
[
"Partially Ordered",
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1",
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type != 'Material Transfer'",
],
[
"Manufactured",

View File

@@ -18,7 +18,7 @@ from erpnext.controllers.accounts_controller import (
validate_inclusive_tax,
validate_taxes_and_charges,
)
from erpnext.stock.get_item_details import _get_item_tax_template
from erpnext.stock.get_item_details import _get_item_tax_template, get_item_tax_map
from erpnext.utilities.regional import temporary_flag
@@ -70,6 +70,7 @@ class calculate_taxes_and_totals:
self.validate_conversion_rate()
self.calculate_item_values()
self.validate_item_tax_template()
self.update_item_tax_map()
self.initialize_taxes()
self.determine_exclusive_rate()
self.calculate_net_total()
@@ -134,6 +135,14 @@ class calculate_taxes_and_totals:
)
)
def update_item_tax_map(self):
for item in self.doc.items:
item.item_tax_rate = get_item_tax_map(
company=self.doc.get("company"),
item_tax_template=item.item_tax_template,
as_json=True,
)
def validate_conversion_rate(self):
# validate conversion rate
company_currency = erpnext.get_company_currency(self.doc.company)

View File

@@ -13,6 +13,8 @@
"supplier",
"supplier_name",
"column_break_8",
"order_no",
"order_date",
"from_date",
"to_date",
"company",
@@ -129,15 +131,27 @@
"fieldname": "terms",
"fieldtype": "Text Editor",
"label": "Terms and Conditions Details"
},
{
"fieldname": "order_no",
"fieldtype": "Data",
"label": "Order No"
},
{
"depends_on": "eval:doc.order_no",
"fieldname": "order_date",
"fieldtype": "Date",
"label": "Order Date"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-06-29 00:30:30.621636",
"modified": "2024-12-05 15:44:21.520093",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Blanket Order",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{

View File

@@ -31,6 +31,8 @@ class BlanketOrder(Document):
from_date: DF.Date
items: DF.Table[BlanketOrderItem]
naming_series: DF.Literal["MFG-BLR-.YYYY.-"]
order_date: DF.Date | None
order_no: DF.Data | None
supplier: DF.Link | None
supplier_name: DF.Data | None
tc_name: DF.Link | None

View File

@@ -2,13 +2,13 @@ frappe.listview_settings["BOM"] = {
add_fields: ["is_active", "is_default", "total_cost", "has_variants"],
get_indicator: function (doc) {
if (doc.is_active && doc.has_variants) {
return [__("Template"), "orange", "has_variants,=,Yes"];
return [__("Template"), "orange", "has_variants,=,1"];
} else if (doc.is_default) {
return [__("Default"), "green", "is_default,=,Yes"];
return [__("Default"), "green", "is_default,=,1"];
} else if (doc.is_active) {
return [__("Active"), "blue", "is_active,=,Yes"];
return [__("Active"), "blue", "is_active,=,1"];
} else if (!doc.is_active) {
return [__("Not active"), "gray", "is_active,=,No"];
return [__("Not active"), "gray", "is_active,=,0"];
}
},
};

View File

@@ -230,8 +230,8 @@ class ForecastingReport(ExponentialSmoothingForecast):
"data": {
"labels": labels,
"datasets": [
{"name": "Demand", "values": self.total_demand},
{"name": "Forecast", "values": self.total_forecast},
{"name": _("Demand"), "values": self.total_demand},
{"name": _("Forecast"), "values": self.total_forecast},
],
},
"type": "line",

View File

@@ -106,7 +106,7 @@ def get_data(filters, columns):
for label in labels:
work = {}
work["Status"] = label
work["Status"] = _(label)
for _dummy, end_date in ranges:
period = get_period(end_date, filters)
if periodic_data.get(label).get(period):

View File

@@ -4,61 +4,67 @@ import frappe
from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
DEFAULT_FILTERS = {
"company": "_Test Company",
"from_date": "2010-01-01",
"to_date": "2030-01-01",
"warehouse": "_Test Warehouse - _TC",
}
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
("BOM Explorer", {"bom": frappe.get_last_doc("BOM").name}),
("BOM Operations Time", {}),
("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}),
("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}),
("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}),
("Downtime Analysis", {}),
(
"Exponential Smoothing Forecasting",
{
"based_on_document": "Sales Order",
"based_on_field": "Qty",
"no_of_years": 3,
"periodicity": "Yearly",
"smoothing_constant": 0.3,
},
),
("Job Card Summary", {"fiscal_year": "2021-2022"}),
("Production Analytics", {"range": "Monthly"}),
("Quality Inspection Summary", {}),
("Process Loss Report", {}),
("Work Order Stock Report", {}),
("Work Order Summary", {"fiscal_year": "2021-2022", "age": 0}),
]
if frappe.db.a_row_exists("Production Plan"):
REPORT_FILTER_TEST_CASES.append(
("Production Plan Summary", {"production_plan": frappe.get_last_doc("Production Plan").name})
)
OPTIONAL_FILTERS = {
"warehouse": "_Test Warehouse - _TC",
"item": "_Test Item",
"item_group": "_Test Item Group",
}
class TestManufacturingReports(unittest.TestCase):
def setUp(self):
self.setup_default_filters()
def tearDown(self):
frappe.db.rollback()
def setup_default_filters(self):
self.last_bom = frappe.get_last_doc("BOM").name
self.DEFAULT_FILTERS = {
"company": "_Test Company",
"from_date": "2010-01-01",
"to_date": "2030-01-01",
"warehouse": "_Test Warehouse - _TC",
}
self.REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
("BOM Explorer", {"bom": self.last_bom}),
("BOM Operations Time", {}),
("BOM Stock Calculated", {"bom": self.last_bom, "qty_to_make": 2}),
("BOM Stock Report", {"bom": self.last_bom, "qty_to_produce": 2}),
("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}),
("Downtime Analysis", {}),
(
"Exponential Smoothing Forecasting",
{
"based_on_document": "Sales Order",
"based_on_field": "Qty",
"no_of_years": 3,
"periodicity": "Yearly",
"smoothing_constant": 0.3,
},
),
("Job Card Summary", {"fiscal_year": "2021-2022"}),
("Production Analytics", {"range": "Monthly"}),
("Quality Inspection Summary", {}),
("Process Loss Report", {}),
("Work Order Stock Report", {}),
("Work Order Summary", {"fiscal_year": "2021-2022", "age": 0}),
]
if frappe.db.a_row_exists("Production Plan"):
self.REPORT_FILTER_TEST_CASES.append(
("Production Plan Summary", {"production_plan": frappe.get_last_doc("Production Plan").name})
)
self.OPTIONAL_FILTERS = {
"warehouse": "_Test Warehouse - _TC",
"item": "_Test Item",
"item_group": "_Test Item Group",
}
def test_execute_all_manufacturing_reports(self):
"""Test that all script report in manufacturing modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES:
for report, filter in self.REPORT_FILTER_TEST_CASES:
with self.subTest(report=report):
execute_script_report(
report_name=report,
module="Manufacturing",
filters=filter,
default_filters=DEFAULT_FILTERS,
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
default_filters=self.DEFAULT_FILTERS,
optional_filters=self.OPTIONAL_FILTERS if filter.get("_optional") else None,
)

View File

@@ -147,10 +147,10 @@ class BOMConfigurator {
if (!node.expanded) {
view.tree.load_children(node, true);
$(node.parent[0]).find(".tree-children").show();
node.$toolbar.find(".expand-all-btn").html("Collapse All");
node.$toolbar.find(".expand-all-btn").html(__("Collapse All"));
} else {
node.$tree_link.trigger("click");
node.$toolbar.find(".expand-all-btn").html("Expand All");
node.$toolbar.find(".expand-all-btn").html(__("Expand All"));
}
},
condition: function (node) {
@@ -190,10 +190,10 @@ class BOMConfigurator {
if (!node.expanded) {
view.tree.load_children(node, true);
$(node.parent[0]).find(".tree-children").show();
node.$toolbar.find(".expand-all-btn").html("Collapse All");
node.$toolbar.find(".expand-all-btn").html(__("Collapse All"));
} else {
node.$tree_link.trigger("click");
node.$toolbar.find(".expand-all-btn").html("Expand All");
node.$toolbar.find(".expand-all-btn").html(__("Expand All"));
}
},
condition: function (node) {

View File

@@ -25,6 +25,14 @@ erpnext.buying = {
};
});
this.frm.set_query("project", function (doc) {
return {
filters: {
company: doc.company,
},
};
});
if (this.frm.doc.__islocal
&& frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) {
@@ -145,6 +153,18 @@ erpnext.buying = {
});
}
company(){
if(!frappe.meta.has_field(this.frm.doc.doctype, "billing_address")) return;
frappe.call({
method: "erpnext.setup.doctype.company.company.get_default_company_address",
args: { name: this.frm.doc.company, existing_address:this.frm.doc.billing_address },
callback: (r) => {
this.frm.set_value("billing_address", r.message || "");
},
});
}
supplier_address() {
erpnext.utils.get_address_display(this.frm);
erpnext.utils.set_taxes_from_address(this.frm, "supplier_address", "supplier_address", "supplier_address");

View File

@@ -813,6 +813,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
validate() {
this.apply_pricing_rule()
this.calculate_taxes_and_totals(false);
}
@@ -959,7 +960,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let is_drop_ship = me.frm.doc.items.some(item => item.delivered_by_supplier);
if (!is_drop_ship) {
console.log('get_shipping_address');
erpnext.utils.get_shipping_address(this.frm, function() {
set_party_account(set_pricing);
});
@@ -975,6 +975,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
transaction_date() {
this.apply_pricing_rule()
if (this.frm.doc.transaction_date) {
this.frm.transaction_date = this.frm.doc.transaction_date;
frappe.ui.form.trigger(this.frm.doc.doctype, "currency");
@@ -983,6 +984,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
posting_date() {
var me = this;
me.apply_pricing_rule()
if (this.frm.doc.posting_date) {
this.frm.posting_date = this.frm.doc.posting_date;
@@ -2310,6 +2312,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
fieldname: "batch_no",
label: __("Batch No"),
hidden: true
},
{
fieldtype: "Data",
fieldname: "child_row_reference",
label: __("Child Row Reference"),
hidden: true
}
]
}
@@ -2353,14 +2361,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (this.has_inspection_required(item)) {
let dialog_items = dialog.fields_dict.items;
dialog_items.df.data.push({
"docname": item.name,
"item_code": item.item_code,
"item_name": item.item_name,
"qty": item.qty,
"description": item.description,
"serial_no": item.serial_no,
"batch_no": item.batch_no,
"sample_size": item.sample_quantity
"sample_size": item.sample_quantity,
"child_row_reference": item.name,
});
dialog_items.grid.refresh();
}

View File

@@ -285,6 +285,7 @@ erpnext.PointOfSale.Controller = class {
edit_cart: () => this.payment.edit_cart(),
customer_details_updated: (details) => {
this.item_selector.load_items_data();
this.customer_details = details;
// will add/remove LP payment method
this.payment.render_loyalty_points_payment_mode();

View File

@@ -390,6 +390,14 @@ erpnext.PointOfSale.ItemCart = class {
input_class: "input-xs",
onchange: function () {
this.value = flt(this.value);
if (this.value > 100) {
frappe.msgprint({
title: __("Invalid Discount"),
indicator: "red",
message: __("Discount cannot be greater than 100%."),
});
this.value = 0;
}
frappe.model.set_value(
frm.doc.doctype,
frm.doc.name,

View File

@@ -315,8 +315,12 @@ erpnext.PointOfSale.ItemDetails = class {
frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => {
const field_control = this[`${fieldname}_control`];
const item_row_is_being_edited = this.compare_with_current_item(item_row);
if (item_row_is_being_edited && field_control && field_control.get_value() !== value) {
if (
item_row_is_being_edited &&
field_control &&
field_control.get_value() !== value &&
value == item_row[fieldname]
) {
field_control.set_value(value);
cur_pos.update_cart_html(item_row);
}

View File

@@ -235,7 +235,7 @@ erpnext.PointOfSale.Payment = class {
frappe.ui.form.on("Sales Invoice Payment", "amount", (frm, cdt, cdn) => {
// for setting correct amount after loyalty points are redeemed
const default_mop = locals[cdt][cdn];
const mode = default_mop.mode_of_payment.replace(/ +/g, "_").toLowerCase();
const mode = this.sanitize_mode_of_payment(default_mop.mode_of_payment);
if (this[`${mode}_control`] && this[`${mode}_control`].get_value() != default_mop.amount) {
this[`${mode}_control`].set_value(default_mop.amount);
}
@@ -388,7 +388,7 @@ erpnext.PointOfSale.Payment = class {
this.$payment_modes.html(
`${payments
.map((p, i) => {
const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
const mode = this.sanitize_mode_of_payment(p.mode_of_payment);
const payment_type = p.type;
const margin = i % 2 === 0 ? "pr-2" : "pl-2";
const amount = p.amount > 0 ? format_currency(p.amount, currency) : "";
@@ -407,7 +407,7 @@ erpnext.PointOfSale.Payment = class {
);
payments.forEach((p) => {
const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
const mode = this.sanitize_mode_of_payment(p.mode_of_payment);
const me = this;
this[`${mode}_control`] = frappe.ui.form.make_control({
df: {
@@ -442,7 +442,7 @@ erpnext.PointOfSale.Payment = class {
const doc = this.events.get_frm().doc;
const payments = doc.payments;
payments.forEach((p) => {
const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
const mode = this.sanitize_mode_of_payment(p.mode_of_payment);
if (p.default) {
setTimeout(() => {
this.$payment_modes.find(`.${mode}.mode-of-payment-control`).parent().click();
@@ -612,4 +612,12 @@ erpnext.PointOfSale.Payment = class {
toggle_component(show) {
show ? this.$component.css("display", "flex") : this.$component.css("display", "none");
}
sanitize_mode_of_payment(mode_of_payment) {
return mode_of_payment
.replace(/ +/g, "_")
.replace(/[^\p{L}\p{N}_-]/gu, "")
.replace(/^[^_a-zA-Z\p{L}]+/u, "")
.toLowerCase();
}
};

View File

@@ -292,7 +292,7 @@ def get_or_create_tax_group(company_name, root_type):
tax_group_account.flags.ignore_links = True
tax_group_account.flags.ignore_validate = True
tax_group_account.insert(ignore_permissions=True)
tax_group_account.insert(ignore_permissions=True, ignore_if_duplicate=True)
tax_group_name = tax_group_account.name

View File

@@ -181,6 +181,9 @@ class DeprecatedBatchNoValuation:
stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
self.stock_value_change += stock_value_change
self.non_batchwise_balance_value[batch_no] -= stock_value_change
self.non_batchwise_balance_qty[batch_no] -= ledger.qty
frappe.db.set_value(
"Serial and Batch Entry",
ledger.name,
@@ -220,7 +223,6 @@ class DeprecatedBatchNoValuation:
.select(
sle.batch_no,
Sum(sle.actual_qty).as_("batch_qty"),
Sum(sle.stock_value_difference).as_("batch_value"),
)
.where(
(sle.item_code == self.sle.item_code)
@@ -237,11 +239,59 @@ class DeprecatedBatchNoValuation:
if self.sle.name:
query = query.where(sle.name != self.sle.name)
for d in query.run(as_dict=True):
self.non_batchwise_balance_value[d.batch_no] += flt(d.batch_value)
self.non_batchwise_balance_qty[d.batch_no] += flt(d.batch_qty)
batch_data = query.run(as_dict=True)
for d in batch_data:
self.available_qty[d.batch_no] += flt(d.batch_qty)
last_sle = self.get_last_sle_for_non_batch()
for d in batch_data:
self.non_batchwise_balance_value[d.batch_no] += flt(last_sle.stock_value)
self.non_batchwise_balance_qty[d.batch_no] += flt(last_sle.qty_after_transaction)
def get_last_sle_for_non_batch(self):
from erpnext.stock.utils import get_combine_datetime
sle = frappe.qb.DocType("Stock Ledger Entry")
batch = frappe.qb.DocType("Batch")
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
if not self.sle.creation:
posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1)
timestamp_condition = sle.posting_datetime < posting_datetime
if self.sle.creation:
timestamp_condition |= (sle.posting_datetime == posting_datetime) & (
sle.creation < self.sle.creation
)
query = (
frappe.qb.from_(sle)
.inner_join(batch)
.on(sle.batch_no == batch.name)
.select(
sle.stock_value,
sle.qty_after_transaction,
)
.where(
(sle.item_code == self.sle.item_code)
& (sle.warehouse == self.sle.warehouse)
& (sle.batch_no.isnotnull())
& (batch.use_batchwise_valuation == 0)
& (sle.is_cancelled == 0)
)
.where(timestamp_condition)
.orderby(sle.posting_datetime, order=Order.desc)
.orderby(sle.creation, order=Order.desc)
.limit(1)
)
if self.sle.name:
query = query.where(sle.name != self.sle.name)
data = query.run(as_dict=True)
return data[0] if data else {}
@deprecated
def set_balance_value_from_bundle(self) -> None:
bundle = frappe.qb.DocType("Serial and Batch Bundle")

View File

@@ -65,7 +65,7 @@ class ClosingStockBalance(Document):
& (
(table.from_date.between(self.from_date, self.to_date))
| (table.to_date.between(self.from_date, self.to_date))
| ((table.from_date >= self.from_date) & (table.to_date >= self.to_date))
| ((self.from_date >= table.from_date) & (table.from_date >= self.to_date))
)
)
)

View File

@@ -237,8 +237,13 @@ class InventoryDimension(Document):
custom_fields["Stock Ledger Entry"] = dimension_field
filter_custom_fields = {}
ignore_doctypes = ["Serial and Batch Bundle", "Serial and Batch Entry", "Pick List Item"]
if custom_fields:
for doctype, fields in custom_fields.items():
if doctype in ignore_doctypes:
continue
if isinstance(fields, dict):
fields = [fields]

View File

@@ -322,7 +322,7 @@ frappe.ui.form.on("Material Request", {
default: 1,
},
],
primary_action_label: "Get Items",
primary_action_label: __("Get Items"),
primary_action(values) {
if (!values) return;
values["company"] = frm.doc.company;

View File

@@ -766,6 +766,7 @@ def raise_work_orders(material_request):
)
wo_order.set_work_order_operations()
wo_order.flags.ignore_validate = True
wo_order.flags.ignore_mandatory = True
wo_order.save()

View File

@@ -14,6 +14,12 @@ frappe.listview_settings["Material Request"] = {
}
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) {
return [__("Pending"), "orange", "per_ordered,=,0"];
} else if (
doc.docstatus == 1 &&
flt(doc.per_ordered, precision) < 100 &&
doc.material_request_type == "Material Transfer"
) {
return [__("Partially Received"), "yellow", "per_ordered,<,100"];
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) < 100) {
return [__("Partially ordered"), "yellow", "per_ordered,<,100"];
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 100) {

View File

@@ -17,6 +17,7 @@ from erpnext.stock.doctype.material_request.material_request import (
make_supplier_quotation,
raise_work_orders,
)
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
class TestMaterialRequest(FrappeTestCase):
@@ -59,6 +60,43 @@ class TestMaterialRequest(FrappeTestCase):
self.assertEqual(se.doctype, "Stock Entry")
self.assertEqual(len(se.get("items")), len(mr.get("items")))
def test_partial_make_stock_entry(self):
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry as _make_stock_entry
mr = frappe.copy_doc(test_records[0]).insert()
source_wh = create_warehouse(
warehouse_name="_Test Source Warehouse",
properties={"parent_warehouse": "All Warehouses - _TC"},
company="_Test Company",
)
mr = frappe.get_doc("Material Request", mr.name)
mr.material_request_type = "Material Transfer"
for row in mr.items:
_make_stock_entry(
item_code=row.item_code,
qty=10,
to_warehouse=source_wh,
company="_Test Company",
rate=100,
)
row.from_warehouse = source_wh
row.qty = 10
mr.save()
mr.submit()
se = make_stock_entry(mr.name)
se.get("items")[0].qty = 5
se.insert()
se.submit()
mr.reload()
self.assertEqual(mr.status, "Partially Received")
def test_in_transit_make_stock_entry(self):
mr = frappe.copy_doc(test_records[0]).insert()

View File

@@ -15,6 +15,7 @@
"inspection_type",
"reference_type",
"reference_name",
"child_row_reference",
"section_break_7",
"item_code",
"item_serial_no",
@@ -238,6 +239,15 @@
"fieldname": "manual_inspection",
"fieldtype": "Check",
"label": "Manual Inspection"
},
{
"fieldname": "child_row_reference",
"fieldtype": "Data",
"hidden": 1,
"label": "Child Row Reference",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"icon": "fa fa-search",
@@ -245,7 +255,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-08-23 11:56:50.282878",
"modified": "2024-12-30 19:08:16.611192",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection",
@@ -272,4 +282,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"states": []
}
}

View File

@@ -29,6 +29,7 @@ class QualityInspection(Document):
amended_from: DF.Link | None
batch_no: DF.Link | None
bom_no: DF.Link | None
child_row_reference: DF.Data | None
description: DF.SmallText | None
inspected_by: DF.Link
inspection_type: DF.Literal["", "Incoming", "Outgoing", "In Process"]
@@ -74,6 +75,64 @@ class QualityInspection(Document):
self.inspect_and_set_status()
self.validate_inspection_required()
self.set_child_row_reference()
def set_child_row_reference(self):
if self.child_row_reference:
return
if not (self.reference_type and self.reference_name):
return
doctype = self.reference_type + " Item"
if self.reference_type == "Stock Entry":
doctype = "Stock Entry Detail"
child_row_references = frappe.get_all(
doctype,
filters={"parent": self.reference_name, "item_code": self.item_code},
pluck="name",
)
if not child_row_references:
return
if len(child_row_references) == 1:
self.child_row_reference = child_row_references[0]
else:
self.distribute_child_row_reference(child_row_references)
def distribute_child_row_reference(self, child_row_references):
quality_inspections = frappe.get_all(
"Quality Inspection",
filters={
"reference_name": self.reference_name,
"item_code": self.item_code,
"docstatus": ("<", 2),
},
fields=["name", "child_row_reference", "docstatus"],
order_by="child_row_reference desc",
)
for row in quality_inspections:
if not child_row_references:
break
if row.child_row_reference and row.child_row_reference in child_row_references:
child_row_references.remove(row.child_row_reference)
continue
if row.docstatus == 1:
continue
if row.name == self.name:
self.child_row_reference = child_row_references[0]
else:
frappe.db.set_value(
"Quality Inspection", row.name, "child_row_reference", child_row_references[0]
)
child_row_references.remove(child_row_references[0])
def validate_inspection_required(self):
if self.reference_type in ["Purchase Receipt", "Purchase Invoice"] and not frappe.get_cached_value(
@@ -157,35 +216,38 @@ class QualityInspection(Document):
)
else:
args = [quality_inspection, self.modified, self.reference_name, self.item_code]
doctype = self.reference_type + " Item"
if self.reference_type == "Stock Entry":
doctype = "Stock Entry Detail"
if self.reference_type and self.reference_name:
conditions = ""
if doctype and self.reference_name:
child_doc = frappe.qb.DocType(doctype)
query = (
frappe.qb.update(child_doc)
.set(child_doc.quality_inspection, quality_inspection)
.where(
(child_doc.parent == self.reference_name) & (child_doc.item_code == self.item_code)
)
)
if self.batch_no and self.docstatus == 1:
conditions += " and t1.batch_no = %s"
args.append(self.batch_no)
query = query.where(child_doc.batch_no == self.batch_no)
if self.docstatus == 2: # if cancel, then remove qi link wherever same name
conditions += " and t1.quality_inspection = %s"
args.append(self.name)
query = query.where(child_doc.quality_inspection == self.name)
frappe.db.sql(
f"""
UPDATE
`tab{doctype}` t1, `tab{self.reference_type}` t2
SET
t1.quality_inspection = %s, t2.modified = %s
WHERE
t1.parent = %s
and t1.item_code = %s
and t1.parent = t2.name
{conditions}
""",
args,
if self.child_row_reference:
query = query.where(child_doc.name == self.child_row_reference)
query.run()
frappe.db.set_value(
self.reference_type,
self.reference_name,
"modified",
self.modified,
)
def inspect_and_set_status(self):

View File

@@ -166,7 +166,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
for qty, valuation in {10: 100, 20: 200}.items():
stock_queue.append([qty, valuation])
qty_after_transaction += qty
balance_value += qty_after_transaction * valuation
balance_value += qty * valuation
doc = frappe.get_doc(
{
@@ -177,6 +177,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
"incoming_rate": valuation,
"qty_after_transaction": qty_after_transaction,
"stock_value_difference": valuation * qty,
"stock_value": balance_value,
"balance_value": balance_value,
"valuation_rate": balance_value / qty_after_transaction,
"actual_qty": qty,

View File

@@ -3289,8 +3289,10 @@ def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=N
doc.append("entries", {"serial_no": serial_no, "warehouse": row.warehouse, "qty": -1})
elif row.batches_to_be_consume:
precision = frappe.get_precision("Serial and Batch Entry", "qty")
doc.has_batch_no = 1
for batch_no, qty in row.batches_to_be_consume.items():
qty = flt(qty, precision)
doc.append("entries", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty * -1})
if not doc.entries:

View File

@@ -592,7 +592,10 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False):
if tax.valid_from or tax.maximum_net_rate:
# In purchase Invoice first preference will be given to supplier invoice date
# if supplier date is not present then posting date
validation_date = args.get("bill_date") or args.get("transaction_date")
validation_date = (
args.get("bill_date") or args.get("posting_date") or args.get("transaction_date")
)
if getdate(tax.valid_from) <= getdate(validation_date) and is_within_valid_range(args, tax):
taxes_with_validity.append(tax)

View File

@@ -775,6 +775,9 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False
postprocess=post_process,
)
if not target_doc.get("items"):
add_po_items_to_pr(source_doc, target_doc)
if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
target_doc.save()
@@ -794,3 +797,29 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False
)
return target_doc
def add_po_items_to_pr(scr_doc, target_doc):
fg_items = {(item.item_code, item.purchase_order): item.qty for item in scr_doc.items}
for (item_code, po_name), fg_qty in fg_items.items():
po_doc = frappe.get_doc("Purchase Order", po_name)
for item in po_doc.items:
if item.fg_item != item_code:
continue
qty = (item.stock_qty - item.received_qty) * fg_qty / item.fg_item_qty
if qty:
target_doc.append(
"items",
{
"item_code": item.item_code,
"item_name": item.item_name,
"description": item.description,
"qty": qty,
"rate": item.rate,
"warehouse": item.warehouse,
"purchase_order": item.parent,
"purchase_order_item": item.name,
},
)

View File

@@ -1137,6 +1137,80 @@ class TestSubcontractingReceipt(FrappeTestCase):
self.assertEqual(pr_details[0]["total_taxes_and_charges"], 60)
@change_settings("Buying Settings", {"auto_create_purchase_receipt": 1})
def test_auto_create_purchase_receipt_with_no_reference_of_po_item(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
fg_item = "Subcontracted Item SA1"
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": 10,
"rate": 100,
"fg_item": fg_item,
"fg_item_qty": 5,
},
]
po = create_purchase_order(
rm_items=service_items,
is_subcontracted=1,
supplier_warehouse="_Test Warehouse 1 - _TC",
do_not_submit=True,
)
po.append(
"taxes",
{
"account_head": "_Test Account Excise Duty - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "Excise Duty",
"doctype": "Purchase Taxes and Charges",
"rate": 10,
},
)
po.save()
po.submit()
sco = get_subcontracting_order(po_name=po.name)
for row in sco.items:
row.db_set("purchase_order_item", None)
sco.reload()
for row in sco.items:
self.assertFalse(row.purchase_order_item)
rm_items = get_rm_items(sco.supplied_items)
itemwise_details = make_stock_in_entry(rm_items=rm_items)
make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
scr = make_subcontracting_receipt(sco.name)
for row in scr.items:
self.assertFalse(row.purchase_order_item)
scr.items[0].qty = 3
scr.save()
scr.submit()
pr_details = frappe.get_all(
"Purchase Receipt",
filters={"subcontracting_receipt": scr.name},
fields=["name", "total_taxes_and_charges"],
)
self.assertTrue(pr_details)
pr_qty = frappe.db.get_value("Purchase Receipt Item", {"parent": pr_details[0]["name"]}, "qty")
self.assertEqual(pr_qty, 6)
self.assertEqual(pr_details[0]["total_taxes_and_charges"], 60)
def test_use_serial_batch_fields_for_subcontracting_receipt(self):
fg_item = make_item(
"Test Subcontracted Item With Batch No",

View File

@@ -17,7 +17,7 @@ frappe.ready(function() {
if($("#footer-subscribe-email").val() && validate_email($("#footer-subscribe-email").val())) {
$("#footer-subscribe-email").attr('disabled', true);
$("#footer-subscribe-button").html("Sending...")
$("#footer-subscribe-button").html(__("Sending..."))
.attr("disabled", true);
erpnext.subscribe_to_newsletter({
email: $("#footer-subscribe-email").val(),

View File

@@ -12,14 +12,14 @@
{% endif %}
{% for d in doc.taxes %}
{% if d.base_tax_amount %}
{% if d.tax_amount %}
<div class="order-taxes w-100 mt-5">
<div class="col-4 d-flex border-btm pb-5">
<div class="item-grand-total col-8">
{{ d.description }}
</div>
<div class="item-grand-total col-4 text-right pr-0">
{{ d.get_formatted("base_tax_amount") }}
{{ d.get_formatted("tax_amount") }}
</div>
</div>
</div>

View File

@@ -40,7 +40,7 @@
<p>
<a href="/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn={{ doc.name }}&dt={{ doc.doctype }}&submit_doc=1&order_type=Shopping Cart"
class="btn btn-primary btn-sm" id="pay-for-order">
{{ _("Pay") }} {{doc.get_formatted("grand_total") }}
{{ _("Pay", null, "Amount") }} {{ pay_amount }}
</a>
</p>
</div>