Merge pull request #32493 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
Deepesh Garg
2022-10-04 17:41:32 +05:30
committed by GitHub
42 changed files with 827 additions and 474 deletions

View File

@@ -99,7 +99,7 @@ class BankClearance(Document):
.where(loan_disbursement.clearance_date.isnull()) .where(loan_disbursement.clearance_date.isnull())
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account])) .where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
.orderby(loan_disbursement.disbursement_date) .orderby(loan_disbursement.disbursement_date)
.orderby(loan_disbursement.name, frappe.qb.desc) .orderby(loan_disbursement.name, order=frappe.qb.desc)
).run(as_dict=1) ).run(as_dict=1)
loan_repayment = frappe.qb.DocType("Loan Repayment") loan_repayment = frappe.qb.DocType("Loan Repayment")
@@ -126,7 +126,9 @@ class BankClearance(Document):
if frappe.db.has_column("Loan Repayment", "repay_from_salary"): if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0)) query = query.where((loan_repayment.repay_from_salary == 0))
query = query.orderby(loan_repayment.posting_date).orderby(loan_repayment.name, frappe.qb.desc) query = query.orderby(loan_repayment.posting_date).orderby(
loan_repayment.name, order=frappe.qb.desc
)
loan_repayments = query.run(as_dict=True) loan_repayments = query.run(as_dict=True)

View File

@@ -186,8 +186,10 @@
{ {
"fetch_from": "bank_account.bank", "fetch_from": "bank_account.bank",
"fieldname": "bank", "fieldname": "bank",
"fieldtype": "Read Only", "fieldtype": "Link",
"label": "Bank" "label": "Bank",
"options": "Bank",
"read_only": 1
}, },
{ {
"fetch_from": "bank_account.bank_account_no", "fetch_from": "bank_account.bank_account_no",
@@ -366,10 +368,11 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-09-18 12:24:14.178853", "modified": "2022-09-30 16:19:43.680025",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Request", "name": "Payment Request",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -401,5 +404,6 @@
} }
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
"states": []
} }

View File

@@ -98,7 +98,6 @@
"section_break_44", "section_break_44",
"apply_discount_on", "apply_discount_on",
"base_discount_amount", "base_discount_amount",
"additional_discount_account",
"column_break_46", "column_break_46",
"additional_discount_percentage", "additional_discount_percentage",
"discount_amount", "discount_amount",
@@ -1387,12 +1386,6 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "additional_discount_account",
"fieldtype": "Link",
"label": "Additional Discount Account",
"options": "Account"
},
{ {
"default": "0", "default": "0",
"fieldname": "ignore_default_payment_terms_template", "fieldname": "ignore_default_payment_terms_template",
@@ -1445,7 +1438,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-09-13 23:39:54.525037", "modified": "2022-09-27 11:07:55.766844",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@@ -669,9 +669,6 @@ class PurchaseInvoice(BuyingController):
exchange_rate_map, net_rate_map = get_purchase_document_details(self) exchange_rate_map, net_rate_map = get_purchase_document_details(self)
enable_discount_accounting = cint(
frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
)
provisional_accounting_for_non_stock_items = cint( provisional_accounting_for_non_stock_items = cint(
frappe.db.get_value( frappe.db.get_value(
"Company", self.company, "enable_provisional_accounting_for_non_stock_items" "Company", self.company, "enable_provisional_accounting_for_non_stock_items"
@@ -1159,9 +1156,6 @@ class PurchaseInvoice(BuyingController):
def make_tax_gl_entries(self, gl_entries): def make_tax_gl_entries(self, gl_entries):
# tax table gl entries # tax table gl entries
valuation_tax = {} valuation_tax = {}
enable_discount_accounting = cint(
frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
)
for tax in self.get("taxes"): for tax in self.get("taxes"):
amount, base_amount = self.get_tax_amounts(tax, None) amount, base_amount = self.get_tax_amounts(tax, None)
@@ -1249,15 +1243,6 @@ class PurchaseInvoice(BuyingController):
) )
) )
@property
def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"):
self._enable_discount_accounting = cint(
frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
)
return self._enable_discount_accounting
def make_internal_transfer_gl_entries(self, gl_entries): def make_internal_transfer_gl_entries(self, gl_entries):
if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges):
account_currency = get_account_currency(self.unrealized_profit_loss_account) account_currency = get_account_currency(self.unrealized_profit_loss_account)

View File

@@ -74,7 +74,6 @@
"manufacturer_part_no", "manufacturer_part_no",
"accounting", "accounting",
"expense_account", "expense_account",
"discount_account",
"col_break5", "col_break5",
"is_fixed_asset", "is_fixed_asset",
"asset_location", "asset_location",
@@ -860,12 +859,6 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "discount_account",
"fieldtype": "Link",
"label": "Discount Account",
"options": "Account"
},
{ {
"fieldname": "product_bundle", "fieldname": "product_bundle",
"fieldtype": "Link", "fieldtype": "Link",
@@ -877,7 +870,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-06-17 05:31:10.520171", "modified": "2022-09-27 10:54:23.980713",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@@ -22,9 +22,12 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
from erpnext.assets.doctype.asset.depreciation import ( from erpnext.assets.doctype.asset.depreciation import (
depreciate_asset,
get_disposal_account_and_cost_center, get_disposal_account_and_cost_center,
get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_disposal,
get_gl_entries_on_asset_regain, get_gl_entries_on_asset_regain,
reset_depreciation_schedule,
reverse_depreciation_entry_made_after_disposal,
) )
from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.accounts_controller import validate_account_head
from erpnext.controllers.selling_controller import SellingController from erpnext.controllers.selling_controller import SellingController
@@ -1086,18 +1089,20 @@ class SalesInvoice(SellingController):
asset.db_set("disposal_date", None) asset.db_set("disposal_date", None)
if asset.calculate_depreciation: if asset.calculate_depreciation:
self.reverse_depreciation_entry_made_after_disposal(asset) posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
self.reset_depreciation_schedule(asset) reverse_depreciation_entry_made_after_disposal(asset, posting_date)
reset_depreciation_schedule(asset, self.posting_date)
else: else:
if asset.calculate_depreciation:
depreciate_asset(asset, self.posting_date)
asset.reload()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name") asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name")
) )
asset.db_set("disposal_date", self.posting_date) asset.db_set("disposal_date", self.posting_date)
if asset.calculate_depreciation:
self.depreciate_asset(asset)
for gle in fixed_asset_gl_entries: for gle in fixed_asset_gl_entries:
gle["against"] = self.customer gle["against"] = self.customer
gl_entries.append(self.get_gl_dict(gle, item=item)) gl_entries.append(self.get_gl_dict(gle, item=item))

View File

@@ -8,7 +8,7 @@ import frappe
from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe.tests.utils import change_settings from frappe.tests.utils import change_settings
from frappe.utils import add_days, flt, getdate, nowdate from frappe.utils import add_days, flt, getdate, nowdate, today
import erpnext import erpnext
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
@@ -3196,6 +3196,37 @@ class TestSalesInvoice(unittest.TestCase):
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
) )
def test_batch_expiry_for_sales_invoice_return(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.item.test_item import make_item
item = make_item(
"_Test Batch Item For Return Check",
{
"is_purchase_item": 1,
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TBIRC.#####",
},
)
pr = make_purchase_receipt(qty=1, item_code=item.name)
batch_no = pr.items[0].batch_no
si = create_sales_invoice(qty=1, item_code=item.name, update_stock=1, batch_no=batch_no)
si.load_from_db()
batch_no = si.items[0].batch_no
self.assertTrue(batch_no)
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
return_si = make_return_doc(si.doctype, si.name)
return_si.save().submit()
self.assertTrue(return_si.docstatus == 1)
def get_sales_invoice_for_e_invoice(): def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill() si = make_sales_invoice_for_ewaybill()
@@ -3289,6 +3320,7 @@ def create_sales_invoice(**args):
"serial_no": args.serial_no, "serial_no": args.serial_no,
"conversion_factor": 1, "conversion_factor": 1,
"incoming_rate": args.incoming_rate or 0, "incoming_rate": args.incoming_rate or 0,
"batch_no": args.batch_no or None,
}, },
) )

View File

@@ -335,6 +335,9 @@ def get_advance_vouchers(
"party": ["in", parties], "party": ["in", parties],
} }
if party_type == "Customer":
filters.update({"against_voucher": ["is", "not set"]})
if company: if company:
filters["company"] = company filters["company"] = company
if from_date and to_date: if from_date and to_date:

View File

@@ -370,7 +370,7 @@ def get_conditions(filters):
where parent=`tabSales Invoice`.name where parent=`tabSales Invoice`.name
and ifnull(`tab{table}`.{field}, '') = %({field})s)""" and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment") conditions += get_sales_invoice_item_field_condition("mode_of_payment", "Sales Invoice Payment")
conditions += get_sales_invoice_item_field_condition("cost_center") conditions += get_sales_invoice_item_field_condition("cost_center")
conditions += get_sales_invoice_item_field_condition("warehouse") conditions += get_sales_invoice_item_field_condition("warehouse")
conditions += get_sales_invoice_item_field_condition("brand") conditions += get_sales_invoice_item_field_condition("brand")

View File

@@ -86,7 +86,7 @@ def get_fiscal_years(
) )
) )
query = query.orderby(FY.year_start_date, Order.desc) query = query.orderby(FY.year_start_date, order=Order.desc)
fiscal_years = query.run(as_dict=True) fiscal_years = query.run(as_dict=True)
frappe.cache().hset("fiscal_years", company, fiscal_years) frappe.cache().hset("fiscal_years", company, fiscal_years)

View File

@@ -388,7 +388,7 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Status", "label": "Status",
"no_copy": 1, "no_copy": 1,
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt", "options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nDecapitalized",
"read_only": 1 "read_only": 1
}, },
{ {

View File

@@ -828,7 +828,9 @@ class Asset(AccountsController):
def update_maintenance_status(): def update_maintenance_status():
assets = frappe.get_all("Asset", filters={"docstatus": 1, "maintenance_required": 1}) assets = frappe.get_all(
"Asset", filters={"docstatus": 1, "maintenance_required": 1, "disposal_date": ("is", "not set")}
)
for asset in assets: for asset in assets:
asset = frappe.get_doc("Asset", asset.name) asset = frappe.get_doc("Asset", asset.name)

View File

@@ -4,11 +4,12 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint, flt, getdate, today from frappe.utils import add_months, cint, flt, getdate, nowdate, today
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts, get_checks_for_pl_and_bs_accounts,
) )
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
def post_depreciation_entries(date=None, commit=True): def post_depreciation_entries(date=None, commit=True):
@@ -196,6 +197,11 @@ def scrap_asset(asset_name):
_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status) _("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status)
) )
date = today()
depreciate_asset(asset, date)
asset.reload()
depreciation_series = frappe.get_cached_value( depreciation_series = frappe.get_cached_value(
"Company", asset.company, "series_for_depreciation_entry" "Company", asset.company, "series_for_depreciation_entry"
) )
@@ -203,7 +209,7 @@ def scrap_asset(asset_name):
je = frappe.new_doc("Journal Entry") je = frappe.new_doc("Journal Entry")
je.voucher_type = "Journal Entry" je.voucher_type = "Journal Entry"
je.naming_series = depreciation_series je.naming_series = depreciation_series
je.posting_date = today() je.posting_date = date
je.company = asset.company je.company = asset.company
je.remark = "Scrap Entry for asset {0}".format(asset_name) je.remark = "Scrap Entry for asset {0}".format(asset_name)
@@ -214,7 +220,7 @@ def scrap_asset(asset_name):
je.flags.ignore_permissions = True je.flags.ignore_permissions = True
je.submit() je.submit()
frappe.db.set_value("Asset", asset_name, "disposal_date", today()) frappe.db.set_value("Asset", asset_name, "disposal_date", date)
frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name) frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name)
asset.set_status("Scrapped") asset.set_status("Scrapped")
@@ -225,6 +231,9 @@ def scrap_asset(asset_name):
def restore_asset(asset_name): def restore_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name) asset = frappe.get_doc("Asset", asset_name)
reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date)
reset_depreciation_schedule(asset, asset.disposal_date)
je = asset.journal_entry_for_scrap je = asset.journal_entry_for_scrap
asset.db_set("disposal_date", None) asset.db_set("disposal_date", None)
@@ -235,6 +244,91 @@ def restore_asset(asset_name):
asset.set_status() asset.set_status()
def depreciate_asset(asset, date):
asset.flags.ignore_validate_update_after_submit = True
asset.prepare_depreciation_data(date_of_disposal=date)
asset.save()
make_depreciation_entry(asset.name, date)
def reset_depreciation_schedule(asset, date):
asset.flags.ignore_validate_update_after_submit = True
# recreate original depreciation schedule of the asset
asset.prepare_depreciation_data(date_of_return=date)
modify_depreciation_schedule_for_asset_repairs(asset)
asset.save()
def modify_depreciation_schedule_for_asset_repairs(asset):
asset_repairs = frappe.get_all(
"Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"]
)
for repair in asset_repairs:
if repair.increase_in_asset_life:
asset_repair = frappe.get_doc("Asset Repair", repair.name)
asset_repair.modify_depreciation_schedule()
asset.prepare_depreciation_data()
def reverse_depreciation_entry_made_after_disposal(asset, date):
row = -1
finance_book = asset.get("schedules")[0].get("finance_book")
for schedule in asset.get("schedules"):
if schedule.finance_book != finance_book:
row = 0
finance_book = schedule.finance_book
else:
row += 1
if schedule.schedule_date == date:
if not disposal_was_made_on_original_schedule_date(
asset, schedule, row, date
) or disposal_happens_in_the_future(date):
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate()
frappe.flags.is_reverse_depr_entry = True
reverse_journal_entry.submit()
frappe.flags.is_reverse_depr_entry = False
asset.flags.ignore_validate_update_after_submit = True
schedule.journal_entry = None
depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry)
asset.finance_books[0].value_after_depreciation += depreciation_amount
asset.save()
def get_depreciation_amount_in_je(journal_entry):
if journal_entry.accounts[0].debit_in_account_currency:
return journal_entry.accounts[0].debit_in_account_currency
else:
return journal_entry.accounts[0].credit_in_account_currency
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
def disposal_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_disposal):
for finance_book in asset.get("finance_books"):
if schedule.finance_book == finance_book.finance_book:
orginal_schedule_date = add_months(
finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation)
)
if orginal_schedule_date == posting_date_of_disposal:
return True
return False
def disposal_happens_in_the_future(posting_date_of_disposal):
if posting_date_of_disposal > getdate():
return True
return False
def get_gl_entries_on_asset_regain( def get_gl_entries_on_asset_regain(
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None
): ):

View File

@@ -4,10 +4,23 @@
import unittest import unittest
import frappe import frappe
from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate from frappe.utils import (
add_days,
add_months,
cstr,
flt,
get_first_day,
get_last_day,
getdate,
nowdate,
)
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.assets.doctype.asset.asset import make_sales_invoice, split_asset from erpnext.assets.doctype.asset.asset import (
make_sales_invoice,
split_asset,
update_maintenance_status,
)
from erpnext.assets.doctype.asset.depreciation import ( from erpnext.assets.doctype.asset.depreciation import (
post_depreciation_entries, post_depreciation_entries,
restore_asset, restore_asset,
@@ -178,28 +191,48 @@ class TestAsset(AssetSetup):
self.assertEqual(doc.items[0].is_fixed_asset, 1) self.assertEqual(doc.items[0].is_fixed_asset, 1)
def test_scrap_asset(self): def test_scrap_asset(self):
date = nowdate()
purchase_date = add_months(get_first_day(date), -2)
asset = create_asset( asset = create_asset(
calculate_depreciation=1, calculate_depreciation=1,
available_for_use_date="2020-01-01", available_for_use_date=purchase_date,
purchase_date="2020-01-01", purchase_date=purchase_date,
expected_value_after_useful_life=10000, expected_value_after_useful_life=10000,
total_number_of_depreciations=10, total_number_of_depreciations=10,
frequency_of_depreciation=1, frequency_of_depreciation=1,
submit=1, submit=1,
) )
post_depreciation_entries(date=add_months("2020-01-01", 4)) post_depreciation_entries(date=add_months(purchase_date, 2))
asset.load_from_db()
accumulated_depr_amount = flt(
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"),
)
self.assertEquals(accumulated_depr_amount, 18000.0)
scrap_asset(asset.name) scrap_asset(asset.name)
asset.load_from_db() asset.load_from_db()
accumulated_depr_amount = flt(
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"),
)
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
asset.finance_books[0], 9000, add_months(get_last_day(purchase_date), 1), date
)
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
self.assertEquals(accumulated_depr_amount, 18000.00 + pro_rata_amount)
self.assertEqual(asset.status, "Scrapped") self.assertEqual(asset.status, "Scrapped")
self.assertTrue(asset.journal_entry_for_scrap) self.assertTrue(asset.journal_entry_for_scrap)
expected_gle = ( expected_gle = (
("_Test Accumulated Depreciations - _TC", 36000.0, 0.0), ("_Test Accumulated Depreciations - _TC", 18000.0 + pro_rata_amount, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0), ("_Test Gain/Loss on Asset Disposal - _TC", 82000.0 - pro_rata_amount, 0.0),
) )
gle = frappe.db.sql( gle = frappe.db.sql(
@@ -216,7 +249,64 @@ class TestAsset(AssetSetup):
self.assertFalse(asset.journal_entry_for_scrap) self.assertFalse(asset.journal_entry_for_scrap)
self.assertEqual(asset.status, "Partially Depreciated") self.assertEqual(asset.status, "Partially Depreciated")
accumulated_depr_amount = flt(
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"),
)
this_month_depr_amount = 9000.0 if get_last_day(date) == date else 0
self.assertEquals(accumulated_depr_amount, 18000.0 + this_month_depr_amount)
def test_gle_made_by_asset_sale(self): def test_gle_made_by_asset_sale(self):
date = nowdate()
purchase_date = add_months(get_first_day(date), -2)
asset = create_asset(
calculate_depreciation=1,
available_for_use_date=purchase_date,
purchase_date=purchase_date,
expected_value_after_useful_life=10000,
total_number_of_depreciations=10,
frequency_of_depreciation=1,
submit=1,
)
post_depreciation_entries(date=add_months(purchase_date, 2))
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
si.customer = "_Test Customer"
si.due_date = nowdate()
si.get("items")[0].rate = 25000
si.insert()
si.submit()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
asset.finance_books[0], 9000, add_months(get_last_day(purchase_date), 1), date
)
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
expected_gle = (
("_Test Accumulated Depreciations - _TC", 18000.0 + pro_rata_amount, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 57000.0 - pro_rata_amount, 0.0),
("Debtors - _TC", 25000.0, 0.0),
)
gle = frappe.db.sql(
"""select account, debit, credit from `tabGL Entry`
where voucher_type='Sales Invoice' and voucher_no = %s
order by account""",
si.name,
)
self.assertSequenceEqual(gle, expected_gle)
si.cancel()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
def test_asset_with_maintenance_required_status_after_sale(self):
asset = create_asset( asset = create_asset(
calculate_depreciation=1, calculate_depreciation=1,
available_for_use_date="2020-06-06", available_for_use_date="2020-06-06",
@@ -224,6 +314,7 @@ class TestAsset(AssetSetup):
expected_value_after_useful_life=10000, expected_value_after_useful_life=10000,
total_number_of_depreciations=3, total_number_of_depreciations=3,
frequency_of_depreciation=10, frequency_of_depreciation=10,
maintenance_required=1,
depreciation_start_date="2020-12-31", depreciation_start_date="2020-12-31",
submit=1, submit=1,
) )
@@ -239,24 +330,9 @@ class TestAsset(AssetSetup):
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
expected_gle = ( update_maintenance_status()
("_Test Accumulated Depreciations - _TC", 20490.2, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0),
("Debtors - _TC", 25000.0, 0.0),
)
gle = frappe.db.sql( self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
"""select account, debit, credit from `tabGL Entry`
where voucher_type='Sales Invoice' and voucher_no = %s
order by account""",
si.name,
)
self.assertSequenceEqual(gle, expected_gle)
si.cancel()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
def test_asset_splitting(self): def test_asset_splitting(self):
asset = create_asset( asset = create_asset(
@@ -1376,6 +1452,7 @@ def create_asset(**args):
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0, "number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
"gross_purchase_amount": args.gross_purchase_amount or 100000, "gross_purchase_amount": args.gross_purchase_amount or 100000,
"purchase_receipt_amount": args.purchase_receipt_amount or 100000, "purchase_receipt_amount": args.purchase_receipt_amount or 100000,
"maintenance_required": args.maintenance_required or 0,
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
"available_for_use_date": args.available_for_use_date or "2020-06-06", "available_for_use_date": args.available_for_use_date or "2020-06-06",
"location": args.location or "Test Location", "location": args.location or "Test Location",

View File

@@ -12,8 +12,11 @@ from six import string_types
import erpnext import erpnext
from erpnext.assets.doctype.asset.depreciation import ( from erpnext.assets.doctype.asset.depreciation import (
depreciate_asset,
get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_disposal,
get_value_after_depreciation_on_disposal_date, get_value_after_depreciation_on_disposal_date,
reset_depreciation_schedule,
reverse_depreciation_entry_made_after_disposal,
) )
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import ( from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import (
@@ -424,7 +427,7 @@ class AssetCapitalization(StockController):
asset = self.get_asset(item) asset = self.get_asset(item)
if asset.calculate_depreciation: if asset.calculate_depreciation:
self.depreciate_asset(asset) depreciate_asset(asset, self.posting_date)
asset.reload() asset.reload()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
@@ -520,8 +523,8 @@ class AssetCapitalization(StockController):
self.set_consumed_asset_status(asset) self.set_consumed_asset_status(asset)
if asset.calculate_depreciation: if asset.calculate_depreciation:
self.reverse_depreciation_entry_made_after_disposal(asset) reverse_depreciation_entry_made_after_disposal(asset, self.posting_date)
self.reset_depreciation_schedule(asset) reset_depreciation_schedule(asset, self.posting_date)
def get_asset(self, item): def get_asset(self, item):
asset = frappe.get_doc("Asset", item.asset) asset = frappe.get_doc("Asset", item.asset)

View File

@@ -20,7 +20,6 @@
"maintain_same_rate", "maintain_same_rate",
"allow_multiple_items", "allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice", "bill_for_rejected_quantity_in_purchase_invoice",
"enable_discount_accounting",
"subcontract", "subcontract",
"backflush_raw_materials_of_subcontract_based_on", "backflush_raw_materials_of_subcontract_based_on",
"column_break_11", "column_break_11",
@@ -134,13 +133,6 @@
{ {
"fieldname": "column_break_12", "fieldname": "column_break_12",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"default": "0",
"description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account",
"fieldname": "enable_discount_accounting",
"fieldtype": "Check",
"label": "Enable Discount Accounting for Buying"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@@ -148,7 +140,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2022-09-01 18:01:34.994657", "modified": "2022-09-27 10:50:27.050252",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View File

@@ -5,15 +5,10 @@
import frappe import frappe
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint
class BuyingSettings(Document): class BuyingSettings(Document):
def on_update(self):
self.toggle_discount_accounting_fields()
def validate(self): def validate(self):
for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]: for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]:
frappe.db.set_default(key, self.get(key, "")) frappe.db.set_default(key, self.get(key, ""))
@@ -26,60 +21,3 @@ class BuyingSettings(Document):
self.get("supp_master_name") == "Naming Series", self.get("supp_master_name") == "Naming Series",
hide_name_field=False, hide_name_field=False,
) )
def toggle_discount_accounting_fields(self):
enable_discount_accounting = cint(self.enable_discount_accounting)
make_property_setter(
"Purchase Invoice Item",
"discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
if enable_discount_accounting:
make_property_setter(
"Purchase Invoice Item",
"discount_account",
"mandatory_depends_on",
"eval: doc.discount_amount",
"Code",
validate_fields_for_doctype=False,
)
else:
make_property_setter(
"Purchase Invoice Item",
"discount_account",
"mandatory_depends_on",
"",
"Code",
validate_fields_for_doctype=False,
)
make_property_setter(
"Purchase Invoice",
"additional_discount_account",
"hidden",
not (enable_discount_accounting),
"Check",
validate_fields_for_doctype=False,
)
if enable_discount_accounting:
make_property_setter(
"Purchase Invoice",
"additional_discount_account",
"mandatory_depends_on",
"eval: doc.discount_amount",
"Code",
validate_fields_for_doctype=False,
)
else:
make_property_setter(
"Purchase Invoice",
"additional_discount_account",
"mandatory_depends_on",
"",
"Code",
validate_fields_for_doctype=False,
)

View File

@@ -33,6 +33,7 @@ frappe.ui.form.on("Purchase Order", {
frm.set_query("fg_item", "items", function() { frm.set_query("fg_item", "items", function() {
return { return {
filters: { filters: {
'is_stock_item': 1,
'is_sub_contracted_item': 1, 'is_sub_contracted_item': 1,
'default_bom': ['!=', ''] 'default_bom': ['!=', '']
} }

View File

@@ -38,7 +38,6 @@ from erpnext.accounts.party import (
validate_party_frozen_disabled, validate_party_frozen_disabled,
) )
from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year
from erpnext.assets.doctype.asset.depreciation import make_depreciation_entry
from erpnext.buying.utils import update_last_purchase_rate from erpnext.buying.utils import update_last_purchase_rate
from erpnext.controllers.print_settings import ( from erpnext.controllers.print_settings import (
set_print_templates_for_item_table, set_print_templates_for_item_table,
@@ -1891,88 +1890,6 @@ class AccountsController(TransactionBase):
_("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx) _("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx)
) )
def depreciate_asset(self, asset):
asset.flags.ignore_validate_update_after_submit = True
asset.prepare_depreciation_data(date_of_disposal=self.posting_date)
asset.save()
make_depreciation_entry(asset.name, self.posting_date)
def reset_depreciation_schedule(self, asset):
asset.flags.ignore_validate_update_after_submit = True
# recreate original depreciation schedule of the asset
asset.prepare_depreciation_data(date_of_return=self.posting_date)
self.modify_depreciation_schedule_for_asset_repairs(asset)
asset.save()
def modify_depreciation_schedule_for_asset_repairs(self, asset):
asset_repairs = frappe.get_all(
"Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"]
)
for repair in asset_repairs:
if repair.increase_in_asset_life:
asset_repair = frappe.get_doc("Asset Repair", repair.name)
asset_repair.modify_depreciation_schedule()
asset.prepare_depreciation_data()
def reverse_depreciation_entry_made_after_disposal(self, asset):
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
posting_date_of_original_disposal = self.get_posting_date_of_disposal_entry()
row = -1
finance_book = asset.get("schedules")[0].get("finance_book")
for schedule in asset.get("schedules"):
if schedule.finance_book != finance_book:
row = 0
finance_book = schedule.finance_book
else:
row += 1
if schedule.schedule_date == posting_date_of_original_disposal:
if not self.disposal_was_made_on_original_schedule_date(
asset, schedule, row, posting_date_of_original_disposal
) or self.disposal_happens_in_the_future(posting_date_of_original_disposal):
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate()
frappe.flags.is_reverse_depr_entry = True
reverse_journal_entry.submit()
frappe.flags.is_reverse_depr_entry = False
asset.flags.ignore_validate_update_after_submit = True
schedule.journal_entry = None
asset.save()
def get_posting_date_of_disposal_entry(self):
if self.doctype == "Sales Invoice" and self.return_against:
return frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
else:
return self.posting_date
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
def disposal_was_made_on_original_schedule_date(
self, asset, schedule, row, posting_date_of_disposal
):
for finance_book in asset.get("finance_books"):
if schedule.finance_book == finance_book.finance_book:
orginal_schedule_date = add_months(
finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation)
)
if orginal_schedule_date == posting_date_of_disposal:
return True
return False
def disposal_happens_in_the_future(self, posting_date_of_disposal):
if posting_date_of_disposal > getdate():
return True
return False
@frappe.whitelist() @frappe.whitelist()
def get_tax_rate(account_head): def get_tax_rate(account_head):

View File

@@ -212,21 +212,15 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
meta = frappe.get_meta(doctype, cached=True) meta = frappe.get_meta(doctype, cached=True)
searchfields = meta.get_search_fields() searchfields = meta.get_search_fields()
# these are handled separately
ignored_search_fields = ("item_name", "description")
for ignored_field in ignored_search_fields:
if ignored_field in searchfields:
searchfields.remove(ignored_field)
columns = "" columns = ""
extra_searchfields = [ extra_searchfields = [field for field in searchfields if not field in ["name", "description"]]
field
for field in searchfields
if not field in ["name", "item_group", "description", "item_name"]
]
if extra_searchfields: if extra_searchfields:
columns = ", " + ", ".join(extra_searchfields) columns += ", " + ", ".join(extra_searchfields)
if "description" in searchfields:
columns += """, if(length(tabItem.description) > 40, \
concat(substr(tabItem.description, 1, 40), "..."), description) as description"""
searchfields = searchfields + [ searchfields = searchfields + [
field field
@@ -266,12 +260,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
if frappe.db.count(doctype, cache=True) < 50000: if frappe.db.count(doctype, cache=True) < 50000:
# scan description only if items are less than 50000 # scan description only if items are less than 50000
description_cond = "or tabItem.description LIKE %(txt)s" description_cond = "or tabItem.description LIKE %(txt)s"
return frappe.db.sql( return frappe.db.sql(
"""select """select
tabItem.name, tabItem.item_name, tabItem.item_group, tabItem.name {columns}
if(length(tabItem.description) > 40, \
concat(substr(tabItem.description, 1, 40), "..."), description) as description
{columns}
from tabItem from tabItem
where tabItem.docstatus < 2 where tabItem.docstatus < 2
and tabItem.disabled=0 and tabItem.disabled=0

View File

@@ -69,9 +69,18 @@ class SubcontractingController(StockController):
def validate_items(self): def validate_items(self):
for item in self.items: for item in self.items:
if not frappe.get_value("Item", item.item_code, "is_sub_contracted_item"): is_stock_item, is_sub_contracted_item = frappe.get_value(
"Item", item.item_code, ["is_stock_item", "is_sub_contracted_item"]
)
if not is_stock_item:
msg = f"Item {item.item_name} must be a stock item."
frappe.throw(_(msg))
if not is_sub_contracted_item:
msg = f"Item {item.item_name} must be a subcontracted item." msg = f"Item {item.item_name} must be a subcontracted item."
frappe.throw(_(msg)) frappe.throw(_(msg))
if item.bom: if item.bom:
bom = frappe.get_doc("BOM", item.bom) bom = frappe.get_doc("BOM", item.bom)
if not bom.is_active: if not bom.is_active:
@@ -841,7 +850,7 @@ def make_rm_stock_entry(
for fg_item_code in fg_item_code_list: for fg_item_code in fg_item_code_list:
for rm_item in rm_items: for rm_item in rm_items:
if rm_item.get("main_item_code") or rm_item.get("item_code") == fg_item_code: if rm_item.get("main_item_code") == fg_item_code or rm_item.get("item_code") == fg_item_code:
rm_item_code = rm_item.get("rm_item_code") rm_item_code = rm_item.get("rm_item_code")
items_dict = { items_dict = {

View File

@@ -508,6 +508,7 @@ accounting_dimension_doctypes = [
"Landed Cost Item", "Landed Cost Item",
"Asset Value Adjustment", "Asset Value Adjustment",
"Asset Repair", "Asset Repair",
"Asset Capitalization",
"Loyalty Program", "Loyalty Program",
"Stock Reconciliation", "Stock Reconciliation",
"POS Profile", "POS Profile",

View File

@@ -315,3 +315,4 @@ erpnext.patches.v14_0.fix_crm_no_of_employees
erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes
erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries
erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger
erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization

View File

@@ -100,6 +100,7 @@ def execute():
"mode_of_payment": loan.mode_of_payment, "mode_of_payment": loan.mode_of_payment,
"loan_account": loan.loan_account, "loan_account": loan.loan_account,
"payment_account": loan.payment_account, "payment_account": loan.payment_account,
"disbursement_account": loan.payment_account,
"interest_income_account": loan.interest_income_account, "interest_income_account": loan.interest_income_account,
"penalty_income_account": loan.penalty_income_account, "penalty_income_account": loan.penalty_income_account,
}, },
@@ -190,6 +191,7 @@ def create_loan_type(loan, loan_type_name, penalty_account):
loan_type_doc.company = loan.company loan_type_doc.company = loan.company
loan_type_doc.mode_of_payment = loan.mode_of_payment loan_type_doc.mode_of_payment = loan.mode_of_payment
loan_type_doc.payment_account = loan.payment_account loan_type_doc.payment_account = loan.payment_account
loan_type_doc.disbursement_account = loan.payment_account
loan_type_doc.loan_account = loan.loan_account loan_type_doc.loan_account = loan.loan_account
loan_type_doc.interest_income_account = loan.interest_income_account loan_type_doc.interest_income_account = loan.interest_income_account
loan_type_doc.penalty_income_account = penalty_account loan_type_doc.penalty_income_account = penalty_account

View File

@@ -0,0 +1,31 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
def execute():
accounting_dimensions = frappe.db.get_all(
"Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"]
)
if not accounting_dimensions:
return
doctype = "Asset Capitalization"
for d in accounting_dimensions:
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
if field:
continue
df = {
"fieldname": d.fieldname,
"label": d.label,
"fieldtype": "Link",
"options": d.document_type,
"insert_after": "accounting_dimensions_section",
}
create_custom_field(doctype, df, ignore_validate=True)
frappe.clear_cache(doctype=doctype)

View File

@@ -0,0 +1,115 @@
#page-order {
.main-column {
.page-content-wrapper {
.breadcrumb-container {
@media screen and (min-width: 567px) {
padding-left: var(--padding-sm);
}
}
.container.my-4 {
background-color: var(--fg-color);
@media screen and (min-width: 567px) {
padding: 1.25rem 1.5rem;
border-radius: var(--border-radius-md);
box-shadow: var(--card-shadow);
}
}
}
}
}
.indicator-container {
@media screen and (max-width: 567px) {
padding-bottom: 0.8rem;
}
}
.order-items {
padding: 1.5rem 0;
border-bottom: 1px solid var(--border-color);
color: var(--gray-700);
@media screen and (max-width: 567px) {
align-items: flex-start !important;
}
.col-2 {
@media screen and (max-width: 567px) {
flex: auto;
max-width: 28%;
}
}
.order-item-name {
font-size: var(--text-base);
font-weight: 500;
}
.btn:focus,
.btn:hover {
background-color: var(--control-bg);
}
.col-6 {
@media screen and (max-width: 567px) {
max-width: 100%;
}
&.order-item-name {
font-size: var(--text-base);
}
}
}
.item-grand-total {
font-size: var(--text-base);
}
.list-item-name,
.item-total,
.order-container,
.order-qty {
font-size: var(--text-md);
}
.d-s-n {
@media screen and (max-width: 567px) {
display: none;
}
}
.d-l-n {
@media screen and (min-width: 567px) {
display: none;
}
}
.border-btm {
border-bottom: 1px solid var(--border-color);
}
.order-taxes {
display: flex;
@media screen and (min-width: 567px) {
justify-content: flex-end;
}
.col-4 {
padding-right: 0;
.col-8 {
padding-left: 0;
padding-right: 0;
}
@media screen and (max-width: 567px) {
padding-left: 0;
flex: auto;
max-width: 100%;
}
}
}

View File

@@ -1,3 +1,4 @@
@import './order-page';
.filter-options { .filter-options {
max-height: 300px; max-height: 300px;
@@ -32,19 +33,29 @@
height: 24px; height: 24px;
} }
.website-list .result { .website-list {
margin-top: 2rem; background-color: var(--fg-color);
} padding: 0 var(--padding-lg);
border-radius: var(--border-radius-md);
.result { @media screen and (max-width: 567px) {
border-bottom: 1px solid var(--border-color); margin-left: -2rem;
}
&.result {
border-bottom: 1px solid var(--border-color);
}
} }
.transaction-list-item { .transaction-list-item {
padding: 1rem 0; padding: 1rem 0;
border-top: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
position: relative; position: relative;
&:only-child, &:last-child {
border: 0;
}
a.transaction-item-link { a.transaction-item-link {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -68,3 +79,13 @@
line-height: 1.3; line-height: 1.3;
} }
} }
.list-item-name, .item-total {
font-size: var(--font-size-sm);
}
.items-preview {
@media screen and (max-width: 567px) {
margin-top: 1rem;
}
}

View File

@@ -6,7 +6,7 @@ import json
import frappe import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import cstr, flt, nowdate, nowtime from frappe.utils import add_days, cstr, flt, nowdate, nowtime, today
from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.accounts.utils import get_balance_on from erpnext.accounts.utils import get_balance_on
@@ -1091,6 +1091,36 @@ class TestDeliveryNote(FrappeTestCase):
frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype}) frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype})
) )
def test_batch_expiry_for_delivery_note(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
item = make_item(
"_Test Batch Item For Return Check",
{
"is_purchase_item": 1,
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TBIRC.#####",
},
)
pi = make_purchase_receipt(qty=1, item_code=item.name)
dn = create_delivery_note(qty=1, item_code=item.name, batch_no=pi.items[0].batch_no)
dn.load_from_db()
batch_no = dn.items[0].batch_no
self.assertTrue(batch_no)
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
return_dn = make_return_doc(dn.doctype, dn.name)
return_dn.save().submit()
self.assertTrue(return_dn.docstatus == 1)
def create_delivery_note(**args): def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note") dn = frappe.new_doc("Delivery Note")
@@ -1117,6 +1147,7 @@ def create_delivery_note(**args):
"expense_account": args.expense_account or "Cost of Goods Sold - _TC", "expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no, "serial_no": args.serial_no,
"batch_no": args.batch_no or None,
"target_warehouse": args.target_warehouse, "target_warehouse": args.target_warehouse,
}, },
) )

View File

@@ -10,6 +10,31 @@ frappe.ui.form.on("Item", {
frm.add_fetch('attribute', 'to_range', 'to_range'); frm.add_fetch('attribute', 'to_range', 'to_range');
frm.add_fetch('attribute', 'increment', 'increment'); frm.add_fetch('attribute', 'increment', 'increment');
frm.add_fetch('tax_type', 'tax_rate', 'tax_rate'); frm.add_fetch('tax_type', 'tax_rate', 'tax_rate');
frm.make_methods = {
'Sales Order': () => {
open_form(frm, "Sales Order", "Sales Order Item", "items");
},
'Delivery Note': () => {
open_form(frm, "Delivery Note", "Delivery Note Item", "items");
},
'Sales Invoice': () => {
open_form(frm, "Sales Invoice", "Sales Invoice Item", "items");
},
'Purchase Order': () => {
open_form(frm, "Purchase Order", "Purchase Order Item", "items");
},
'Purchase Receipt': () => {
open_form(frm, "Purchase Receipt", "Purchase Receipt Item", "items");
},
'Purchase Invoice': () => {
open_form(frm, "Purchase Invoice", "Purchase Invoice Item", "items");
},
'Material Request': () => {
open_form(frm, "Material Request", "Material Request Item", "items");
},
};
}, },
onload: function(frm) { onload: function(frm) {
erpnext.item.setup_queries(frm); erpnext.item.setup_queries(frm);
@@ -858,3 +883,17 @@ frappe.tour['Item'] = [
]; ];
function open_form(frm, doctype, child_doctype, parentfield) {
frappe.model.with_doctype(doctype, () => {
let new_doc = frappe.model.get_new_doc(doctype);
let new_child_doc = frappe.model.add_child(new_doc, child_doctype, parentfield);
new_child_doc.item_code = frm.doc.name;
new_child_doc.item_name = frm.doc.item_name;
new_child_doc.uom = frm.doc.stock_uom;
new_child_doc.description = frm.doc.description;
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
});
}

View File

@@ -937,17 +937,21 @@ class Item(Document):
"Purchase Order Item", "Purchase Order Item",
"Material Request Item", "Material Request Item",
"Product Bundle", "Product Bundle",
"BOM",
] ]
for doctype in linked_doctypes: for doctype in linked_doctypes:
filters = {"item_code": self.name, "docstatus": 1} filters = {"item_code": self.name, "docstatus": 1}
if doctype == "Product Bundle": if doctype in ("Product Bundle", "BOM"):
filters = {"new_item_code": self.name} if doctype == "Product Bundle":
filters = {"new_item_code": self.name}
fieldname = "new_item_code as docname"
else:
filters = {"item": self.name, "docstatus": 1}
fieldname = "name as docname"
if linked_doc := frappe.db.get_value( if linked_doc := frappe.db.get_value(doctype, filters, fieldname, as_dict=True):
doctype, filters, ["new_item_code as docname"], as_dict=True
):
return linked_doc.update({"doctype": doctype}) return linked_doc.update({"doctype": doctype})
elif doctype in ( elif doctype in (

View File

@@ -5,6 +5,7 @@
import json import json
import frappe import frappe
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.test_runner import make_test_objects from frappe.test_runner import make_test_objects
from frappe.tests.utils import FrappeTestCase, change_settings from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, today from frappe.utils import add_days, today
@@ -816,6 +817,30 @@ class TestItem(FrappeTestCase):
item.reload() item.reload()
self.assertEqual(item.is_stock_item, 1) self.assertEqual(item.is_stock_item, 1)
def test_serach_fields_for_item(self):
from erpnext.controllers.queries import item_query
make_property_setter("Item", None, "search_fields", "item_name", "Data", for_doctype="Doctype")
item = make_item(properties={"item_name": "Test Item", "description": "Test Description"})
data = item_query(
"Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True
)
self.assertEqual(data[0].name, item.name)
self.assertEqual(data[0].item_name, item.item_name)
self.assertTrue("description" not in data[0])
make_property_setter(
"Item", None, "search_fields", "item_name, description", "Data", for_doctype="Doctype"
)
data = item_query(
"Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True
)
self.assertEqual(data[0].name, item.name)
self.assertEqual(data[0].item_name, item.item_name)
self.assertEqual(data[0].description, item.description)
self.assertTrue("description" in data[0])
def set_item_variant_settings(fields): def set_item_variant_settings(fields):
doc = frappe.get_doc("Item Variant Settings") doc = frappe.get_doc("Item Variant Settings")

View File

@@ -183,7 +183,7 @@ class PickList(Document):
frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx)) frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx))
item_code = item.item_code item_code = item.item_code
reference = item.sales_order_item or item.material_request_item reference = item.sales_order_item or item.material_request_item
key = (item_code, item.uom, item.warehouse, reference) key = (item_code, item.uom, item.warehouse, item.batch_no, reference)
item.idx = None item.idx = None
item.name = None item.name = None

View File

@@ -153,7 +153,9 @@ class StockLedgerEntry(Document):
def validate_batch(self): def validate_batch(self):
if self.batch_no and self.voucher_type != "Stock Entry": if self.batch_no and self.voucher_type != "Stock Entry":
if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and self.actual_qty < 0: if (self.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and self.actual_qty < 0) or (
self.voucher_type in ["Delivery Note", "Sales Invoice"] and self.actual_qty > 0
):
return return
expiry_date = frappe.db.get_value("Batch", self.batch_no, "expiry_date") expiry_date = frappe.db.get_value("Batch", self.batch_no, "expiry_date")

View File

@@ -6,7 +6,7 @@
aria-label="{{ _('Your email address...') }}" aria-label="{{ _('Your email address...') }}"
aria-describedby="footer-subscribe-button"> aria-describedby="footer-subscribe-button">
<div class="input-group-append"> <div class="input-group-append">
<button class="btn btn-sm btn-default" <button class="btn btn-sm btn-secondary pl-3 pr-3 ml-2"
type="button" id="footer-subscribe-button">{{ _("Get Updates") }}</button> type="button" id="footer-subscribe-button">{{ _("Get Updates") }}</button>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
{% macro product_image_square(website_image, css_class="") %} {% macro product_image_square(website_image, css_class="") %}
<div class="product-image product-image-square <div class="product-image product-image-square h-100 rounded
{% if not website_image -%} missing-image {%- endif %} {{ css_class }}" {% if not website_image -%} missing-image {%- endif %} {{ css_class }}"
{% if website_image -%} {% if website_image -%}
style="background-image: url('{{ frappe.utils.quoted(website_image) | abs_url }}');" style="background-image: url('{{ frappe.utils.quoted(website_image) | abs_url }}');"

View File

@@ -3,7 +3,7 @@
{% macro item_name_and_description(d) %} {% macro item_name_and_description(d) %}
<div class="row item_name_and_description"> <div class="row item_name_and_description">
<div class="col-xs-4 col-sm-2 order-image-col"> <div class="col-xs-4 col-sm-2 order-image-col">
<div class="order-image"> <div class="order-image h-100">
{% if d.thumbnail or d.image %} {% if d.thumbnail or d.image %}
{{ product_image(d.thumbnail or d.image, no_border=True) }} {{ product_image(d.thumbnail or d.image, no_border=True) }}
{% else %} {% else %}
@@ -18,6 +18,9 @@
<div class="text-muted small item-description"> <div class="text-muted small item-description">
{{ html2text(d.description) | truncate(140) }} {{ html2text(d.description) | truncate(140) }}
</div> </div>
<span class="text-muted mt-2 d-l-n order-qty">
{{ _("Qty ") }}({{ d.get_formatted("qty") }})
</span>
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}

View File

@@ -1,84 +1,111 @@
{% if doc.taxes %} {% if doc.taxes %}
<tr> <div class="w-100 order-taxes mt-5">
<td class="text-left" colspan="1"> <div class="col-4 d-flex border-btm pb-5">
{{ _("Net Total") }} <div class="item-grand-total col-8">
</td> {{ _("Net Total") }}
<td class="text-right totals" colspan="3"> </div>
{{ doc.get_formatted("net_total") }} <div class="item-grand-total col-4 text-right pr-0">
</td> {{ doc.get_formatted("net_total") }}
</tr> </div>
</div>
</div>
{% endif %} {% endif %}
{% for d in doc.taxes %} {% for d in doc.taxes %}
{% if d.base_tax_amount %} {% if d.base_tax_amount %}
<tr> <div class="order-taxes w-100 mt-5">
<td class="text-left" colspan="1"> <div class="col-4 d-flex border-btm pb-5">
{{ d.description }} <div class="item-grand-total col-8">
</td> {{ d.description }}
<td class="text-right totals" colspan="3"> </div>
{{ d.get_formatted("base_tax_amount") }} <div class="item-grand-total col-4 text-right pr-0">
</td> {{ doc.get_formatted("net_total") }}
</tr> </div>
</div>
</div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if doc.doctype == 'Quotation' %} {% if doc.doctype == 'Quotation' %}
{% if doc.coupon_code %} {% if doc.coupon_code %}
<tr> <div class="w-100 mt-5 order-taxes font-weight-bold">
<td class="text-left total-discount" colspan="1"> <div class="col-4 d-flex border-btm pb-5">
{{ _("Savings") }} <div class="item-grand-total col-8">
</td> {{ _("Savings") }}
<td class="text-right tot_quotation_discount total-discount totals" colspan="3"> </div>
{% set tot_quotation_discount = [] %} <div class="item-grand-total col-4 text-right pr-0">
{%- for item in doc.items -%} {% set tot_quotation_discount = [] %}
{% if tot_quotation_discount.append((((item.price_list_rate * item.qty) {%- for item in doc.items -%}
* item.discount_percentage) / 100)) %} {% if tot_quotation_discount.append((((item.price_list_rate * item.qty)
{% endif %} * item.discount_percentage) / 100)) %}
{% endfor %} {% endif %}
{{ frappe.utils.fmt_money((tot_quotation_discount | sum),currency=doc.currency) }} {% endfor %}
</td> {{ frappe.utils.fmt_money((tot_quotation_discount | sum),currency=doc.currency) }} </div>
</tr> </div>
</div>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if doc.doctype == 'Sales Order' %} {% if doc.doctype == 'Sales Order' %}
{% if doc.coupon_code %} {% if doc.coupon_code %}
<tr> <div class="w-100 order-taxes mt-5">
<td class="text-left total-discount" colspan="2" style="padding-right: 2rem;"> <div class="col-4 d-flex border-btm pb-5">
{{ _("Applied Coupon Code") }} <div class="item-grand-total col-8">
</td> {{ _("Total Amount") }}
<td class="text-right total-discount"> </div>
<span> <div class="item-grand-total col-4 text-right pr-0">
{%- for row in frappe.get_all(doctype="Coupon Code", <span>
fields=["coupon_code"], filters={ "name":doc.coupon_code}) -%} {% set total_amount = [] %}
<span>{{ row.coupon_code }}</span> {%- for item in doc.items -%}
{% endfor %} {% if total_amount.append((item.price_list_rate * item.qty)) %}{% endif %}
</span> {% endfor %}
</td> {{ frappe.utils.fmt_money((total_amount | sum),currency=doc.currency) }}
</tr> </span>
<tr> </div>
<td class="text-left total-discount" colspan="2"> </div>
{{ _("Savings") }} </div>
</td> <div class="order-taxes w-100 mt-5">
<td class="text-right total-discount"> <div class="col-4 d-flex">
<span> <div class="item-grand-total col-8">
{% set tot_SO_discount = [] %} {{ _("Applied Coupon Code") }}
{%- for item in doc.items -%} </div>
{% if tot_SO_discount.append((((item.price_list_rate * item.qty) <div class="item-grand-total col-4 text-right pr-0">
* item.discount_percentage) / 100)) %}{% endif %} <span>
{% endfor %} {%- for row in frappe.get_all(doctype="Coupon Code",
{{ frappe.utils.fmt_money((tot_SO_discount | sum),currency=doc.currency) }} fields=["coupon_code"], filters={ "name":doc.coupon_code}) -%}
</span> <span>{{ row.coupon_code }}</span>
</td> {% endfor %}
</tr> </span>
</div>
</div>
</div>
<div class="order-taxes mt-5">
<div class="col-4 d-flex border-btm pb-5">
<div class="item-grand-total col-8">
{{ _("Savings") }}
</div>
<div class="item-grand-total col-4 text-right pr-0">
<span>
{% set tot_SO_discount = [] %}
{%- for item in doc.items -%}
{% if tot_SO_discount.append((((item.price_list_rate * item.qty)
* item.discount_percentage) / 100)) %}{% endif %}
{% endfor %}
{{ frappe.utils.fmt_money((tot_SO_discount | sum),currency=doc.currency) }}
</span>
</div>
</div>
</div>
{% endif %} {% endif %}
{% endif %} {% endif %}
<tr> <div class="w-100 mt-5 order-taxes font-weight-bold">
<th class="text-left item-grand-total" colspan="1"> <div class="col-4 d-flex">
{{ _("Grand Total") }} <div class="item-grand-total col-8">
</th> {{ _("Grand Total") }}
<th class="text-right item-grand-total totals" colspan="3"> </div>
{{ doc.get_formatted("grand_total") }} <div class="item-grand-total col-4 text-right pr-0">
</th> {{ doc.get_formatted("grand_total") }}
</tr> </div>
</div>
</div>

View File

@@ -1,20 +1,22 @@
<div class="web-list-item transaction-list-item"> <div class="web-list-item transaction-list-item">
<div class="row"> <div class="row align-items-center">
<div class="col-sm-4"> <div class="col-sm-4">
<span class="indicator small {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "gray") }}"> <span class="list-item-name font-weight-bold">{{ doc.name }}</span>
{{ doc.name }}</span>
<div class="small text-muted transaction-time" <div class="small text-muted transaction-time"
title="{{ frappe.utils.format_datetime(doc.modified, "medium") }}"> title="{{ frappe.utils.format_datetime(doc.modified, "medium") }}">
{{ frappe.utils.global_date_format(doc.modified) }} {{ frappe.utils.global_date_format(doc.modified) }}
</div> </div>
</div> </div>
<div class="col-sm-5"> <div class="col-sm-3">
<span class="indicator-pill {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "gray") }} list-item-status">{{doc.status}}</span>
</div>
<div class="col-sm-2">
<div class="small text-muted items-preview ellipsis ellipsis-width"> <div class="small text-muted items-preview ellipsis ellipsis-width">
{{ doc.items_preview }} {{ doc.items_preview }}
</div> </div>
</div> </div>
{% if doc.get('grand_total') %} {% if doc.get('grand_total') %}
<div class="col-sm-3 text-right bold"> <div class="col-sm-3 text-right font-weight-bold item-total">
{{ doc.get_formatted("grand_total") }} {{ doc.get_formatted("grand_total") }}
</div> </div>
{% endif %} {% endif %}

View File

@@ -5,149 +5,159 @@
{% include "templates/includes/breadcrumbs.html" %} {% include "templates/includes/breadcrumbs.html" %}
{% endblock %} {% endblock %}
{% block title %}{{ doc.name }}{% endblock %} {% block title %}
{{ doc.name }}
{% endblock %}
{% block header %} {% block header %}
<h2 class="m-0">{{ doc.name }}</h2> <h3 class="m-0">{{ doc.name }}</h3>
{% endblock %} {% endblock %}
{% block header_actions %} {% block header_actions %}
<div class="dropdown"> <div class="row">
<button class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <div class="dropdown">
<span class="font-md">{{ _('Actions') }}</span> <button class="btn btn-sm btn-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<b class="caret"></b> <span class="font-md">{{ _('Actions') }}</span>
</button> <b class="caret"></b>
<ul class="dropdown-menu dropdown-menu-right" role="menu"> </button>
{% if doc.doctype == 'Purchase Order' %} <ul class="dropdown-menu dropdown-menu-right" role="menu">
<a class="dropdown-item" href="/api/method/erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_invoice_from_portal?purchase_order_name={{ doc.name }}" data-action="make_purchase_invoice">{{ _("Make Purchase Invoice") }}</a> {% if doc.doctype == 'Purchase Order' and show_make_pi_button %}
{% endif %} <a class="dropdown-item"
<a class="dropdown-item" href='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}' href="/api/method/erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_invoice_from_portal?purchase_order_name={{ doc.name }}"
target="_blank" rel="noopener noreferrer"> data-action="make_purchase_invoice">{{ _("Make Purchase Invoice") }}
{{ _("Print") }} </a>
</a> {% endif %}
</ul> <a class="dropdown-item"
href='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}' target="_blank"
rel="noopener noreferrer">
{{ _("Print") }}
</a>
</ul>
</div>
<div class="form-column col-sm-6">
<div class="page-header-actions-block" data-html-block="header-actions">
<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") }}
</a>
</p>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}
{% block page_content %} {% block page_content %}
<div class="row transaction-subheading"> <div>
<div class="col-6"> <div class="row transaction-subheading mt-1">
<span class="font-md indicator-pill {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "darkgrey") }}"> <div class="col-6 text-muted small mt-1">
{% if doc.doctype == "Quotation" and not doc.docstatus %} {{ frappe.utils.format_date(doc.transaction_date, 'medium') }}
{{ _("Pending") }} {% if doc.valid_till %}
{% else %} <p>
{{ _(doc.get('indicator_title')) or _(doc.status) or _("Submitted") }} {{ _("Valid Till") }}: {{ frappe.utils.format_date(doc.valid_till, 'medium') }}
</p>
{% endif %} {% endif %}
</span> </div>
</div> </div>
<div class="col-6 text-muted text-right small pt-3"> <div class="row indicator-container mt-2">
{{ frappe.utils.format_date(doc.transaction_date, 'medium') }} <div class="col-10">
{% if doc.valid_till %} <span class="indicator-pill {{ doc.indicator_color or (" blue" if doc.docstatus==1 else "darkgrey" ) }}">
<p> {% if doc.doctype == "Quotation" and not doc.docstatus %}
{{ _("Valid Till") }}: {{ frappe.utils.format_date(doc.valid_till, 'medium') }} {{ _("Pending") }}
</p> {% else %}
{% endif %} {{ _(doc.get('indicator_title')) or _(doc.status) or _("Submitted") }}
</div>
</div>
<p class="small my-3">
{%- set party_name = doc.supplier_name if doc.doctype in ['Supplier Quotation', 'Purchase Invoice', 'Purchase Order'] else doc.customer_name %}
<b>{{ party_name }}</b>
{% if doc.contact_display and doc.contact_display != party_name %}
<br>
{{ doc.contact_display }}
{% endif %}
</p>
{% if doc._header %}
{{ doc._header }}
{% endif %}
<div class="order-container">
<!-- items -->
<table class="order-item-table w-100 table">
<thead class="order-items order-item-header">
<th width="60%">
{{ _("Item") }}
</th>
<th width="20%" class="text-right">
{{ _("Quantity") }}
</th>
<th width="20%" class="text-right">
{{ _("Amount") }}
</th>
</thead>
<tbody>
{% for d in doc.items %}
<tr class="order-items">
<td>
{{ item_name_and_description(d) }}
</td>
<td class="text-right">
{{ d.qty }}
{% if d.delivered_qty is defined and d.delivered_qty != None %}
<p class="text-muted small">{{ _("Delivered") }}&nbsp;{{ d.delivered_qty }}</p>
{% endif %} {% endif %}
</td> </span>
<td class="text-right"> </div>
{{ d.get_formatted("amount") }} <div class="text-right col-2">
<p class="text-muted small">{{ _("Rate:") }}&nbsp;{{ d.get_formatted("rate") }}</p> {%- set party_name = doc.supplier_name if doc.doctype in ['Supplier Quotation', 'Purchase Invoice', 'Purchase
</td> Order'] else doc.customer_name %}
</tr> <b>{{ party_name }}</b>
{% endfor %}
</tbody> {% if doc.contact_display and doc.contact_display != party_name %}
</table> <br>
<!-- taxes --> {{ doc.contact_display }}
<div class="order-taxes d-flex justify-content-end"> {% endif %}
<table> </div>
</div>
{% if doc._header %}
{{ doc._header }}
{% endif %}
<div class="order-container mt-4">
<!-- items -->
<div class="w-100">
<div class="order-items order-item-header mb-1 row text-muted">
<span class="col-5">
{{ _("Item") }}
</span>
<span class="d-s-n col-3">
{{ _("Quantity") }}
</span>
<span class="col-2 pl-10">
{{ _("Rate") }}
</span>
<span class="col-2 text-right">
{{ _("Amount") }}
</span>
</div>
{% for d in doc.items %}
<div class="order-items row align-items-center">
<span class="order-item-name col-5 pr-0">
{{ item_name_and_description(d) }}
</span>
<span class="d-s-n col-3 pl-10">
{{ d.get_formatted("qty") }}
</span>
<span class="order-rate pl-4 col-2">
{{ d.get_formatted("rate") }}
</span>
<span class="col-2 text-right">
{{ d.get_formatted("amount") }}
</span>
</div>
{% endfor %}
</div>
<!-- taxes -->
<div class="">
{% include "erpnext/templates/includes/order/order_taxes.html" %} {% include "erpnext/templates/includes/order/order_taxes.html" %}
</table> </div>
</div> </div>
</div> </div>
{% if enabled_checkout and ((doc.doctype=="Sales Order" and doc.per_billed <= 0) {% if enabled_checkout and ((doc.doctype=="Sales Order" and doc.per_billed <= 0)
or (doc.doctype=="Sales Invoice" and doc.outstanding_amount > 0)) %} or (doc.doctype=="Sales Invoice" and doc.outstanding_amount> 0)) %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="form-column col-sm-6 address-title">
<strong>Payment</strong>
</div>
</div>
</div>
<div class="panel-collapse"> <div class="panel-collapse">
<div class="panel-body text-muted small"> <div class="panel-body text-muted small">
<div class="row"> <div class="row">
<div class="form-column col-sm-6"> <div class="form-column col-sm-6">
{% if available_loyalty_points %} {% if available_loyalty_points %}
<div class="panel-heading">
<div class="row">
<div class="form-column col-sm-6 address-title">
<strong>Loyalty Points</strong>
</div>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div class="h6">Enter Loyalty Points</div> <div class="h6">Enter Loyalty Points</div>
<div class="control-input-wrapper"> <div class="control-input-wrapper">
<div class="control-input"> <div class="control-input">
<input class="form-control" type="number" min="0" max="{{ available_loyalty_points }}" id="loyalty-point-to-redeem"> <input class="form-control" type="number" min="0"
max="{{ available_loyalty_points }}" id="loyalty-point-to-redeem">
</div> </div>
<p class="help-box small text-muted d-none d-sm-block"> Available Points: {{ available_loyalty_points }} </p> <p class="help-box small text-muted d-none d-sm-block"> Available Points: {{
available_loyalty_points }} </p>
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="form-column col-sm-6">
<div id="loyalty-points-status" style="text-align: right"></div>
<div class="page-header-actions-block" data-html-block="header-actions">
<p class="mt-2" style="float: right;">
<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") }}
</a>
</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -172,17 +182,17 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div>
{% if doc.terms %} {% if doc.terms %}
<div class="terms-and-condition text-muted small"> <div class="terms-and-condition text-muted small">
<hr><p>{{ doc.terms }}</p> <hr>
<p>{{ doc.terms }}</p>
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block script %} {% block script %}
<script> {% include "templates/pages/order.js" %} </script> <script> {% include "templates/pages/order.js" %}</script>
<script> <script>
window.doc_info = { window.doc_info = {
customer: '{{doc.customer}}', customer: '{{doc.customer}}',
@@ -192,4 +202,4 @@
currency: '{{ doc.currency }}' currency: '{{ doc.currency }}'
} }
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -52,6 +52,9 @@ def get_context(context):
) )
context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points")) context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points"))
# show Make Purchase Invoice button based on permission
context.show_make_pi_button = frappe.has_permission("Purchase Invoice", "create")
def get_attachments(dt, dn): def get_attachments(dt, dn):
return frappe.get_all( return frappe.get_all(

View File

@@ -9,7 +9,6 @@ from frappe import _
def transaction_processing(data, from_doctype, to_doctype): def transaction_processing(data, from_doctype, to_doctype):
if isinstance(data, str): if isinstance(data, str):
deserialized_data = json.loads(data) deserialized_data = json.loads(data)
else: else:
deserialized_data = data deserialized_data = data
@@ -30,30 +29,29 @@ def transaction_processing(data, from_doctype, to_doctype):
def job(deserialized_data, from_doctype, to_doctype): def job(deserialized_data, from_doctype, to_doctype):
failed_history = [] fail_count = 0
i = 0
for d in deserialized_data: for d in deserialized_data:
failed = []
try: try:
i += 1
doc_name = d.get("name") doc_name = d.get("name")
frappe.db.savepoint("before_creation_state") frappe.db.savepoint("before_creation_state")
task(doc_name, from_doctype, to_doctype) task(doc_name, from_doctype, to_doctype)
except Exception as e: except Exception as e:
frappe.db.rollback(save_point="before_creation_state") frappe.db.rollback(save_point="before_creation_state")
failed_history.append(e) fail_count += 1
failed.append(e)
update_logger( update_logger(
doc_name, e, from_doctype, to_doctype, status="Failed", log_date=str(date.today()) doc_name,
str(frappe.get_traceback()),
from_doctype,
to_doctype,
status="Failed",
log_date=str(date.today()),
) )
if not failed: else:
update_logger( update_logger(
doc_name, None, from_doctype, to_doctype, status="Success", log_date=str(date.today()) doc_name, None, from_doctype, to_doctype, status="Success", log_date=str(date.today())
) )
show_job_status(failed_history, deserialized_data, to_doctype) show_job_status(fail_count, len(deserialized_data), to_doctype)
def task(doc_name, from_doctype, to_doctype): def task(doc_name, from_doctype, to_doctype):
@@ -94,7 +92,7 @@ def task(doc_name, from_doctype, to_doctype):
"Purchase Invoice": purchase_order.make_purchase_invoice, "Purchase Invoice": purchase_order.make_purchase_invoice,
"Purchase Receipt": purchase_order.make_purchase_receipt, "Purchase Receipt": purchase_order.make_purchase_receipt,
}, },
"Purhcase Invoice": { "Purchase Invoice": {
"Purchase Receipt": purchase_invoice.make_purchase_receipt, "Purchase Receipt": purchase_invoice.make_purchase_receipt,
"Payment": payment_entry.get_payment_entry, "Payment": payment_entry.get_payment_entry,
}, },
@@ -150,15 +148,14 @@ def update_logger(doc_name, e, from_doctype, to_doctype, status, log_date=None,
log_doc.save() log_doc.save()
def show_job_status(failed_history, deserialized_data, to_doctype): def show_job_status(fail_count, deserialized_data_count, to_doctype):
if not failed_history: if not fail_count:
frappe.msgprint( frappe.msgprint(
_("Creation of {0} successful").format(to_doctype), _("Creation of {0} successful").format(to_doctype),
title="Successful", title="Successful",
indicator="green", indicator="green",
) )
elif fail_count != 0 and fail_count < deserialized_data_count:
if len(failed_history) != 0 and len(failed_history) < len(deserialized_data):
frappe.msgprint( frappe.msgprint(
_( _(
"""Creation of {0} partially successful. """Creation of {0} partially successful.
@@ -167,8 +164,7 @@ def show_job_status(failed_history, deserialized_data, to_doctype):
title="Partially successful", title="Partially successful",
indicator="orange", indicator="orange",
) )
else:
if len(failed_history) == len(deserialized_data):
frappe.msgprint( frappe.msgprint(
_( _(
"""Creation of {0} failed. """Creation of {0} failed.
@@ -180,9 +176,7 @@ def show_job_status(failed_history, deserialized_data, to_doctype):
def record_exists(log_doc, doc_name, status): def record_exists(log_doc, doc_name, status):
record = mark_retrired_transaction(log_doc, doc_name) record = mark_retrired_transaction(log_doc, doc_name)
if record and status == "Failed": if record and status == "Failed":
return False return False
elif record and status == "Success": elif record and status == "Success":

View File

@@ -45,7 +45,7 @@
.time-slot.selected { .time-slot.selected {
color: white; color: white;
background: #5e64ff; background: var(--primary-color);
} }
.time-slot.selected .text-muted { .time-slot.selected .text-muted {